Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / oa2 / backends / base.py @ 252eb705

History | View | Annotate | Download (21.5 kB)

1
import urllib
2
import urlparse
3
import uuid
4
import datetime
5
import json
6

    
7
from base64 import b64encode, b64decode
8
from hashlib import sha512
9
from time import time, mktime
10

    
11

    
12
import logging
13
logger = logging.getLogger(__name__)
14

    
15

    
16
def handles_oa2_requests(func):
17
    def wrapper(self, *args, **kwargs):
18
        if not self._errors_to_http:
19
            return func(self, *args, **kwargs)
20
        try:
21
            return func(self, *args, **kwargs)
22
        except OA2Error, e:
23
            return self.build_response_from_error(e)
24
    return wrapper
25

    
26

    
27
class OA2Error(Exception):
28
    error = None
29

    
30

    
31
class InvalidClientID(OA2Error):
32
    pass
33

    
34

    
35
class NotAuthenticatedError(OA2Error):
36
    pass
37

    
38

    
39
class InvalidClientRedirectUrl(OA2Error):
40
    pass
41

    
42

    
43
class InvalidAuthorizationRequest(OA2Error):
44
    pass
45

    
46

    
47
class Response(object):
48

    
49
    def __init__(self, status, body='', headers=None,
50
                 content_type='plain/text'):
51
        if not body:
52
            body = ''
53
        if not headers:
54
            headers = {}
55

    
56
        self.status = status
57
        self.body = body
58
        self.headers = headers
59
        self.content_type = content_type
60

    
61
    def __repr__(self):
62
        return "%d RESPONSE (BODY: %r, HEADERS: %r)" % (self.status,
63
                                                        self.body,
64
                                                        self.headers)
65

    
66

    
67
class Request(object):
68

    
69
    def __init__(self, method, path, GET=None, POST=None, META=None,
70
                 secure=False, user=None):
71
        self.method = method
72
        self.path = path
73

    
74
        if not GET:
75
            GET = {}
76
        if not POST:
77
            POST = {}
78
        if not META:
79
            META = {}
80

    
81
        self.secure = secure
82
        self.GET = GET
83
        self.POST = POST
84
        self.META = META
85
        self.user = user
86

    
87
    def __repr__(self):
88
        prepend = ""
89
        if self.secure:
90
            prepend = "SECURE "
91
        return "%s%s REQUEST (POST: %r, GET:%r, HEADERS:%r, " % (prepend,
92
                                                                 self.method,
93
                                                                 self.POST,
94
                                                                 self.GET,
95
                                                                 self.META)
96

    
97

    
98
class ORMAbstractBase(type):
99

    
100
    def __new__(cls, name, bases, attrs):
101
        attrs['ENTRIES'] = {}
102
        return super(ORMAbstractBase, cls).__new__(cls, name, bases, attrs)
103

    
104

    
105
class ORMAbstract(object):
106

    
107
    ENTRIES = {}
108

    
109
    __metaclass__ = ORMAbstractBase
110

    
111
    def __init__(self, **kwargs):
112
        for key, value in kwargs.iteritems():
113
            setattr(self, key, value)
114

    
115
    @classmethod
116
    def create(cls, id, **params):
117
        params = cls.clean_params(params)
118
        params['id'] = id
119
        cls.ENTRIES[id] = cls(**params)
120
        return cls.get(id)
121

    
122
    @classmethod
123
    def get(cls, pk):
124
        return cls.ENTRIES.get(pk)
125

    
126
    @classmethod
127
    def clean_params(cls, params):
128
        return params
129

    
130

    
131
class Client(ORMAbstract):
132

    
133
    def get_id(self):
134
        return self.id
135

    
136
    def get_redirect_uris(self):
137
        return self.uris
138

    
139
    def get_default_redirect_uri(self):
140
        return self.uris[0]
141

    
142
    def redirect_uri_is_valid(self, redirect_uri):
143
        split = urlparse.urlsplit(redirect_uri)
144
        if split.scheme not in urlparse.uses_query:
145
            raise OA2Error("Invalid redirect url scheme")
146
        uris = self.get_redirect_uris()
147
        return redirect_uri in uris
148

    
149
    def requires_auth(self):
150
        if self.client_type == 'confidential':
151
            return True
152
        return 'secret' in dir(self)
153

    
154
    def check_credentials(self, username, secret):
155
        return username == self.id and secret == self.secret
156

    
157

    
158
class Token(ORMAbstract):
159

    
160
    def to_dict(self):
161
        params = {
162
            'access_token': self.token,
163
            'token_type': self.token_type,
164
            'expires_in': self.expires,
165
        }
166
        if self.refresh_token:
167
            params['refresh_token'] = self.refresh_token
168
        return params
169

    
170

    
171
class AuthorizationCode(ORMAbstract):
172
    pass
173

    
174

    
175
class User(ORMAbstract):
176
    pass
177

    
178

    
179
class BackendBase(type):
180

    
181
    def __new__(cls, name, bases, attrs):
182
        super_new = super(BackendBase, cls).__new__
183
        #parents = [b for b in bases if isinstance(b, BackendBase)]
184
        #meta = attrs.pop('Meta', None)
185
        return super_new(cls, name, bases, attrs)
186

    
187
    @classmethod
188
    def get_orm_options(cls, attrs):
189
        meta = attrs.pop('ORM', None)
190
        orm = {}
191
        if meta:
192
            for attr in dir(meta):
193
                orm[attr] = getattr(meta, attr)
194
        return orm
195

    
196

    
197
class SimpleBackend(object):
198

    
199
    __metaclass__ = BackendBase
200

    
201
    base_url = ''
202
    endpoints_prefix = '/oa2/'
203

    
204
    token_endpoint = 'token/'
205
    token_length = 30
206
    token_expires = 300
207

    
208
    authorization_endpoint = 'auth/'
209
    authorization_code_length = 60
210
    authorization_response_types = ['code', 'token']
211

    
212
    grant_types = ['authorization_code']
213

    
214
    response_cls = Response
215
    request_cls = Request
216

    
217
    client_model = Client
218
    token_model = Token
219
    code_model = AuthorizationCode
220
    user_model = User
221

    
222
    def __init__(self, base_url='', endpoints_prefix='/oa2/', id='oa2',
223
                 **kwargs):
224
        self.base_url = base_url
225
        self.endpoints_prefix = endpoints_prefix
226
        self.id = id
227
        self._errors_to_http = kwargs.get('errors_to_http', True)
228

    
229
    # Request/response builders
230
    def build_request(self, method, get, post, meta):
231
        return self.request_cls(method=method, GET=get, POST=post, META=meta)
232

    
233
    def build_response(self, status, headers=None, body=''):
234
        return self.response_cls(status=status, headers=headers, body=body)
235

    
236
    # ORM Methods
237
    def create_authorization_code(self, user, client, code, redirect_uri,
238
                                  scope, state, **kwargs):
239
        code_params = {
240
            'code': code,
241
            'redirect_uri': redirect_uri,
242
            'client': client,
243
            'scope': scope,
244
            'state': state,
245
            'user': user
246
        }
247
        code_params.update(kwargs)
248
        code_instance = self.code_model.create(**code_params)
249
        logger.info('%r created' % code_instance)
250
        return code_instance
251

    
252
    def _token_params(self, value, token_type, authorization, scope):
253
        created_at = datetime.datetime.now()
254
        expires = self.token_expires
255
        expires_at = created_at + datetime.timedelta(seconds=expires)
256
        token_params = {
257
            'code': value,
258
            'token_type': token_type,
259
            'created_at': created_at,
260
            'expires_at': expires_at,
261
            'user': authorization.user,
262
            'redirect_uri': authorization.redirect_uri,
263
            'client': authorization.client,
264
            'scope': authorization.scope,
265
        }
266
        return token_params
267

    
268
    def create_token(self, value, token_type, authorization, scope,
269
                     refresh=False):
270
        params = self._token_params(value, token_type, authorization, scope)
271
        if refresh:
272
            refresh_token = self.generate_token()
273
            params['refresh_token'] = refresh_token
274
            # TODO: refresh token expires ???
275
        token = self.token_model.create(**params)
276
        logger.info('%r created' % token)
277
        return token
278

    
279
#    def delete_authorization_code(self, code):
280
#        del self.code_model.ENTRIES[code]
281

    
282
    def get_client_by_id(self, client_id):
283
        return self.client_model.get(client_id)
284

    
285
    def get_client_by_credentials(self, username, password):
286
        return None
287

    
288
    def get_authorization_code(self, code):
289
        return self.code_model.get(code)
290

    
291
    def get_client_authorization_code(self, client, code):
292
        code_instance = self.get_authorization_code(code)
293
        if not code_instance:
294
            raise OA2Error("Invalid code")
295

    
296
        if client.get_id() != code_instance.client.get_id():
297
            raise OA2Error("Mismatching client with code client")
298
        return code_instance
299

    
300
    def client_id_exists(self, client_id):
301
        return bool(self.get_client_by_id(client_id))
302

    
303
    def build_site_url(self, prefix='', **params):
304
        params = urllib.urlencode(params)
305
        return "%s%s%s%s" % (self.base_url, self.endpoints_prefix, prefix,
306
                             params)
307

    
308
    def _get_uri_base(self, uri):
309
        split = urlparse.urlsplit(uri)
310
        return "%s://%s%s" % (split.scheme, split.netloc, split.path)
311

    
312
    def build_client_redirect_uri(self, client, uri, **params):
313
        if not client.redirect_uri_is_valid(uri):
314
            raise OA2Error("Invalid redirect uri")
315
        params = urllib.urlencode(params)
316
        uri = self._get_uri_base(uri)
317
        return "%s?%s" % (uri, params)
318

    
319
    def generate_authorization_code(self):
320
        dg64 = b64encode(sha512(str(uuid.uuid4())).hexdigest())
321
        return dg64[:self.authorization_code_length]
322

    
323
    def generate_token(self, *args, **kwargs):
324
        dg64 = b64encode(sha512(str(uuid.uuid4())).hexdigest())
325
        return dg64[:self.token_length]
326

    
327
    def add_authorization_code(self, user, client, redirect_uri, scope, state,
328
                               **kwargs):
329
        code = self.generate_authorization_code()
330
        self.create_authorization_code(user, client, code, redirect_uri, scope,
331
                                       state, **kwargs)
332
        return code
333

    
334
    def add_token_for_client(self, token_type, authorization, refresh=False):
335
        token = self.generate_token()
336
        self.create_token(token, token_type, authorization, refresh)
337
        return token
338

    
339
    #
340
    # Response helpers
341
    #
342

    
343
    def grant_accept_response(self, client, redirect_uri, scope, state):
344
        context = {'client': client.get_id(), 'redirect_uri': redirect_uri,
345
                   'scope': scope, 'state': state,
346
                   #'url': url,
347
                   }
348
        json_content = json.dumps(context)
349
        return self.response_cls(status=200, body=json_content)
350

    
351
    def grant_token_response(self, token, token_type):
352
        context = {'access_token': token, 'token_type': token_type,
353
                   'expires_in': self.token_expires}
354
        json_content = json.dumps(context)
355
        return self.response_cls(status=200, body=json_content)
356

    
357
    def redirect_to_login_response(self, request, params):
358
        parts = list(urlparse.urlsplit(request.path))
359
        parts[3] = urllib.urlencode(params)
360
        query = {'next': urlparse.urlunsplit(parts)}
361
        return Response(302,
362
                        headers={'Location': '%s?%s' %
363
                                 (self.get_login_uri(),
364
                                  urllib.urlencode(query))})
365

    
366
    def redirect_to_uri(self, redirect_uri, code, state=None):
367
        parts = list(urlparse.urlsplit(redirect_uri))
368
        params = dict(urlparse.parse_qsl(parts[3], keep_blank_values=True))
369
        params['code'] = code
370
        if state is not None:
371
            params['state'] = state
372
        parts[3] = urllib.urlencode(params)
373
        return Response(302,
374
                        headers={'Location': '%s' %
375
                                 urlparse.urlunsplit(parts)})
376

    
377
    def build_response_from_error(self, exception):
378
        response = Response(400)
379
        logger.exception(exception)
380
        error = 'generic_error'
381
        if exception.error:
382
            error = exception.error
383
        body = {
384
            'error': error,
385
            'exception': exception.message,
386
        }
387
        response.body = json.dumps(body)
388
        response.content_type = "application/json"
389
        return response
390

    
391
    #
392
    # Processor methods
393
    #
394

    
395
    def process_code_request(self, user, client, uri, scope, state):
396
        code = self.add_authorization_code(user, client, uri, scope, state)
397
        return self.redirect_to_uri(uri, code, state)
398

    
399
    #
400
    # Helpers
401
    #
402

    
403
    def grant_authorization_code(self, client, code_instance, redirect_uri,
404
                                 scope=None, token_type="Bearer"):
405
        if scope and code_instance.scope != scope:
406
            raise OA2Error("Invalid scope")
407
        if redirect_uri != code_instance.redirect_uri:
408
            raise OA2Error("The redirect uri does not match "
409
                           "the one used during authorization")
410
        token = self.add_token_for_client(token_type, code_instance)
411
        self.delete_authorization_code(code_instance)  # use only once
412
        return token, token_type
413

    
414
    def consume_token(self, token):
415
        token_instance = self.get_token(token)
416
        expires_at = mktime(token_instance.expires_at.timetuple())
417
        if time() > expires_at:
418
            self.delete_token(token_instance)  # delete expired token
419
            raise OA2Error("Token has expired")
420
        # TODO: delete token?
421
        return token_instance
422

    
423
    def _get_credentials(self, params, headers):
424
        if 'HTTP_AUTHORIZATION' in headers:
425
            scheme, b64credentials = headers.get(
426
                'HTTP_AUTHORIZATION').split(" ")
427
            if scheme != 'Basic':
428
                # TODO: raise 401 + WWW-Authenticate
429
                raise OA2Error("Unsupported authorization scheme")
430
            credentials = b64decode(b64credentials).split(":")
431
            return scheme, credentials
432
        else:
433
            return None, None
434
        pass
435

    
436
    def _get_authorization(self, params, headers):
437
        scheme, client_credentials = self._get_credentials(params, headers)
438
        no_authorization = scheme is None and client_credentials is None
439
        if no_authorization:
440
            raise OA2Error("Missing authorization header")
441
        return client_credentials
442

    
443
    def get_redirect_uri_from_params(self, client, params, default=True):
444
        """
445
        Accepts a client instance and request parameters.
446
        """
447
        redirect_uri = params.get('redirect_uri', None)
448
        if not redirect_uri and default:
449
            redirect_uri = client.get_default_redirect_uri()
450
        else:
451
            # TODO: sanitize redirect_uri (self.clean_redirect_uri ???)
452
            # clean and validate
453
            if not client.redirect_uri_is_valid(redirect_uri):
454
                raise OA2Error("Invalid client redirect uri")
455
        return redirect_uri
456

    
457
    #
458
    # Request identifiers
459
    #
460

    
461
    def identify_authorize_request(self, params, headers):
462
        return params.get('response_type'), params
463

    
464
    def identify_token_request(self, headers, params):
465
        content_type = headers.get('CONTENT_TYPE')
466
        if content_type != 'application/x-www-form-urlencoded':
467
            raise OA2Error("Invalid Content-Type header")
468
        return params.get('grant_type')
469

    
470
    #
471
    # Parameters validation methods
472
    #
473

    
474
    def validate_client(self, params, meta, requires_auth=True,
475
                        client_id_required=True):
476
        client_id = params.get('client_id')
477
        if client_id is None and client_id_required:
478
            raise OA2Error("Client identification is required")
479

    
480
        client_credentials = None
481
        try:  # check authorization header
482
            client_credentials = self._get_authorization(params, meta)
483
            if client_credentials is not None:
484
                _client_id = client_credentials[0]
485
                if client_id is not None and client_id != _client_id:
486
                    raise OA2Error("Client identification conflicts "
487
                                   "with client authorization")
488
                client_id = _client_id
489
        except:
490
            pass
491

    
492
        if client_id is None:
493
            raise OA2Error("Missing client identification")
494

    
495
        client = self.get_client_by_id(client_id)
496

    
497
        if requires_auth and client.requires_auth():
498
            if client_credentials is None:
499
                raise OA2Error("Client authentication is required")
500

    
501
        if client_credentials is not None:
502
            self.check_credentials(client, *client_credentials)
503
        return client
504

    
505
    def validate_redirect_uri(self, client, params, headers,
506
                              allow_default=True, is_required=False,
507
                              expected_value=None):
508
        redirect_uri = params.get('redirect_uri')
509
        if is_required and redirect_uri is None:
510
            raise OA2Error("Missing redirect uri")
511
        if redirect_uri is not None:
512
            if not bool(urlparse.urlparse(redirect_uri).scheme):
513
                raise OA2Error("Redirect uri should be an absolute URI")
514
            if not client.redirect_uri_is_valid(redirect_uri):
515
                raise OA2Error("Mismatching redirect uri")
516
            if expected_value is not None and redirect_uri != expected_value:
517
                raise OA2Error("Invalid redirect uri")
518
        else:
519
            try:
520
                redirect_uri = client.redirecturl_set.values_list('url',
521
                                                                  flat=True)[0]
522
            except IndexError:
523
                raise OA2Error("Unable to fallback to client redirect URI")
524
        return redirect_uri
525

    
526
    def validate_state(self, client, params, headers):
527
        return params.get('state')
528
        raise OA2Error("Invalid state")
529

    
530
    def validate_scope(self, client, params, headers):
531
        scope = params.get('scope')
532
        if scope is not None:
533
            scope = scope.split(' ')[0]  # keep only the first
534
        # TODO: check for invalid characters
535
        return scope
536

    
537
    def validate_code(self, client, params, headers):
538
        code = params.get('code')
539
        if code is None:
540
            raise OA2Error("Missing authorization code")
541
        return self.get_client_authorization_code(client, code)
542

    
543
    #
544
    # Requests validation methods
545
    #
546

    
547
    def validate_code_request(self, params, headers):
548
        client = self.validate_client(params, headers, requires_auth=False)
549
        redirect_uri = self.validate_redirect_uri(client, params, headers)
550
        scope = self.validate_scope(client, params, headers)
551
        scope = scope or redirect_uri  # set default
552
        state = self.validate_state(client, params, headers)
553
        return client, redirect_uri, scope, state
554

    
555
    def validate_token_request(self, params, headers, requires_auth=False):
556
        client = self.validate_client(params, headers)
557
        redirect_uri = self.validate_redirect_uri(client, params, headers)
558
        scope = self.validate_scope(client, params, headers)
559
        scope = scope or redirect_uri  # set default
560
        state = self.validate_state(client, params, headers)
561
        return client, redirect_uri, scope, state
562

    
563
    def validate_code_grant(self, params, headers):
564
        client = self.validate_client(params, headers,
565
                                      client_id_required=False)
566
        code_instance = self.validate_code(client, params, headers)
567
        redirect_uri = self.validate_redirect_uri(
568
            client, params, headers,
569
            expected_value=code_instance.redirect_uri)
570
        return client, redirect_uri, code_instance
571

    
572
    #
573
    # Endpoint methods
574
    #
575

    
576
    @handles_oa2_requests
577
    def authorize(self, request, **extra):
578
        """
579
        Used in the following cases
580
        """
581
        if not request.secure:
582
            raise OA2Error("Secure request required")
583

    
584
        # identify
585
        request_params = request.GET
586
        if request.method == "POST":
587
            request_params = request.POST
588

    
589
        auth_type, params = self.identify_authorize_request(request_params,
590
                                                            request.META)
591

    
592
        if auth_type is None:
593
            raise OA2Error("Missing authorization type")
594
        if auth_type == 'code':
595
            client, uri, scope, state = \
596
                self.validate_code_request(params, request.META)
597
        elif auth_type == 'token':
598
            raise OA2Error("Unsupported authorization type")
599
#            client, uri, scope, state = \
600
#                self.validate_token_request(params, request.META)
601
        else:
602
            #TODO: handle custom type
603
            raise OA2Error("Invalid authorization type")
604

    
605
        user = getattr(request, 'user', None)
606
        if not user:
607
            return self.redirect_to_login_response(request, params)
608

    
609
        if request.method == 'POST':
610
            if auth_type == 'code':
611
                return self.process_code_request(user, client, uri, scope,
612
                                                 state)
613
            elif auth_type == 'token':
614
                raise OA2Error("Unsupported response type")
615
#                return self.process_token_request(user, client, uri, scope,
616
#                                                 state)
617
            else:
618
                #TODO: handle custom type
619
                raise OA2Error("Invalid authorization type")
620
        else:
621
            if client.is_trusted:
622
                return self.process_code_request(user, client, uri, scope,
623
                                                 state)
624
            else:
625
                return self.grant_accept_response(client, uri, scope, state)
626

    
627
    @handles_oa2_requests
628
    def grant_token(self, request, **extra):
629
        """
630
        Used in the following cases
631
        """
632
        if not request.secure:
633
            raise OA2Error("Secure request required")
634

    
635
        grant_type = self.identify_token_request(request.META, request.POST)
636

    
637
        if grant_type is None:
638
            raise OA2Error("Missing grant type")
639
        elif grant_type == 'authorization_code':
640
            client, redirect_uri, code = \
641
                self.validate_code_grant(request.POST, request.META)
642
            token, token_type = \
643
                self.grant_authorization_code(client, code, redirect_uri)
644
            return self.grant_token_response(token, token_type)
645
        elif (grant_type in ['client_credentials', 'token'] or
646
              self.is_uri(grant_type)):
647
            raise OA2Error("Unsupported grant type")
648
        else:
649
            #TODO: handle custom type
650
            raise OA2Error("Invalid grant type")