Statistics
| Branch: | Tag: | Revision:

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

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

    
10
import logging
11
logger = logging.getLogger(__name__)
12

    
13

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

    
24

    
25
class OA2Error(Exception):
26
    error = None
27

    
28

    
29
class InvalidClientID(OA2Error):
30
    pass
31

    
32

    
33
class NotAuthenticatedError(OA2Error):
34
    pass
35

    
36

    
37
class InvalidClientRedirectUrl(OA2Error):
38
    pass
39

    
40

    
41
class InvalidAuthorizationRequest(OA2Error):
42
    pass
43

    
44

    
45
class Response(object):
46

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

    
54
        self.status = status
55
        self.body = body
56
        self.headers = headers
57
        self.content_type = content_type
58

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

    
64

    
65
class Request(object):
66

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

    
72
        if not GET:
73
            GET = {}
74
        if not POST:
75
            POST = {}
76
        if not META:
77
            META = {}
78

    
79
        self.secure = secure
80
        self.GET = GET
81
        self.POST = POST
82
        self.META = META
83
        self.user = user
84

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

    
95

    
96
class ORMAbstractBase(type):
97

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

    
102

    
103
class ORMAbstract(object):
104

    
105
    ENTRIES = {}
106

    
107
    __metaclass__ = ORMAbstractBase
108

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

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

    
120
    @classmethod
121
    def get(cls, pk):
122
        return cls.ENTRIES.get(pk)
123

    
124
    @classmethod
125
    def clean_params(cls, params):
126
        return params
127

    
128

    
129
class Client(ORMAbstract):
130

    
131
    def get_id(self):
132
        return self.id
133

    
134
    def get_redirect_uris(self):
135
        return self.uris
136

    
137
    def get_default_redirect_uri(self):
138
        return self.uris[0]
139

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

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

    
152
    def check_credentials(self, username, secret):
153
        return username == self.id and secret == self.secret
154

    
155

    
156
class Token(ORMAbstract):
157

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

    
168

    
169
class AuthorizationCode(ORMAbstract):
170
    pass
171

    
172

    
173
class User(ORMAbstract):
174
    pass
175

    
176

    
177
class BackendBase(type):
178

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

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

    
194

    
195
class SimpleBackend(object):
196

    
197
    __metaclass__ = BackendBase
198

    
199
    base_url = ''
200
    endpoints_prefix = '/oa2/'
201

    
202
    token_endpoint = 'token/'
203
    token_length = 30
204
    token_expires = 300
205

    
206
    authorization_endpoint = 'auth/'
207
    authorization_code_length = 60
208
    authorization_response_types = ['code', 'token']
209

    
210
    grant_types = ['authorization_code']
211

    
212
    response_cls = Response
213
    request_cls = Request
214

    
215
    client_model = Client
216
    token_model = Token
217
    code_model = AuthorizationCode
218
    user_model = User
219

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

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

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

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

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

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

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

    
280
    def get_client_by_id(self, client_id):
281
        return self.client_model.get(client_id)
282

    
283
    def get_client_by_credentials(self, username, password):
284
        return None
285

    
286
    def get_authorization_code(self, code):
287
        return self.code_model.get(code)
288

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

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

    
298
    def client_id_exists(self, client_id):
299
        return bool(self.get_client_by_id(client_id))
300

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

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

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

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

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

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

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

    
337
    #
338
    # Response helpers
339
    #
340

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

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

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

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

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

    
389
    #
390
    # Processor methods
391
    #
392

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

    
397
    #
398
    # Helpers
399
    #
400

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

    
412
    def consume_token(self, token):
413
        token_instance = self.get_token(token)
414
        if datetime.datetime.now() > token_instance.expires_at:
415
            self.delete_token(token_instance)  # delete expired token
416
            raise OA2Error("Token has expired")
417
        # TODO: delete token?
418
        return token_instance
419

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

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

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

    
454
    #
455
    # Request identifiers
456
    #
457

    
458
    def identify_authorize_request(self, params, headers):
459
        return params.get('response_type'), params
460

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

    
467
    #
468
    # Parameters validation methods
469
    #
470

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

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

    
489
        if client_id is None:
490
            raise OA2Error("Missing client identification")
491

    
492
        client = self.get_client_by_id(client_id)
493

    
494
        if requires_auth and client.requires_auth():
495
            if client_credentials is None:
496
                raise OA2Error("Client authentication is required")
497

    
498
        if client_credentials is not None:
499
            self.check_credentials(client, *client_credentials)
500
        return client
501

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

    
523
    def validate_state(self, client, params, headers):
524
        return params.get('state')
525
        raise OA2Error("Invalid state")
526

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

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

    
540
    #
541
    # Requests validation methods
542
    #
543

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

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

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

    
569
    #
570
    # Endpoint methods
571
    #
572

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

    
581
        # identify
582
        request_params = request.GET
583
        if request.method == "POST":
584
            request_params = request.POST
585

    
586
        auth_type, params = self.identify_authorize_request(request_params,
587
                                                            request.META)
588

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

    
602
        user = getattr(request, 'user', None)
603
        if not user:
604
            return self.redirect_to_login_response(request, params)
605

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

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

    
632
        grant_type = self.identify_token_request(request.META, request.POST)
633

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