Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.2 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import urllib
35
import urlparse
36
import uuid
37
import datetime
38
import json
39

    
40
from base64 import b64encode, b64decode
41
from hashlib import sha512
42

    
43
from synnefo.util.text import uenc
44
from synnefo.util import urltools
45

    
46
import logging
47
logger = logging.getLogger(__name__)
48

    
49

    
50
def urlencode(params):
51
    if hasattr(params, 'urlencode') and callable(getattr(params, 'urlencode')):
52
        return params.urlencode()
53
    for k in params:
54
        params[uenc(k)] = uenc(params.pop(k))
55
    return urllib.urlencode(params)
56

    
57
def normalize(url):
58
    return urltools.normalize(uenc(url))
59

    
60
def handles_oa2_requests(func):
61
    def wrapper(self, *args, **kwargs):
62
        if not self._errors_to_http:
63
            return func(self, *args, **kwargs)
64
        try:
65
            return func(self, *args, **kwargs)
66
        except OA2Error, e:
67
            return self.build_response_from_error(e)
68
    return wrapper
69

    
70

    
71
class OA2Error(Exception):
72
    error = None
73

    
74

    
75
class InvalidClientID(OA2Error):
76
    pass
77

    
78

    
79
class NotAuthenticatedError(OA2Error):
80
    pass
81

    
82

    
83
class InvalidClientRedirectUrl(OA2Error):
84
    pass
85

    
86

    
87
class InvalidAuthorizationRequest(OA2Error):
88
    pass
89

    
90

    
91
class Response(object):
92

    
93
    def __init__(self, status, body='', headers=None,
94
                 content_type='plain/text'):
95
        if not body:
96
            body = ''
97
        if not headers:
98
            headers = {}
99

    
100
        self.status = status
101
        self.body = body
102
        self.headers = headers
103
        self.content_type = content_type
104

    
105
    def __repr__(self):
106
        return "%d RESPONSE (BODY: %r, HEADERS: %r)" % (self.status,
107
                                                        self.body,
108
                                                        self.headers)
109

    
110

    
111
class Request(object):
112

    
113
    def __init__(self, method, path, GET=None, POST=None, META=None,
114
                 secure=False, user=None):
115
        self.method = method
116
        self.path = path
117

    
118
        if not GET:
119
            GET = {}
120
        if not POST:
121
            POST = {}
122
        if not META:
123
            META = {}
124

    
125
        self.secure = secure
126
        self.GET = GET
127
        self.POST = POST
128
        self.META = META
129
        self.user = user
130

    
131
    def __repr__(self):
132
        prepend = ""
133
        if self.secure:
134
            prepend = "SECURE "
135
        return "%s%s REQUEST (POST: %r, GET:%r, HEADERS:%r, " % (prepend,
136
                                                                 self.method,
137
                                                                 self.POST,
138
                                                                 self.GET,
139
                                                                 self.META)
140

    
141

    
142
class ORMAbstractBase(type):
143

    
144
    def __new__(cls, name, bases, attrs):
145
        attrs['ENTRIES'] = {}
146
        return super(ORMAbstractBase, cls).__new__(cls, name, bases, attrs)
147

    
148

    
149
class ORMAbstract(object):
150

    
151
    ENTRIES = {}
152

    
153
    __metaclass__ = ORMAbstractBase
154

    
155
    def __init__(self, **kwargs):
156
        for key, value in kwargs.iteritems():
157
            setattr(self, key, value)
158

    
159
    @classmethod
160
    def create(cls, id, **params):
161
        params = cls.clean_params(params)
162
        params['id'] = id
163
        cls.ENTRIES[id] = cls(**params)
164
        return cls.get(id)
165

    
166
    @classmethod
167
    def get(cls, pk):
168
        return cls.ENTRIES.get(pk)
169

    
170
    @classmethod
171
    def clean_params(cls, params):
172
        return params
173

    
174

    
175
class Client(ORMAbstract):
176

    
177
    def get_id(self):
178
        return self.id
179

    
180
    def get_redirect_uris(self):
181
        return self.uris
182

    
183
    def get_default_redirect_uri(self):
184
        return self.uris[0]
185

    
186
    def redirect_uri_is_valid(self, redirect_uri):
187
        split = urlparse.urlsplit(redirect_uri)
188
        if split.scheme not in urlparse.uses_query:
189
            raise OA2Error("Invalid redirect url scheme")
190
        uris = self.get_redirect_uris()
191
        return redirect_uri in uris
192

    
193
    def requires_auth(self):
194
        if self.client_type == 'confidential':
195
            return True
196
        return 'secret' in dir(self)
197

    
198
    def check_credentials(self, username, secret):
199
        return username == self.id and secret == self.secret
200

    
201

    
202
class Token(ORMAbstract):
203

    
204
    def to_dict(self):
205
        params = {
206
            'access_token': self.token,
207
            'token_type': self.token_type,
208
            'expires_in': self.expires,
209
        }
210
        if self.refresh_token:
211
            params['refresh_token'] = self.refresh_token
212
        return params
213

    
214

    
215
class AuthorizationCode(ORMAbstract):
216
    pass
217

    
218

    
219
class User(ORMAbstract):
220
    pass
221

    
222

    
223
class BackendBase(type):
224

    
225
    def __new__(cls, name, bases, attrs):
226
        super_new = super(BackendBase, cls).__new__
227
        #parents = [b for b in bases if isinstance(b, BackendBase)]
228
        #meta = attrs.pop('Meta', None)
229
        return super_new(cls, name, bases, attrs)
230

    
231
    @classmethod
232
    def get_orm_options(cls, attrs):
233
        meta = attrs.pop('ORM', None)
234
        orm = {}
235
        if meta:
236
            for attr in dir(meta):
237
                orm[attr] = getattr(meta, attr)
238
        return orm
239

    
240

    
241
class SimpleBackend(object):
242

    
243
    __metaclass__ = BackendBase
244

    
245
    base_url = ''
246
    endpoints_prefix = 'oauth2/'
247

    
248
    token_endpoint = 'token/'
249
    token_length = 30
250
    token_expires = 20
251

    
252
    authorization_endpoint = 'auth/'
253
    authorization_code_length = 60
254
    authorization_response_types = ['code', 'token']
255

    
256
    grant_types = ['authorization_code']
257

    
258
    response_cls = Response
259
    request_cls = Request
260

    
261
    client_model = Client
262
    token_model = Token
263
    code_model = AuthorizationCode
264
    user_model = User
265

    
266
    def __init__(self, base_url='', endpoints_prefix='oauth2/', id='oauth2',
267
                 token_endpoint='token/', token_length=30,
268
                 token_expires=20, authorization_endpoint='auth/',
269
                 authorization_code_length=60,
270
                 redirect_uri_limit=5000, **kwargs):
271
        self.base_url = base_url
272
        self.endpoints_prefix = endpoints_prefix
273
        self.token_endpoint = token_endpoint
274
        self.token_length = token_length
275
        self.token_expires = token_expires
276
        self.authorization_endpoint = authorization_endpoint
277
        self.authorization_code_length = authorization_code_length
278
        self.id = id
279
        self._errors_to_http = kwargs.get('errors_to_http', True)
280
        self.redirect_uri_limit = redirect_uri_limit
281

    
282
    # Request/response builders
283
    def build_request(self, method, get, post, meta):
284
        return self.request_cls(method=method, GET=get, POST=post, META=meta)
285

    
286
    def build_response(self, status, headers=None, body=''):
287
        return self.response_cls(status=status, headers=headers, body=body)
288

    
289
    # ORM Methods
290
    def create_authorization_code(self, user, client, code, redirect_uri,
291
                                  scope, state, **kwargs):
292
        code_params = {
293
            'code': code,
294
            'redirect_uri': redirect_uri,
295
            'client': client,
296
            'scope': scope,
297
            'state': state,
298
            'user': user
299
        }
300
        code_params.update(kwargs)
301
        code_instance = self.code_model.create(**code_params)
302
        logger.info(u'%r created' % code_instance)
303
        return code_instance
304

    
305
    def _token_params(self, value, token_type, authorization, scope):
306
        created_at = datetime.datetime.now()
307
        expires = self.token_expires
308
        expires_at = created_at + datetime.timedelta(seconds=expires)
309
        token_params = {
310
            'code': value,
311
            'token_type': token_type,
312
            'created_at': created_at,
313
            'expires_at': expires_at,
314
            'user': authorization.user,
315
            'redirect_uri': authorization.redirect_uri,
316
            'client': authorization.client,
317
            'scope': authorization.scope,
318
        }
319
        return token_params
320

    
321
    def create_token(self, value, token_type, authorization, scope,
322
                     refresh=False):
323
        params = self._token_params(value, token_type, authorization, scope)
324
        if refresh:
325
            refresh_token = self.generate_token()
326
            params['refresh_token'] = refresh_token
327
            # TODO: refresh token expires ???
328
        token = self.token_model.create(**params)
329
        logger.info(u'%r created' % token)
330
        return token
331

    
332
#    def delete_authorization_code(self, code):
333
#        del self.code_model.ENTRIES[code]
334

    
335
    def get_client_by_id(self, client_id):
336
        return self.client_model.get(client_id)
337

    
338
    def get_client_by_credentials(self, username, password):
339
        return None
340

    
341
    def get_authorization_code(self, code):
342
        return self.code_model.get(code)
343

    
344
    def get_client_authorization_code(self, client, code):
345
        code_instance = self.get_authorization_code(code)
346
        if not code_instance:
347
            raise OA2Error("Invalid code")
348

    
349
        if client.get_id() != code_instance.client.get_id():
350
            raise OA2Error("Mismatching client with code client")
351
        return code_instance
352

    
353
    def client_id_exists(self, client_id):
354
        return bool(self.get_client_by_id(client_id))
355

    
356
    def build_site_url(self, prefix='', **params):
357
        params = urlencode(params)
358
        return "%s%s%s%s" % (self.base_url, self.endpoints_prefix, prefix,
359
                             params)
360

    
361
    def _get_uri_base(self, uri):
362
        split = urlparse.urlsplit(uri)
363
        return "%s://%s%s" % (split.scheme, split.netloc, split.path)
364

    
365
    def build_client_redirect_uri(self, client, uri, **params):
366
        if not client.redirect_uri_is_valid(uri):
367
            raise OA2Error("Invalid redirect uri")
368
        params = urlencode(params)
369
        uri = self._get_uri_base(uri)
370
        return "%s?%s" % (uri, params)
371

    
372
    def generate_authorization_code(self):
373
        dg64 = b64encode(sha512(str(uuid.uuid4())).hexdigest())
374
        return dg64[:self.authorization_code_length]
375

    
376
    def generate_token(self, *args, **kwargs):
377
        dg64 = b64encode(sha512(str(uuid.uuid4())).hexdigest())
378
        return dg64[:self.token_length]
379

    
380
    def add_authorization_code(self, user, client, redirect_uri, scope, state,
381
                               **kwargs):
382
        code = self.generate_authorization_code()
383
        self.create_authorization_code(user, client, code, redirect_uri, scope,
384
                                       state, **kwargs)
385
        return code
386

    
387
    def add_token_for_client(self, token_type, authorization, refresh=False):
388
        token = self.generate_token()
389
        self.create_token(token, token_type, authorization, refresh)
390
        return token
391

    
392
    #
393
    # Response helpers
394
    #
395

    
396
    def grant_accept_response(self, client, redirect_uri, scope, state):
397
        context = {'client': client.get_id(), 'redirect_uri': redirect_uri,
398
                   'scope': scope, 'state': state,
399
                   #'url': url,
400
                   }
401
        json_content = json.dumps(context)
402
        return self.response_cls(status=200, body=json_content)
403

    
404
    def grant_token_response(self, token, token_type):
405
        context = {'access_token': token, 'token_type': token_type,
406
                   'expires_in': self.token_expires}
407
        json_content = json.dumps(context)
408
        return self.response_cls(status=200, body=json_content)
409

    
410
    def redirect_to_login_response(self, request, params):
411
        parts = list(urlparse.urlsplit(request.path))
412
        parts[3] = urlencode(params)
413
        query = {'next': urlparse.urlunsplit(parts)}
414
        return Response(302,
415
                        headers={'Location': '%s?%s' %
416
                                 (self.get_login_uri(),
417
                                  urlencode(query))})
418

    
419
    def redirect_to_uri(self, redirect_uri, code, state=None):
420
        parts = list(urlparse.urlsplit(redirect_uri))
421
        params = dict(urlparse.parse_qsl(parts[3], keep_blank_values=True))
422
        params['code'] = code
423
        if state is not None:
424
            params['state'] = state
425
        parts[3] = urlencode(params)
426
        return Response(302,
427
                        headers={'Location': '%s' %
428
                                 urlparse.urlunsplit(parts)})
429

    
430
    def build_response_from_error(self, exception):
431
        response = Response(400)
432
        logger.exception(exception)
433
        error = 'generic_error'
434
        if exception.error:
435
            error = exception.error
436
        body = {
437
            'error': error,
438
            'exception': exception.message,
439
        }
440
        response.body = json.dumps(body)
441
        response.content_type = "application/json"
442
        return response
443

    
444
    #
445
    # Processor methods
446
    #
447

    
448
    def process_code_request(self, user, client, uri, scope, state):
449
        code = self.add_authorization_code(user, client, uri, scope, state)
450
        return self.redirect_to_uri(uri, code, state)
451

    
452
    #
453
    # Helpers
454
    #
455

    
456
    def grant_authorization_code(self, client, code_instance, redirect_uri,
457
                                 scope=None, token_type="Bearer"):
458
        if scope and code_instance.scope != scope:
459
            raise OA2Error("Invalid scope")
460
        if normalize(redirect_uri) != normalize(code_instance.redirect_uri):
461
            raise OA2Error("The redirect uri does not match "
462
                           "the one used during authorization")
463
        token = self.add_token_for_client(token_type, code_instance)
464
        self.delete_authorization_code(code_instance)  # use only once
465
        return token, token_type
466

    
467
    def consume_token(self, token):
468
        token_instance = self.get_token(token)
469
        if datetime.datetime.now() > token_instance.expires_at:
470
            self.delete_token(token_instance)  # delete expired token
471
            raise OA2Error("Token has expired")
472
        # TODO: delete token?
473
        return token_instance
474

    
475
    def _get_credentials(self, params, headers):
476
        if 'HTTP_AUTHORIZATION' in headers:
477
            scheme, b64credentials = headers.get(
478
                'HTTP_AUTHORIZATION').split(" ")
479
            if scheme != 'Basic':
480
                # TODO: raise 401 + WWW-Authenticate
481
                raise OA2Error("Unsupported authorization scheme")
482
            credentials = b64decode(b64credentials).split(":")
483
            return scheme, credentials
484
        else:
485
            return None, None
486
        pass
487

    
488
    def _get_authorization(self, params, headers, authorization_required=True):
489
        scheme, client_credentials = self._get_credentials(params, headers)
490
        no_authorization = scheme is None and client_credentials is None
491
        if authorization_required and no_authorization:
492
            raise OA2Error("Missing authorization header")
493
        return client_credentials
494

    
495
    def get_redirect_uri_from_params(self, client, params, default=True):
496
        """
497
        Accepts a client instance and request parameters.
498
        """
499
        redirect_uri = params.get('redirect_uri', None)
500
        if not redirect_uri and default:
501
            redirect_uri = client.get_default_redirect_uri()
502
        else:
503
            # TODO: sanitize redirect_uri (self.clean_redirect_uri ???)
504
            # clean and validate
505
            if not client.redirect_uri_is_valid(redirect_uri):
506
                raise OA2Error("Invalid client redirect uri")
507
        return redirect_uri
508

    
509
    #
510
    # Request identifiers
511
    #
512

    
513
    def identify_authorize_request(self, params, headers):
514
        return params.get('response_type'), params
515

    
516
    def identify_token_request(self, headers, params):
517
        content_type = headers.get('CONTENT_TYPE')
518
        if content_type != 'application/x-www-form-urlencoded':
519
            raise OA2Error("Invalid Content-Type header")
520
        return params.get('grant_type')
521

    
522
    #
523
    # Parameters validation methods
524
    #
525

    
526
    def validate_client(self, params, meta, requires_auth=True,
527
                        client_id_required=True):
528
        client_id = params.get('client_id')
529
        if client_id is None and client_id_required:
530
            raise OA2Error("Client identification is required")
531

    
532
        client_credentials = None
533
        try:  # check authorization header
534
            client_credentials = self._get_authorization(params, meta,
535
                                                         authorization_required=False)
536
        except:
537
            pass
538
        else:
539
            if client_credentials is not None:
540
                _client_id = client_credentials[0]
541
                if client_id is not None and client_id != _client_id:
542
                    raise OA2Error("Client identification conflicts "
543
                                   "with client authorization")
544
                client_id = _client_id
545

    
546
        if client_id is None:
547
            raise OA2Error("Missing client identification")
548

    
549
        client = self.get_client_by_id(client_id)
550

    
551
        if requires_auth and client.requires_auth():
552
            if client_credentials is None:
553
                raise OA2Error("Client authentication is required")
554

    
555
        if client_credentials is not None:
556
            self.check_credentials(client, *client_credentials)
557
        return client
558

    
559
    def validate_redirect_uri(self, client, params, headers,
560
                              allow_default=True, is_required=False,
561
                              expected_value=None):
562
        redirect_uri = params.get('redirect_uri')
563
        if is_required and redirect_uri is None:
564
            raise OA2Error("Missing redirect uri")
565
        if redirect_uri is not None:
566
            if not bool(urlparse.urlparse(redirect_uri).scheme):
567
                raise OA2Error("Redirect uri should be an absolute URI")
568
            if len(redirect_uri) > self.redirect_uri_limit:
569
                raise OA2Error("Redirect uri length limit exceeded")
570
            if not client.redirect_uri_is_valid(redirect_uri):
571
                raise OA2Error("Mismatching redirect uri")
572
            if expected_value is not None and \
573
                    normalize(redirect_uri) != normalize(expected_value):
574
                raise OA2Error("Invalid redirect uri")
575
        else:
576
            try:
577
                redirect_uri = client.redirecturl_set.values_list('url',
578
                                                                  flat=True)[0]
579
            except IndexError:
580
                raise OA2Error("Unable to fallback to client redirect URI")
581
        return redirect_uri
582

    
583
    def validate_state(self, client, params, headers):
584
        return params.get('state')
585
        #raise OA2Error("Invalid state")
586

    
587
    def validate_scope(self, client, params, headers):
588
        scope = params.get('scope')
589
        if scope is not None:
590
            scope = scope.split(' ')[0]  # keep only the first
591
        # TODO: check for invalid characters
592
        return scope
593

    
594
    def validate_code(self, client, params, headers):
595
        code = params.get('code')
596
        if code is None:
597
            raise OA2Error("Missing authorization code")
598
        return self.get_client_authorization_code(client, code)
599

    
600
    #
601
    # Requests validation methods
602
    #
603

    
604
    def validate_code_request(self, params, headers):
605
        client = self.validate_client(params, headers, requires_auth=False)
606
        redirect_uri = self.validate_redirect_uri(client, params, headers)
607
        scope = self.validate_scope(client, params, headers)
608
        scope = scope or redirect_uri  # set default
609
        state = self.validate_state(client, params, headers)
610
        return client, redirect_uri, scope, state
611

    
612
    def validate_token_request(self, params, headers, requires_auth=False):
613
        client = self.validate_client(params, headers)
614
        redirect_uri = self.validate_redirect_uri(client, params, headers)
615
        scope = self.validate_scope(client, params, headers)
616
        scope = scope or redirect_uri  # set default
617
        state = self.validate_state(client, params, headers)
618
        return client, redirect_uri, scope, state
619

    
620
    def validate_code_grant(self, params, headers):
621
        client = self.validate_client(params, headers,
622
                                      client_id_required=False)
623
        code_instance = self.validate_code(client, params, headers)
624
        redirect_uri = self.validate_redirect_uri(
625
            client, params, headers,
626
            expected_value=code_instance.redirect_uri)
627
        return client, redirect_uri, code_instance
628

    
629
    #
630
    # Endpoint methods
631
    #
632

    
633
    @handles_oa2_requests
634
    def authorize(self, request, **extra):
635
        """
636
        Used in the following cases
637
        """
638
        if not request.secure:
639
            raise OA2Error("Secure request required")
640

    
641
        # identify
642
        request_params = request.GET
643
        if request.method == "POST":
644
            request_params = request.POST
645

    
646
        auth_type, params = self.identify_authorize_request(request_params,
647
                                                            request.META)
648

    
649
        if auth_type is None:
650
            raise OA2Error("Missing authorization type")
651
        if auth_type == 'code':
652
            client, uri, scope, state = \
653
                self.validate_code_request(params, request.META)
654
        elif auth_type == 'token':
655
            raise OA2Error("Unsupported authorization type")
656
#            client, uri, scope, state = \
657
#                self.validate_token_request(params, request.META)
658
        else:
659
            #TODO: handle custom type
660
            raise OA2Error("Invalid authorization type")
661

    
662
        user = getattr(request, 'user', None)
663
        if not user:
664
            return self.redirect_to_login_response(request, params)
665

    
666
        if request.method == 'POST':
667
            if auth_type == 'code':
668
                return self.process_code_request(user, client, uri, scope,
669
                                                 state)
670
            elif auth_type == 'token':
671
                raise OA2Error("Unsupported response type")
672
#                return self.process_token_request(user, client, uri, scope,
673
#                                                 state)
674
            else:
675
                #TODO: handle custom type
676
                raise OA2Error("Invalid authorization type")
677
        else:
678
            if client.is_trusted:
679
                return self.process_code_request(user, client, uri, scope,
680
                                                 state)
681
            else:
682
                return self.grant_accept_response(client, uri, scope, state)
683

    
684
    @handles_oa2_requests
685
    def grant_token(self, request, **extra):
686
        """
687
        Used in the following cases
688
        """
689
        if not request.secure:
690
            raise OA2Error("Secure request required")
691

    
692
        grant_type = self.identify_token_request(request.META, request.POST)
693

    
694
        if grant_type is None:
695
            raise OA2Error("Missing grant type")
696
        elif grant_type == 'authorization_code':
697
            client, redirect_uri, code = \
698
                self.validate_code_grant(request.POST, request.META)
699
            token, token_type = \
700
                self.grant_authorization_code(client, code, redirect_uri)
701
            return self.grant_token_response(token, token_type)
702
        elif (grant_type in ['client_credentials', 'token'] or
703
              self.is_uri(grant_type)):
704
            raise OA2Error("Unsupported grant type")
705
        else:
706
            #TODO: handle custom type
707
            raise OA2Error("Invalid grant type")