Revision e28a4841
b/snf-astakos-app/astakos/api/services.py | ||
---|---|---|
96 | 96 |
{'versionId': '', |
97 | 97 |
'publicURL': None}, |
98 | 98 |
], |
99 |
'resources': {}, |
|
99 | 100 |
}, |
101 |
|
|
102 |
'astakos_oa2': { |
|
103 |
'type': 'astakos_auth', |
|
104 |
'component': 'astakos', |
|
105 |
'prefix': 'oa2', |
|
106 |
'public': True, |
|
107 |
'endpoints': [ |
|
108 |
{'versionId': '', |
|
109 |
'publicURL': None}, |
|
110 |
], |
|
111 |
'resources': {}, |
|
112 |
} |
|
100 | 113 |
} |
b/snf-astakos-app/astakos/im/settings.py | ||
---|---|---|
55 | 55 |
KEYSTONE_PREFIX = get_path(astakos_services, 'astakos_identity.prefix') |
56 | 56 |
WEBLOGIN_PREFIX = get_path(astakos_services, 'astakos_weblogin.prefix') |
57 | 57 |
ADMIN_PREFIX = get_path(astakos_services, 'astakos_admin.prefix') |
58 |
OA2_PREFIX = get_path(astakos_services, 'astakos_oa2.prefix') |
|
58 | 59 |
|
59 | 60 |
# Set the expiration time of newly created auth tokens |
60 | 61 |
# to be this many hours after their creation time. |
b/snf-astakos-app/astakos/oa2/backends/base.py | ||
---|---|---|
6 | 6 |
|
7 | 7 |
from base64 import b64encode, b64decode |
8 | 8 |
from hashlib import sha512 |
9 |
from time import time, mktime |
|
10 |
|
|
9 | 11 |
|
10 | 12 |
import logging |
11 | 13 |
logger = logging.getLogger(__name__) |
... | ... | |
64 | 66 |
|
65 | 67 |
class Request(object): |
66 | 68 |
|
67 |
def __init__(self, method, GET=None, POST=None, META=None, secure=False,
|
|
68 |
user=None): |
|
69 |
def __init__(self, method, path, GET=None, POST=None, META=None,
|
|
70 |
secure=False, user=None):
|
|
69 | 71 |
self.method = method |
72 |
self.path = path |
|
70 | 73 |
|
71 | 74 |
if not GET: |
72 | 75 |
GET = {} |
... | ... | |
177 | 180 |
|
178 | 181 |
def __new__(cls, name, bases, attrs): |
179 | 182 |
super_new = super(BackendBase, cls).__new__ |
180 |
parents = [b for b in bases if isinstance(b, BackendBase)] |
|
181 |
meta = attrs.pop('Meta', None) |
|
183 |
#parents = [b for b in bases if isinstance(b, BackendBase)]
|
|
184 |
#meta = attrs.pop('Meta', None)
|
|
182 | 185 |
return super_new(cls, name, bases, attrs) |
183 | 186 |
|
184 | 187 |
@classmethod |
... | ... | |
200 | 203 |
|
201 | 204 |
token_endpoint = 'token/' |
202 | 205 |
token_length = 30 |
203 |
token_expires = 3600
|
|
206 |
token_expires = 300 |
|
204 | 207 |
|
205 | 208 |
authorization_endpoint = 'auth/' |
206 | 209 |
authorization_code_length = 60 |
207 | 210 |
authorization_response_types = ['code', 'token'] |
208 | 211 |
|
209 |
grant_types = ['authorization_code', 'implicit']
|
|
212 |
grant_types = ['authorization_code'] |
|
210 | 213 |
|
211 | 214 |
response_cls = Response |
212 | 215 |
request_cls = Request |
... | ... | |
231 | 234 |
return self.response_cls(status=status, headers=headers, body=body) |
232 | 235 |
|
233 | 236 |
# ORM Methods |
234 |
def create_authorization_code(self, client, code, redirect_uri, scope,
|
|
235 |
state, **kwargs): |
|
237 |
def create_authorization_code(self, user, client, code, redirect_uri,
|
|
238 |
scope, state, **kwargs):
|
|
236 | 239 |
code_params = { |
237 | 240 |
'code': code, |
238 | 241 |
'redirect_uri': redirect_uri, |
239 |
'client_id': client.get_id(),
|
|
242 |
'client': client,
|
|
240 | 243 |
'scope': scope, |
241 |
'state': state |
|
244 |
'state': state, |
|
245 |
'user': user |
|
242 | 246 |
} |
243 | 247 |
code_params.update(kwargs) |
244 |
return self.code_model.create(code, **code_params) |
|
248 |
code_instance = self.code_model.create(**code_params) |
|
249 |
logger.info('%r created' % code_instance) |
|
250 |
return code_instance |
|
245 | 251 |
|
246 |
def _token_params(self, value, token_type, client, scope):
|
|
252 |
def _token_params(self, value, token_type, authorization, scope):
|
|
247 | 253 |
created_at = datetime.datetime.now() |
248 | 254 |
expires = self.token_expires |
249 | 255 |
expires_at = created_at + datetime.timedelta(seconds=expires) |
250 | 256 |
token_params = { |
251 |
'token': value,
|
|
257 |
'code': value,
|
|
252 | 258 |
'token_type': token_type, |
253 |
'client': client, |
|
254 |
'scope': scope, |
|
255 | 259 |
'created_at': created_at, |
256 |
'expires': expires, |
|
257 |
'expires_at': expires_at |
|
260 |
'expires_at': expires_at, |
|
261 |
'user': authorization.user, |
|
262 |
'redirect_uri': authorization.redirect_uri, |
|
263 |
'client': authorization.client, |
|
264 |
'scope': authorization.scope, |
|
258 | 265 |
} |
259 | 266 |
return token_params |
260 | 267 |
|
261 |
def create_token(self, value, token_type, client, scope, refresh=False): |
|
262 |
params = self._token_params(value, token_type, client, scope) |
|
268 |
def create_token(self, value, token_type, authorization, scope, |
|
269 |
refresh=False): |
|
270 |
params = self._token_params(value, token_type, authorization, scope) |
|
263 | 271 |
if refresh: |
264 | 272 |
refresh_token = self.generate_token() |
265 | 273 |
params['refresh_token'] = refresh_token |
266 | 274 |
# TODO: refresh token expires ??? |
267 |
token = self.token_model.create(value, **params) |
|
275 |
token = self.token_model.create(**params) |
|
276 |
logger.info('%r created' % token) |
|
277 |
return token |
|
268 | 278 |
|
269 |
def delete_authorization_code(self, code): |
|
270 |
del self.code_model.ENTRIES[code] |
|
279 |
# def delete_authorization_code(self, code):
|
|
280 |
# del self.code_model.ENTRIES[code]
|
|
271 | 281 |
|
272 | 282 |
def get_client_by_id(self, client_id): |
273 | 283 |
return self.client_model.get(client_id) |
... | ... | |
283 | 293 |
if not code_instance: |
284 | 294 |
raise OA2Error("Invalid code", code) |
285 | 295 |
|
286 |
if client.id != code_instance.client_id:
|
|
296 |
if client.get_id() != code_instance.client.get_id():
|
|
287 | 297 |
raise OA2Error("Invalid code for client", code, client) |
288 | 298 |
return code_instance |
289 | 299 |
|
... | ... | |
321 | 331 |
state, **kwargs) |
322 | 332 |
return code |
323 | 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 |
|
|
324 | 339 |
# |
325 | 340 |
# Response helpers |
326 | 341 |
# |
327 | 342 |
|
328 |
def grant_accept_response(self, client, redirect_uri, scope, state, |
|
329 |
request): |
|
343 |
def grant_accept_response(self, client, redirect_uri, scope, state): |
|
330 | 344 |
context = {'client': client.get_id(), 'redirect_uri': redirect_uri, |
331 |
'scope': scope, 'state': state, 'url': url} |
|
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} |
|
332 | 354 |
json_content = json.dumps(context) |
333 | 355 |
return self.response_cls(status=200, body=json_content) |
334 | 356 |
|
335 |
def build_redirect_to_login_response(self, request): |
|
336 |
return Response(302, headers={'Location': '/login'}) |
|
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)}) |
|
337 | 376 |
|
338 | 377 |
def build_response_from_error(self, exception): |
339 | 378 |
response = Response(400) |
... | ... | |
353 | 392 |
# Processor methods |
354 | 393 |
# |
355 | 394 |
|
356 |
def grant_authorization_code(self, user, client, code, redirect_uri, |
|
357 |
scope): |
|
358 |
code = self.get_client_authorization_code(client, code) |
|
359 |
if code.scope != scope: |
|
360 |
raise OA2Error("Invalid scope") |
|
361 |
token = self.add_token_for_client(client, "Bearer", code.scope, |
|
362 |
refresh=True) |
|
363 |
self.delete_authorization_code(code.code) |
|
364 |
return token |
|
365 |
|
|
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) |
|
366 | 398 |
|
367 | 399 |
# |
368 | 400 |
# Helpers |
369 | 401 |
# |
370 | 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 |
|
|
371 | 423 |
def _get_credentials(self, params, headers): |
372 | 424 |
if 'HTTP_AUTHORIZATION' in headers: |
373 | 425 |
scheme, b64credentials = headers.get( |
374 | 426 |
'HTTP_AUTHORIZATION').split(" ") |
427 |
if scheme != 'Basic': |
|
428 |
# TODO: raise 401 + WWW-Authenticate |
|
429 |
raise OA2Error("Unsupported authorization scheme") |
|
375 | 430 |
credentials = b64decode(b64credentials).split(":") |
376 | 431 |
return scheme, credentials |
377 | 432 |
else: |
378 | 433 |
return None, None |
379 | 434 |
pass |
380 | 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 |
|
|
381 | 443 |
def get_redirect_uri_from_params(self, client, params, default=True): |
382 | 444 |
""" |
383 | 445 |
Accepts a client instance and request parameters. |
... | ... | |
393 | 455 |
return redirect_uri |
394 | 456 |
|
395 | 457 |
# |
396 |
# Parameters validation methods
|
|
458 |
# Request identifiers
|
|
397 | 459 |
# |
398 | 460 |
|
399 |
def validate_client(params, meta, requires_auth=False):
|
|
400 |
raise OA2Error("Invalid client")
|
|
461 |
def identify_authorize_request(self, params, headers):
|
|
462 |
return params.get('response_type'), params
|
|
401 | 463 |
|
402 |
def validate_redirect_uri(client, params, headers, allow_default=True): |
|
403 |
raise OA2Error("Invalid redirect uri") |
|
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') |
|
404 | 469 |
|
405 |
def validate_state(params, meta): |
|
406 |
raise OA2Error("Invalid state") |
|
470 |
# |
|
471 |
# Parameters validation methods |
|
472 |
# |
|
407 | 473 |
|
408 |
def validate_scope(client, params, headers): |
|
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 in 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 |
return redirect_uri |
|
519 |
|
|
520 |
def validate_state(self, client, params, headers): |
|
521 |
return params.get('state') |
|
409 | 522 |
raise OA2Error("Invalid state") |
410 | 523 |
|
524 |
def validate_scope(self, client, params, headers): |
|
525 |
scope = params.get('scope') |
|
526 |
if scope is not None: |
|
527 |
scope = scope.split(' ')[0] # keep only the first |
|
528 |
# TODO: check for invalid characters |
|
529 |
return scope |
|
530 |
|
|
531 |
def validate_code(self, client, params, headers): |
|
532 |
code = params.get('code') |
|
533 |
if code is None: |
|
534 |
raise OA2Error("Missing authorization code") |
|
535 |
return self.get_client_authorization_code(client, code) |
|
536 |
|
|
411 | 537 |
# |
412 | 538 |
# Requests validation methods |
413 | 539 |
# |
414 | 540 |
|
415 |
def validate_code_request(params, headers): |
|
416 |
client = self.validate_client(params, headers, False) |
|
541 |
def validate_code_request(self, params, headers):
|
|
542 |
client = self.validate_client(params, headers, requires_auth=False)
|
|
417 | 543 |
redirect_uri = self.validate_redirect_uri(client, params, headers) |
418 | 544 |
scope = self.validate_scope(client, params, headers) |
419 | 545 |
state = self.validate_state(client, params, headers) |
420 | 546 |
return client, redirect_uri, scope, state |
421 | 547 |
|
422 |
def validate_token_request(params, headers):
|
|
423 |
client = self.validate_client(params, headers, False)
|
|
548 |
def validate_token_request(self, params, headers, requires_auth=False):
|
|
549 |
client = self.validate_client(params, headers) |
|
424 | 550 |
redirect_uri = self.validate_redirect_uri(client, params, headers) |
425 | 551 |
scope = self.validate_scope(client, params, headers) |
426 | 552 |
state = self.validate_state(client, params, headers) |
427 | 553 |
return client, redirect_uri, scope, state |
428 | 554 |
|
555 |
def validate_code_grant(self, params, headers): |
|
556 |
client = self.validate_client(params, headers, |
|
557 |
client_id_required=False) |
|
558 |
code_instance = self.validate_code(client, params, headers) |
|
559 |
redirect_uri = self.validate_redirect_uri( |
|
560 |
client, params, headers, |
|
561 |
expected_value=code_instance.redirect_uri) |
|
562 |
return client, redirect_uri, code_instance |
|
563 |
|
|
429 | 564 |
# |
430 | 565 |
# Endpoint methods |
431 | 566 |
# |
... | ... | |
448 | 583 |
|
449 | 584 |
if auth_type == 'code': |
450 | 585 |
client, uri, scope, state = \ |
451 |
self.validate_code_request(params, request.META)
|
|
586 |
self.validate_code_request(params, request.META) |
|
452 | 587 |
elif auth_type == 'token': |
453 |
client, uri, scope, state = \ |
|
454 |
self.validate_token_request(params, request.META) |
|
588 |
raise OA2Error("Unsupported response type") |
|
589 |
# client, uri, scope, state = \ |
|
590 |
# self.validate_token_request(params, request.META) |
|
455 | 591 |
else: |
456 | 592 |
#TODO: handle custom type |
457 | 593 |
raise OA2Error("Invalid authorization type") |
458 | 594 |
|
459 |
user = self.get_user_from_request(request)
|
|
595 |
user = getattr(request, 'user', None)
|
|
460 | 596 |
if not user: |
461 |
return self.redirect_to_login_response(request) |
|
597 |
return self.redirect_to_login_response(request, params)
|
|
462 | 598 |
|
463 | 599 |
if request.method == 'POST': |
464 | 600 |
if auth_type == 'code': |
465 | 601 |
return self.process_code_request(user, client, uri, scope, |
466 | 602 |
state) |
467 | 603 |
elif auth_type == 'token': |
468 |
return self.process_code_request(user, client, uri, scope, |
|
469 |
state) |
|
604 |
raise OA2Error("Unsupported response type") |
|
605 |
# return self.process_token_request(user, client, uri, scope, |
|
606 |
# state) |
|
470 | 607 |
else: |
471 | 608 |
#TODO: handle custom type |
472 | 609 |
raise OA2Error("Invalid authorization type") |
473 | 610 |
else: |
474 |
return self.grant_accept_response() |
|
611 |
if client.is_trusted: |
|
612 |
return self.process_code_request(user, client, uri, scope, |
|
613 |
state) |
|
614 |
else: |
|
615 |
return self.grant_accept_response(client, uri, scope, state) |
|
616 |
|
|
617 |
@handles_oa2_requests |
|
618 |
def grant_token(self, request, **extra): |
|
619 |
""" |
|
620 |
Used in the following cases |
|
621 |
""" |
|
622 |
if not request.secure: |
|
623 |
raise OA2Error("Secure request required") |
|
624 |
|
|
625 |
grant_type = self.identify_token_request(request.META, request.POST) |
|
626 |
|
|
627 |
if grant_type == 'authorization_code': |
|
628 |
client, redirect_uri, code = \ |
|
629 |
self.validate_code_grant(request.POST, request.META) |
|
630 |
token, token_type = \ |
|
631 |
self.grant_authorization_code(client, code, redirect_uri) |
|
632 |
return self.grant_token_response(token, token_type) |
|
633 |
elif (grant_type in ['client_credentials', 'token'] or |
|
634 |
self.is_uri(grant_type)): |
|
635 |
raise OA2Error("Unsupported grant type") |
|
636 |
else: |
|
637 |
#TODO: handle custom type |
|
638 |
raise OA2Error("Invalid grant type") |
b/snf-astakos-app/astakos/oa2/backends/djangobackend.py | ||
---|---|---|
1 |
import astakos.oa2.models as oa2_models |
|
2 |
|
|
1 | 3 |
from astakos.oa2.backends import base as oa2base |
2 | 4 |
from astakos.oa2.backends import base as errors |
3 |
from astakos.oa2.models import * |
|
4 | 5 |
|
5 |
from django.conf.urls.defaults import patterns, url |
|
6 | 6 |
from django import http |
7 |
from django.conf import settings |
|
8 |
from django.core.exceptions import ValidationError |
|
9 |
from django.core.validators import URLValidator |
|
10 |
from django.core.urlresolvers import reverse |
|
11 |
from django.conf.urls.defaults import patterns, url |
|
12 |
from django.views.decorators.csrf import csrf_exempt |
|
13 |
|
|
14 |
import logging |
|
15 |
logger = logging.getLogger(__name__) |
|
7 | 16 |
|
8 | 17 |
|
9 | 18 |
class DjangoViewsMixin(object): |
10 | 19 |
|
11 | 20 |
def auth_view(self, request): |
12 | 21 |
oa2request = self.build_request(request) |
13 |
response = self.authorize(oa2request, accept=False) |
|
14 |
return self._build_response(response) |
|
22 |
oa2response = self.authorize(oa2request, accept=False)
|
|
23 |
return self._build_response(oa2response)
|
|
15 | 24 |
|
25 |
@csrf_exempt |
|
16 | 26 |
def token_view(self, request): |
17 |
return http.HttpResponse("token view") |
|
27 |
oa2request = self.build_request(request) |
|
28 |
oa2response = self.grant_token(oa2request) |
|
29 |
return self._build_response(oa2response) |
|
18 | 30 |
|
19 | 31 |
|
20 | 32 |
class DjangoBackendORMMixin(object): |
21 | 33 |
|
22 | 34 |
def get_client_by_credentials(self, username, password): |
23 | 35 |
try: |
24 |
return Client.objects.get(identifier=username, secret=password) |
|
25 |
except Client.DoesNotExist: |
|
36 |
return oa2_models.Client.objects.get(identifier=username, |
|
37 |
secret=password) |
|
38 |
except oa2_models.Client.DoesNotExist: |
|
26 | 39 |
raise errors.InvalidClientID("No such client found") |
27 | 40 |
|
28 | 41 |
def get_client_by_id(self, clientid): |
29 | 42 |
try: |
30 |
return Client.objects.get(identifier=clientid) |
|
31 |
except Client.DoesNotExist: |
|
43 |
return oa2_models.Client.objects.get(identifier=clientid)
|
|
44 |
except oa2_models.Client.DoesNotExist:
|
|
32 | 45 |
raise errors.InvalidClientID("No such client found") |
33 | 46 |
|
34 |
def create_authorization_code(self, client, code, redirect_uri, scope, |
|
35 |
state, **kwargs): |
|
36 |
return AuthorizationCode.objects.create(**{ |
|
37 |
'code': code, |
|
38 |
'client_id': client.get_id(), |
|
39 |
'redirect_uri': redirect_uri, |
|
40 |
'scope': scope, |
|
41 |
'state': state |
|
42 |
}) |
|
43 |
|
|
44 |
def create_token(self, value, token_type, client, scope, refresh=False): |
|
45 |
params = self._token_params(value, token_type, client, scope) |
|
46 |
if refresh: |
|
47 |
refresh_token = self.generate_token() |
|
48 |
params['refresh_token'] = refresh_token |
|
49 |
# TODO: refresh token expires ??? |
|
50 |
token = self.token_model.create(value, **params) |
|
47 |
def get_authorization_code(self, code): |
|
48 |
try: |
|
49 |
return oa2_models.AuthorizationCode.objects.get(code=code) |
|
50 |
except oa2_models.AuthorizationCode.DoesNotExist: |
|
51 |
raise errors.OA2Error("No such authorization code") |
|
52 |
|
|
53 |
def get_token(self, token): |
|
54 |
try: |
|
55 |
return oa2_models.Token.objects.get(code=token) |
|
56 |
except oa2_models.Token.DoesNotExist: |
|
57 |
raise errors.OA2Error("No such token") |
|
51 | 58 |
|
52 | 59 |
def delete_authorization_code(self, code): |
53 |
del self.code_model.ENTRIES[code] |
|
60 |
code.delete() |
|
61 |
logger.info('%r deleted' % code) |
|
62 |
|
|
63 |
def delete_token(self, token): |
|
64 |
token.delete() |
|
65 |
logger.info('%r deleted' % token) |
|
66 |
|
|
67 |
def check_credentials(self, client, username, secret): |
|
68 |
if not (username == client.get_id() and secret == client.secret): |
|
69 |
raise errors.InvalidAuthorizationRequest("Invalid credentials") |
|
54 | 70 |
|
55 | 71 |
|
56 | 72 |
class DjangoBackend(DjangoBackendORMMixin, oa2base.SimpleBackend, |
57 | 73 |
DjangoViewsMixin): |
58 | 74 |
|
59 |
code_model = AuthorizationCode |
|
75 |
code_model = oa2_models.AuthorizationCode.objects |
|
76 |
token_model = oa2_models.Token.objects |
|
77 |
client_model = oa2_models.Client.objects |
|
60 | 78 |
|
61 | 79 |
def _build_response(self, oa2response): |
62 | 80 |
response = http.HttpResponse() |
... | ... | |
69 | 87 |
def build_request(self, django_request): |
70 | 88 |
params = { |
71 | 89 |
'method': django_request.method, |
90 |
'path': django_request.path, |
|
72 | 91 |
'GET': django_request.GET, |
73 | 92 |
'POST': django_request.POST, |
74 | 93 |
'META': django_request.META, |
75 |
'secure': django_request.is_secure(), |
|
94 |
'secure': settings.DEBUG or django_request.is_secure(), |
|
95 |
#'secure': django_request.is_secure(), |
|
76 | 96 |
} |
97 |
# TODO: check for valid astakos user |
|
77 | 98 |
if django_request.user.is_authenticated(): |
78 | 99 |
params['user'] = django_request.user |
79 | 100 |
return oa2base.Request(**params) |
80 | 101 |
|
81 | 102 |
def get_url_patterns(self): |
103 |
sep = '/' if self.endpoints_prefix else '' |
|
82 | 104 |
_patterns = patterns( |
83 | 105 |
'', |
84 |
url(r'^%s/auth/?$' % self.endpoints_prefix, self.auth_view, |
|
106 |
url(r'^%s%sauth/?$' % (self.endpoints_prefix, sep), |
|
107 |
self.auth_view, |
|
85 | 108 |
name='%s_authenticate' % self.id), |
86 |
url(r'^%s/token/?$' % self.endpoints_prefix, self.token_view, |
|
109 |
url(r'^%s%stoken/?$' % (self.endpoints_prefix, sep), |
|
110 |
self.token_view, |
|
87 | 111 |
name='%s_token' % self.id), |
88 | 112 |
) |
89 | 113 |
return _patterns |
90 | 114 |
|
115 |
def is_uri(self, string): |
|
116 |
validator = URLValidator() |
|
117 |
try: |
|
118 |
validator(string) |
|
119 |
except ValidationError: |
|
120 |
return False |
|
121 |
else: |
|
122 |
return True |
|
123 |
|
|
124 |
def get_login_uri(self): |
|
125 |
return reverse('login') |
|
126 |
|
|
91 | 127 |
|
92 | 128 |
class AstakosBackend(DjangoBackend): |
93 | 129 |
pass |
b/snf-astakos-app/astakos/oa2/models.py | ||
---|---|---|
1 |
import datetime |
|
2 |
import urlparse |
|
3 |
|
|
1 | 4 |
from django.db import models |
2 | 5 |
from django.utils.translation import ugettext_lazy as _ |
3 |
|
|
4 |
from astakos.oa2 import settings |
|
6 |
from django.core.exceptions import ValidationError |
|
5 | 7 |
|
6 | 8 |
CLIENT_TYPES = ( |
7 | 9 |
('confidential', _('Confidential')), |
... | ... | |
10 | 12 |
|
11 | 13 |
CONFIDENTIAL_TYPES = ['confidential'] |
12 | 14 |
|
15 |
TOKEN_TYPES = (('Basic', _('Basic')), |
|
16 |
('Bearer', _('Bearer'))) |
|
17 |
|
|
18 |
GRANT_TYPES = (('authorization_code', _('Authorization code')), |
|
19 |
('password', _('Password')), |
|
20 |
('client_credentials', _('Client Credentials'))) |
|
21 |
|
|
13 | 22 |
|
14 | 23 |
class RedirectUrl(models.Model): |
15 |
client = models.ForeignKey('oa2.Client') |
|
16 |
default = models.BooleanField(default=True) |
|
24 |
client = models.ForeignKey('oa2.Client', on_delete=models.PROTECT)
|
|
25 |
is_default = models.BooleanField(default=True)
|
|
17 | 26 |
url = models.URLField(unique=True) |
18 | 27 |
|
19 | 28 |
class Meta: |
20 |
ordering = ('default', ) |
|
29 |
ordering = ('is_default', ) |
|
30 |
unique_together = ('client', 'url',) |
|
21 | 31 |
|
22 | 32 |
|
23 | 33 |
class Client(models.Model): |
24 | 34 |
name = models.CharField(max_length=100) |
25 | 35 |
identifier = models.CharField(max_length=255, unique=True) |
26 |
secret = models.CharField(max_length=255) |
|
36 |
secret = models.CharField(max_length=255, null=True, default=None)
|
|
27 | 37 |
url = models.CharField(max_length=255) |
28 | 38 |
type = models.CharField(max_length=100, choices=CLIENT_TYPES, |
29 | 39 |
default='confidential') |
40 |
is_trusted = models.BooleanField(default=False) |
|
41 |
|
|
42 |
def save(self, **kwargs): |
|
43 |
if self.secret is None and self.type == 'confidential': |
|
44 |
raise ValidationError("Confidential clients require a secret") |
|
45 |
super(Client, self).save(**kwargs) |
|
30 | 46 |
|
31 | 47 |
def requires_auth(self): |
32 | 48 |
return self.type in CONFIDENTIAL_TYPES |
... | ... | |
35 | 51 |
return self.redirecturl_set.get().url |
36 | 52 |
|
37 | 53 |
def redirect_uri_is_valid(self, uri): |
54 |
# ignore user specific uri part |
|
55 |
parts = list(urlparse.urlsplit(uri)) |
|
56 |
path = parts[2] |
|
57 |
pieces = path.rsplit('/', 3) |
|
58 |
parts[2] = '/'.join(pieces[:-3]) if len(pieces) > 3 else path |
|
59 |
uri = urlparse.urlunsplit(parts) |
|
60 |
|
|
61 |
# TODO: handle trailing slashes |
|
38 | 62 |
return self.redirecturl_set.filter(url=uri).count() > 0 |
39 | 63 |
|
40 | 64 |
def get_id(self): |
... | ... | |
42 | 66 |
|
43 | 67 |
|
44 | 68 |
class AuthorizationCode(models.Model): |
69 |
user = models.ForeignKey('im.AstakosUser', on_delete=models.PROTECT) |
|
45 | 70 |
code = models.TextField() |
46 | 71 |
redirect_uri = models.CharField(max_length=255) |
47 |
client_id = models.CharField(max_length=255, db_index=True) |
|
48 |
scope = models.TextField() |
|
72 |
client = models.ForeignKey('oa2.Client', on_delete=models.PROTECT) |
|
73 |
scope = models.TextField(null=True, default=None) |
|
74 |
created_at = models.DateTimeField(default=datetime.datetime.now()) |
|
49 | 75 |
|
50 | 76 |
# not really useful |
51 |
state = models.TextField() |
|
77 |
state = models.TextField(null=True, default=None) |
|
78 |
|
|
79 |
def client_id_is_valid(self, client_id): |
|
80 |
return self.client_id == client_id |
|
81 |
|
|
82 |
def redirect_uri_is_valid(self, redirect_uri, client): |
|
83 |
return (self.redirect_uri == redirect_uri and |
|
84 |
client.redirect_uri_is_valid(redirect_uri)) |
|
85 |
|
|
86 |
def __repr__(self): |
|
87 |
return ("Authorization code: %s " |
|
88 |
"(user: %s, client: %s, redirect_uri: %s, scope: %s)" % ( |
|
89 |
self.code, |
|
90 |
self.user.log_display, |
|
91 |
self.client.get_id(), |
|
92 |
self.redirect_uri, self.scope)) |
|
52 | 93 |
|
53 | 94 |
|
54 | 95 |
class Token(models.Model): |
55 | 96 |
code = models.TextField() |
97 |
created_at = models.DateTimeField(default=datetime.datetime.now()) |
|
56 | 98 |
expires_at = models.DateTimeField() |
99 |
token_type = models.CharField(max_length=100, choices=TOKEN_TYPES, |
|
100 |
default='Bearer') |
|
101 |
grant_type = models.CharField(max_length=100, choices=GRANT_TYPES, |
|
102 |
default='authorization_code') |
|
103 |
|
|
104 |
# authorization fields |
|
105 |
user = models.ForeignKey('im.AstakosUser', on_delete=models.PROTECT) |
|
106 |
redirect_uri = models.CharField(max_length=255) |
|
107 |
client = models.ForeignKey('oa2.Client', on_delete=models.PROTECT) |
|
108 |
scope = models.TextField(null=True, default=None) |
|
109 |
|
|
110 |
# not really useful |
|
111 |
state = models.TextField(null=True, default=None) |
|
112 |
|
|
113 |
def __repr__(self): |
|
114 |
return ("Token: %s (token_type: %s, grant_type: %s, " |
|
115 |
"user: %s, client: %s, scope: %s)" % ( |
|
116 |
self.code, self.token_type, self.grant_type, |
|
117 |
self.user.log_display, self.client.get_id(), self.scope)) |
b/snf-astakos-app/astakos/oa2/tests/djangobackend.py | ||
---|---|---|
10 | 10 |
from django.core.urlresolvers import reverse |
11 | 11 |
from django.contrib.auth.models import User |
12 | 12 |
|
13 |
from astakos.oa2.models import Client, RedirectUrl, AuthorizationCode
|
|
13 |
from astakos.oa2.models import Client, AuthorizationCode |
|
14 | 14 |
|
15 | 15 |
|
16 | 16 |
ParsedURL = namedtuple('ParsedURL', ['host', 'scheme', 'path', 'params', |
... | ... | |
120 | 120 |
params.update(urlparams) |
121 | 121 |
self.set_auth_headers(kwargs) |
122 | 122 |
if 'reject' in params: |
123 |
self.post(self.get_url(self.auth_url), data=params) |
|
123 |
return self.post(self.get_url(self.auth_url), data=params, |
|
124 |
**kwargs) |
|
124 | 125 |
return self.get(self.get_url(self.auth_url, **params), *args, **kwargs) |
125 | 126 |
|
126 | 127 |
def set_auth_headers(self, params): |
128 |
print 'self.credentials:', self.credentials |
|
127 | 129 |
if not self.credentials: |
128 | 130 |
return |
129 | 131 |
credentials = base64.encodestring('%s:%s' % self.credentials).strip() |
... | ... | |
145 | 147 |
baseurl = reverse('oa2_authenticate').replace('/auth', '/') |
146 | 148 |
self.client = OA2Client(baseurl) |
147 | 149 |
client1 = Client.objects.create(identifier="client1", secret="secret") |
150 |
self.client1_redirect_uri = "https://server.com/handle_code" |
|
151 |
client1.redirecturl_set.create(url=self.client1_redirect_uri) |
|
152 |
|
|
148 | 153 |
client2 = Client.objects.create(identifier="client2", type='public') |
149 | 154 |
self.client2_redirect_uri = "https://server2.com/handle_code" |
150 | 155 |
client2.redirecturl_set.create(url=self.client2_redirect_uri) |
151 |
self.client1_redirect_uri = "https://server.com/handle_code" |
|
152 |
client1.redirecturl_set.create(url=self.client1_redirect_uri) |
|
156 |
|
|
157 |
client3 = Client.objects.create(identifier="client3", secret='secret', |
|
158 |
is_trusted=True) |
|
159 |
self.client3_redirect_uri = "https://server3.com/handle_code" |
|
160 |
client3.redirecturl_set.create(url=self.client3_redirect_uri) |
|
153 | 161 |
|
154 | 162 |
u = User.objects.create(username="user@synnefo.org") |
155 | 163 |
u.set_password("password") |
... | ... | |
160 | 168 |
self.assertEqual(r.status_code, 400) |
161 | 169 |
self.assertCount(AuthorizationCode, 0) |
162 | 170 |
|
163 |
# no auth header, client is confidential |
|
164 |
r = self.client.authorize_code('client1') |
|
165 |
self.assertEqual(r.status_code, 400) |
|
166 |
self.assertCount(AuthorizationCode, 0) |
|
167 |
|
|
168 |
# no redirect_uri |
|
169 |
#self.client.credentials = ('client1', 'secret') |
|
170 |
#r = self.client.authorize_code('client1') |
|
171 |
#self.assertEqual(r.status_code, 400) |
|
172 |
#self.assertCount(AuthorizationCode, 0) |
|
171 |
# # no auth header, client is confidential |
|
172 |
# r = self.client.authorize_code('client1') |
|
173 |
# self.assertEqual(r.status_code, 400) |
|
174 |
# self.assertCount(AuthorizationCode, 0) |
|
173 | 175 |
|
174 | 176 |
# mixed up credentials/client_id's |
175 | 177 |
self.client.set_credentials('client1', 'secret') |
... | ... | |
182 | 184 |
self.assertEqual(r.status_code, 400) |
183 | 185 |
self.assertCount(AuthorizationCode, 0) |
184 | 186 |
|
185 |
self.client.set_credentials() |
|
186 |
r = self.client.authorize_code('client1') |
|
187 |
self.assertEqual(r.status_code, 400) |
|
188 |
self.assertCount(AuthorizationCode, 0) |
|
187 |
# self.client.set_credentials()
|
|
188 |
# r = self.client.authorize_code('client1')
|
|
189 |
# self.assertEqual(r.status_code, 400)
|
|
190 |
# self.assertCount(AuthorizationCode, 0)
|
|
189 | 191 |
|
190 | 192 |
# valid request |
191 | 193 |
params = {'redirect_uri': self.client1_redirect_uri, |
... | ... | |
193 | 195 |
self.client.set_credentials('client1', 'secret') |
194 | 196 |
r = self.client.authorize_code('client1', urlparams=params) |
195 | 197 |
self.assertEqual(r.status_code, 302) |
198 |
self.assertTrue('Location' in r) |
|
199 |
p = urlparse.urlparse(r['Location']) |
|
200 |
self.assertEqual(p.netloc, 'testserver:80') |
|
201 |
self.assertEqual(p.path, reverse('login')) |
|
196 | 202 |
|
197 |
self.client.set_credentials() |
|
203 |
self.client.set_credentials('client1', 'secret')
|
|
198 | 204 |
self.client.login(username="user@synnefo.org", password="password") |
199 | 205 |
r = self.client.authorize_code('client1', urlparams=params) |
200 |
print r |
|
201 | 206 |
self.assertEqual(r.status_code, 200) |
202 | 207 |
|
203 | 208 |
r = self.client.authorize_code('client1', urlparams=params, |
... | ... | |
219 | 224 |
self.assertCount(AuthorizationCode, 2) |
220 | 225 |
|
221 | 226 |
code1 = AuthorizationCode.objects.get(code=redirect1.params['code'][0]) |
222 |
self.assertEqual(code1.state, '') |
|
227 |
#self.assertEqual(code1.state, '') |
|
228 |
self.assertEqual(code1.state, None) |
|
223 | 229 |
self.assertEqual(code1.redirect_uri, self.client1_redirect_uri) |
224 | 230 |
|
225 | 231 |
code2 = AuthorizationCode.objects.get(code=redirect2.params['code'][0]) |
b/snf-astakos-app/astakos/oa2/urls.py | ||
---|---|---|
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 |
from astakos.oa2.backends import DjangoBackend |
|
35 |
|
|
36 |
oa2_backend = DjangoBackend(endpoints_prefix='') |
|
37 |
urlpatterns = oa2_backend.get_url_patterns() |
b/snf-astakos-app/astakos/scripts/snf_service_export.py | ||
---|---|---|
69 | 69 |
{'versionId': '', |
70 | 70 |
'publicURL': None}, |
71 | 71 |
], |
72 |
'resources': {}, |
|
72 | 73 |
}, |
74 |
|
|
75 |
'astakos_oa2': { |
|
76 |
'type': 'astakos_auth', |
|
77 |
'component': 'astakos', |
|
78 |
'prefix': 'oa2', |
|
79 |
'public': True, |
|
80 |
'endpoints': [ |
|
81 |
{'versionId': '', |
|
82 |
'publicURL': None}, |
|
83 |
], |
|
84 |
'resources': {}, |
|
85 |
} |
|
73 | 86 |
} |
74 | 87 |
|
75 | 88 |
cyclades_services = { |
b/snf-astakos-app/astakos/urls.py | ||
---|---|---|
33 | 33 |
|
34 | 34 |
from django.conf.urls import include, patterns |
35 | 35 |
|
36 |
from astakos.im.settings import BASE_URL, BASE_PATH, ACCOUNTS_PREFIX, \
|
|
37 |
VIEWS_PREFIX, KEYSTONE_PREFIX, WEBLOGIN_PREFIX, ADMIN_PREFIX |
|
36 |
from astakos.im.settings import BASE_PATH, ACCOUNTS_PREFIX, \ |
|
37 |
VIEWS_PREFIX, KEYSTONE_PREFIX, WEBLOGIN_PREFIX, ADMIN_PREFIX, OA2_PREFIX
|
|
38 | 38 |
from snf_django.lib.api.utils import prefix_pattern |
39 | 39 |
from snf_django.utils.urls import \ |
40 | 40 |
extend_with_root_redirects, extend_endpoint_with_slash |
41 | 41 |
from astakos.im.settings import astakos_services |
42 |
from astakos.oa2.backends import DjangoBackend, AstakosBackend |
|
43 | 42 |
|
44 | 43 |
urlpatterns = [] |
45 | 44 |
|
... | ... | |
47 | 46 |
extend_endpoint_with_slash(urlpatterns, astakos_services, 'astakos_ui') |
48 | 47 |
extend_endpoint_with_slash(urlpatterns, astakos_services, 'astakos_weblogin') |
49 | 48 |
|
50 |
oa2_backend = DjangoBackend(BASE_URL, 'oa2') |
|
51 |
astakos_oa2_backend = AstakosBackend(BASE_URL, 'oa2/services', 'services') |
|
52 |
|
|
53 | 49 |
astakos_patterns = patterns( |
54 | 50 |
'', |
55 | 51 |
(prefix_pattern(VIEWS_PREFIX), include('astakos.im.urls')), |
... | ... | |
57 | 53 |
(prefix_pattern(KEYSTONE_PREFIX), include('astakos.api.keystone_urls')), |
58 | 54 |
(prefix_pattern(WEBLOGIN_PREFIX), include('astakos.im.weblogin_urls')), |
59 | 55 |
(prefix_pattern(ADMIN_PREFIX), include('astakos.admin.admin_urls')), |
56 |
(prefix_pattern(OA2_PREFIX), include('astakos.oa2.urls')), |
|
60 | 57 |
) |
61 | 58 |
|
62 | 59 |
|
... | ... | |
68 | 65 |
# set utility redirects |
69 | 66 |
extend_with_root_redirects(urlpatterns, astakos_services, |
70 | 67 |
'astakos_ui', BASE_PATH) |
71 |
|
|
72 |
urlpatterns += oa2_backend.get_url_patterns() |
|
73 |
#urlpatterns += astakos_oa2_backend.get_url_patterns() |
Also available in: Unified diff