Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.1 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

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

    
61

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

    
72

    
73
class OA2Error(Exception):
74
    error = None
75

    
76

    
77
class InvalidClientID(OA2Error):
78
    pass
79

    
80

    
81
class NotAuthenticatedError(OA2Error):
82
    pass
83

    
84

    
85
class InvalidClientRedirectUrl(OA2Error):
86
    pass
87

    
88

    
89
class InvalidAuthorizationRequest(OA2Error):
90
    pass
91

    
92

    
93
class Response(object):
94

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

    
102
        self.status = status
103
        self.body = body
104
        self.headers = headers
105
        self.content_type = content_type
106

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

    
112

    
113
class Request(object):
114

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

    
120
        if not GET:
121
            GET = {}
122
        if not POST:
123
            POST = {}
124
        if not META:
125
            META = {}
126

    
127
        self.secure = secure
128
        self.GET = GET
129
        self.POST = POST
130
        self.META = META
131
        self.user = user
132

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

    
143

    
144
class ORMAbstractBase(type):
145

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

    
150

    
151
class ORMAbstract(object):
152

    
153
    ENTRIES = {}
154

    
155
    __metaclass__ = ORMAbstractBase
156

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

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

    
168
    @classmethod
169
    def get(cls, pk):
170
        return cls.ENTRIES.get(pk)
171

    
172
    @classmethod
173
    def clean_params(cls, params):
174
        return params
175

    
176

    
177
class Client(ORMAbstract):
178

    
179
    def get_id(self):
180
        return self.id
181

    
182
    def get_redirect_uris(self):
183
        return self.uris
184

    
185
    def get_default_redirect_uri(self):
186
        return self.uris[0]
187

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

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

    
200
    def check_credentials(self, username, secret):
201
        return username == self.id and secret == self.secret
202

    
203

    
204
class Token(ORMAbstract):
205

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

    
216

    
217
class AuthorizationCode(ORMAbstract):
218
    pass
219

    
220

    
221
class User(ORMAbstract):
222
    pass
223

    
224

    
225
class BackendBase(type):
226

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

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

    
242

    
243
class SimpleBackend(object):
244

    
245
    __metaclass__ = BackendBase
246

    
247
    base_url = ''
248
    endpoints_prefix = 'oauth2/'
249

    
250
    token_endpoint = 'token/'
251
    token_length = 30
252
    token_expires = 20
253

    
254
    authorization_endpoint = 'auth/'
255
    authorization_code_length = 60
256
    authorization_response_types = ['code', 'token']
257

    
258
    grant_types = ['authorization_code']
259

    
260
    response_cls = Response
261
    request_cls = Request
262

    
263
    client_model = Client
264
    token_model = Token
265
    code_model = AuthorizationCode
266
    user_model = User
267

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

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

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

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

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

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

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

    
337
    def get_client_by_id(self, client_id):
338
        return self.client_model.get(client_id)
339

    
340
    def get_client_by_credentials(self, username, password):
341
        return None
342

    
343
    def get_authorization_code(self, code):
344
        return self.code_model.get(code)
345

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

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

    
355
    def client_id_exists(self, client_id):
356
        return bool(self.get_client_by_id(client_id))
357

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

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

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

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

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

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

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

    
394
    #
395
    # Response helpers
396
    #
397

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

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

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

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

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

    
446
    #
447
    # Processor methods
448
    #
449

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

    
454
    #
455
    # Helpers
456
    #
457

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

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

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

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

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

    
511
    #
512
    # Request identifiers
513
    #
514

    
515
    def identify_authorize_request(self, params, headers):
516
        return params.get('response_type'), params
517

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

    
524
    #
525
    # Parameters validation methods
526
    #
527

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

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

    
548
        if client_id is None:
549
            raise OA2Error("Missing client identification")
550

    
551
        client = self.get_client_by_id(client_id)
552

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

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

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

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

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

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

    
602
    #
603
    # Requests validation methods
604
    #
605

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

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

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

    
631
    #
632
    # Endpoint methods
633
    #
634

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

    
643
        # identify
644
        request_params = request.GET
645
        if request.method == "POST":
646
            request_params = request.POST
647

    
648
        auth_type, params = self.identify_authorize_request(request_params,
649
                                                            request.META)
650

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

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

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

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

    
694
        grant_type = self.identify_token_request(request.META, request.POST)
695

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