Commit 779572c2 authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

astakos: Extend oa2 app

parent 2f0f58ea
......@@ -96,5 +96,18 @@ astakos_services = {
{'versionId': '',
'publicURL': None},
],
'resources': {},
},
'astakos_oa2': {
'type': 'astakos_auth',
'component': 'astakos',
'prefix': 'oa2',
'public': True,
'endpoints': [
{'versionId': '',
'publicURL': None},
],
'resources': {},
}
}
......@@ -55,6 +55,7 @@ VIEWS_PREFIX = get_path(astakos_services, 'astakos_ui.prefix')
KEYSTONE_PREFIX = get_path(astakos_services, 'astakos_identity.prefix')
WEBLOGIN_PREFIX = get_path(astakos_services, 'astakos_weblogin.prefix')
ADMIN_PREFIX = get_path(astakos_services, 'astakos_admin.prefix')
OA2_PREFIX = get_path(astakos_services, 'astakos_oa2.prefix')
# Set the expiration time of newly created auth tokens
# to be this many hours after their creation time.
......
......@@ -6,6 +6,8 @@ import json
from base64 import b64encode, b64decode
from hashlib import sha512
from time import time, mktime
import logging
logger = logging.getLogger(__name__)
......@@ -64,9 +66,10 @@ class Response(object):
class Request(object):
def __init__(self, method, GET=None, POST=None, META=None, secure=False,
user=None):
def __init__(self, method, path, GET=None, POST=None, META=None,
secure=False, user=None):
self.method = method
self.path = path
if not GET:
GET = {}
......@@ -177,8 +180,8 @@ class BackendBase(type):
def __new__(cls, name, bases, attrs):
super_new = super(BackendBase, cls).__new__
parents = [b for b in bases if isinstance(b, BackendBase)]
meta = attrs.pop('Meta', None)
#parents = [b for b in bases if isinstance(b, BackendBase)]
#meta = attrs.pop('Meta', None)
return super_new(cls, name, bases, attrs)
@classmethod
......@@ -200,13 +203,13 @@ class SimpleBackend(object):
token_endpoint = 'token/'
token_length = 30
token_expires = 3600
token_expires = 300
authorization_endpoint = 'auth/'
authorization_code_length = 60
authorization_response_types = ['code', 'token']
grant_types = ['authorization_code', 'implicit']
grant_types = ['authorization_code']
response_cls = Response
request_cls = Request
......@@ -231,43 +234,50 @@ class SimpleBackend(object):
return self.response_cls(status=status, headers=headers, body=body)
# ORM Methods
def create_authorization_code(self, client, code, redirect_uri, scope,
state, **kwargs):
def create_authorization_code(self, user, client, code, redirect_uri,
scope, state, **kwargs):
code_params = {
'code': code,
'redirect_uri': redirect_uri,
'client_id': client.get_id(),
'client': client,
'scope': scope,
'state': state
'state': state,
'user': user
}
code_params.update(kwargs)
return self.code_model.create(code, **code_params)
code_instance = self.code_model.create(**code_params)
logger.info('%r created' % code_instance)
return code_instance
def _token_params(self, value, token_type, client, scope):
def _token_params(self, value, token_type, authorization, scope):
created_at = datetime.datetime.now()
expires = self.token_expires
expires_at = created_at + datetime.timedelta(seconds=expires)
token_params = {
'token': value,
'code': value,
'token_type': token_type,
'client': client,
'scope': scope,
'created_at': created_at,
'expires': expires,
'expires_at': expires_at
'expires_at': expires_at,
'user': authorization.user,
'redirect_uri': authorization.redirect_uri,
'client': authorization.client,
'scope': authorization.scope,
}
return token_params
def create_token(self, value, token_type, client, scope, refresh=False):
params = self._token_params(value, token_type, client, scope)
def create_token(self, value, token_type, authorization, scope,
refresh=False):
params = self._token_params(value, token_type, authorization, scope)
if refresh:
refresh_token = self.generate_token()
params['refresh_token'] = refresh_token
# TODO: refresh token expires ???
token = self.token_model.create(value, **params)
token = self.token_model.create(**params)
logger.info('%r created' % token)
return token
def delete_authorization_code(self, code):
del self.code_model.ENTRIES[code]
# def delete_authorization_code(self, code):
# del self.code_model.ENTRIES[code]
def get_client_by_id(self, client_id):
return self.client_model.get(client_id)
......@@ -283,7 +293,7 @@ class SimpleBackend(object):
if not code_instance:
raise OA2Error("Invalid code", code)
if client.id != code_instance.client_id:
if client.get_id() != code_instance.client.get_id():
raise OA2Error("Invalid code for client", code, client)
return code_instance
......@@ -321,19 +331,48 @@ class SimpleBackend(object):
state, **kwargs)
return code
def add_token_for_client(self, token_type, authorization, refresh=False):
token = self.generate_token()
self.create_token(token, token_type, authorization, refresh)
return token
#
# Response helpers
#
def grant_accept_response(self, client, redirect_uri, scope, state,
request):
def grant_accept_response(self, client, redirect_uri, scope, state):
context = {'client': client.get_id(), 'redirect_uri': redirect_uri,
'scope': scope, 'state': state, 'url': url}
'scope': scope, 'state': state,
#'url': url,
}
json_content = json.dumps(context)
return self.response_cls(status=200, body=json_content)
def grant_token_response(self, token, token_type):
context = {'access_token': token, 'token_type': token_type,
'expires_in': self.token_expires}
json_content = json.dumps(context)
return self.response_cls(status=200, body=json_content)
def build_redirect_to_login_response(self, request):
return Response(302, headers={'Location': '/login'})
def redirect_to_login_response(self, request, params):
parts = list(urlparse.urlsplit(request.path))
parts[3] = urllib.urlencode(params)
query = {'next': urlparse.urlunsplit(parts)}
return Response(302,
headers={'Location': '%s?%s' %
(self.get_login_uri(),
urllib.urlencode(query))})
def redirect_to_uri(self, redirect_uri, code, state=None):
parts = list(urlparse.urlsplit(redirect_uri))
params = dict(urlparse.parse_qsl(parts[3], keep_blank_values=True))
params['code'] = code
if state is not None:
params['state'] = state
parts[3] = urllib.urlencode(params)
return Response(302,
headers={'Location': '%s' %
urlparse.urlunsplit(parts)})
def build_response_from_error(self, exception):
response = Response(400)
......@@ -353,31 +392,54 @@ class SimpleBackend(object):
# Processor methods
#
def grant_authorization_code(self, user, client, code, redirect_uri,
scope):
code = self.get_client_authorization_code(client, code)
if code.scope != scope:
raise OA2Error("Invalid scope")
token = self.add_token_for_client(client, "Bearer", code.scope,
refresh=True)
self.delete_authorization_code(code.code)
return token
def process_code_request(self, user, client, uri, scope, state):
code = self.add_authorization_code(user, client, uri, scope, state)
return self.redirect_to_uri(uri, code, state)
#
# Helpers
#
def grant_authorization_code(self, client, code_instance, redirect_uri,
scope=None, token_type="Bearer"):
if scope and code_instance.scope != scope:
raise OA2Error("Invalid scope")
if redirect_uri != code_instance.redirect_uri:
raise OA2Error("The redirect uri does not match "
"the one used during authorization")
token = self.add_token_for_client(token_type, code_instance)
self.delete_authorization_code(code_instance) # use only once
return token, token_type
def consume_token(self, token):
token_instance = self.get_token(token)
expires_at = mktime(token_instance.expires_at.timetuple())
if time() > expires_at:
self.delete_token(token_instance) # delete expired token
raise OA2Error("Token has expired")
# TODO: delete token?
return token_instance
def _get_credentials(self, params, headers):
if 'HTTP_AUTHORIZATION' in headers:
scheme, b64credentials = headers.get(
'HTTP_AUTHORIZATION').split(" ")
if scheme != 'Basic':
# TODO: raise 401 + WWW-Authenticate
raise OA2Error("Unsupported authorization scheme")
credentials = b64decode(b64credentials).split(":")
return scheme, credentials
else:
return None, None
pass
def _get_authorization(self, params, headers):
scheme, client_credentials = self._get_credentials(params, headers)
no_authorization = scheme is None and client_credentials is None
if no_authorization:
raise OA2Error("Missing authorization header")
return client_credentials
def get_redirect_uri_from_params(self, client, params, default=True):
"""
Accepts a client instance and request parameters.
......@@ -393,39 +455,112 @@ class SimpleBackend(object):
return redirect_uri
#
# Parameters validation methods
# Request identifiers
#
def validate_client(params, meta, requires_auth=False):
raise OA2Error("Invalid client")
def identify_authorize_request(self, params, headers):
return params.get('response_type'), params
def validate_redirect_uri(client, params, headers, allow_default=True):
raise OA2Error("Invalid redirect uri")
def identify_token_request(self, headers, params):
content_type = headers.get('CONTENT_TYPE')
if content_type != 'application/x-www-form-urlencoded':
raise OA2Error("Invalid Content-Type header")
return params.get('grant_type')
def validate_state(params, meta):
raise OA2Error("Invalid state")
#
# Parameters validation methods
#
def validate_scope(client, params, headers):
def validate_client(self, params, meta, requires_auth=True,
client_id_required=True):
client_id = params.get('client_id')
if client_id is None and client_id_required:
raise OA2Error("Client identification is required")
client_credentials = None
try: # check authorization header
client_credentials = self._get_authorization(params, meta)
if client_credentials is not None:
_client_id = client_credentials[0]
if client_id is not None and client_id != _client_id:
raise OA2Error("Client identification conflicts "
"with client authorization")
client_id = _client_id
except:
pass
if client_id is None:
raise OA2Error("Missing client identification")
client = self.get_client_by_id(client_id)
if requires_auth and client.requires_auth:
if client_credentials is None:
raise OA2Error("Client authentication in required")
if client_credentials is not None:
self.check_credentials(client, *client_credentials)
return client
def validate_redirect_uri(self, client, params, headers,
allow_default=True, is_required=False,
expected_value=None):
redirect_uri = params.get('redirect_uri')
if is_required and redirect_uri is None:
raise OA2Error("Missing redirect uri")
if redirect_uri is not None:
if not bool(urlparse.urlparse(redirect_uri).scheme):
raise OA2Error("Redirect uri should be an absolute URI")
if not client.redirect_uri_is_valid(redirect_uri):
raise OA2Error("Mismatching redirect uri")
if expected_value is not None and redirect_uri != expected_value:
raise OA2Error("Invalid redirect uri")
return redirect_uri
def validate_state(self, client, params, headers):
return params.get('state')
raise OA2Error("Invalid state")
def validate_scope(self, client, params, headers):
scope = params.get('scope')
if scope is not None:
scope = scope.split(' ')[0] # keep only the first
# TODO: check for invalid characters
return scope
def validate_code(self, client, params, headers):
code = params.get('code')
if code is None:
raise OA2Error("Missing authorization code")
return self.get_client_authorization_code(client, code)
#
# Requests validation methods
#
def validate_code_request(params, headers):
client = self.validate_client(params, headers, False)
def validate_code_request(self, params, headers):
client = self.validate_client(params, headers, requires_auth=False)
redirect_uri = self.validate_redirect_uri(client, params, headers)
scope = self.validate_scope(client, params, headers)
state = self.validate_state(client, params, headers)
return client, redirect_uri, scope, state
def validate_token_request(params, headers):
client = self.validate_client(params, headers, False)
def validate_token_request(self, params, headers, requires_auth=False):
client = self.validate_client(params, headers)
redirect_uri = self.validate_redirect_uri(client, params, headers)
scope = self.validate_scope(client, params, headers)
state = self.validate_state(client, params, headers)
return client, redirect_uri, scope, state
def validate_code_grant(self, params, headers):
client = self.validate_client(params, headers,
client_id_required=False)
code_instance = self.validate_code(client, params, headers)
redirect_uri = self.validate_redirect_uri(
client, params, headers,
expected_value=code_instance.redirect_uri)
return client, redirect_uri, code_instance
#
# Endpoint methods
#
......@@ -448,27 +583,56 @@ class SimpleBackend(object):
if auth_type == 'code':
client, uri, scope, state = \
self.validate_code_request(params, request.META)
self.validate_code_request(params, request.META)
elif auth_type == 'token':
client, uri, scope, state = \
self.validate_token_request(params, request.META)
raise OA2Error("Unsupported response type")
# client, uri, scope, state = \
# self.validate_token_request(params, request.META)
else:
#TODO: handle custom type
raise OA2Error("Invalid authorization type")
user = self.get_user_from_request(request)
user = getattr(request, 'user', None)
if not user:
return self.redirect_to_login_response(request)
return self.redirect_to_login_response(request, params)
if request.method == 'POST':
if auth_type == 'code':
return self.process_code_request(user, client, uri, scope,
state)
elif auth_type == 'token':
return self.process_code_request(user, client, uri, scope,
state)
raise OA2Error("Unsupported response type")
# return self.process_token_request(user, client, uri, scope,
# state)
else:
#TODO: handle custom type
raise OA2Error("Invalid authorization type")
else:
return self.grant_accept_response()
if client.is_trusted:
return self.process_code_request(user, client, uri, scope,
state)
else:
return self.grant_accept_response(client, uri, scope, state)
@handles_oa2_requests
def grant_token(self, request, **extra):
"""
Used in the following cases
"""
if not request.secure:
raise OA2Error("Secure request required")
grant_type = self.identify_token_request(request.META, request.POST)
if grant_type == 'authorization_code':
client, redirect_uri, code = \
self.validate_code_grant(request.POST, request.META)
token, token_type = \
self.grant_authorization_code(client, code, redirect_uri)
return self.grant_token_response(token, token_type)
elif (grant_type in ['client_credentials', 'token'] or
self.is_uri(grant_type)):
raise OA2Error("Unsupported grant type")
else:
#TODO: handle custom type
raise OA2Error("Invalid grant type")
import astakos.oa2.models as oa2_models
from astakos.oa2.backends import base as oa2base
from astakos.oa2.backends import base as errors
from astakos.oa2.models import *
from django.conf.urls.defaults import patterns, url
from django import http
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import URLValidator
from django.core.urlresolvers import reverse
from django.conf.urls.defaults import patterns, url
from django.views.decorators.csrf import csrf_exempt
import logging
logger = logging.getLogger(__name__)
class DjangoViewsMixin(object):
def auth_view(self, request):
oa2request = self.build_request(request)
response = self.authorize(oa2request, accept=False)
return self._build_response(response)
oa2response = self.authorize(oa2request, accept=False)
return self._build_response(oa2response)
@csrf_exempt
def token_view(self, request):
return http.HttpResponse("token view")
oa2request = self.build_request(request)
oa2response = self.grant_token(oa2request)
return self._build_response(oa2response)
class DjangoBackendORMMixin(object):
def get_client_by_credentials(self, username, password):
try:
return Client.objects.get(identifier=username, secret=password)
except Client.DoesNotExist:
return oa2_models.Client.objects.get(identifier=username,
secret=password)
except oa2_models.Client.DoesNotExist:
raise errors.InvalidClientID("No such client found")
def get_client_by_id(self, clientid):
try:
return Client.objects.get(identifier=clientid)
except Client.DoesNotExist:
return oa2_models.Client.objects.get(identifier=clientid)
except oa2_models.Client.DoesNotExist:
raise errors.InvalidClientID("No such client found")
def create_authorization_code(self, client, code, redirect_uri, scope,
state, **kwargs):
return AuthorizationCode.objects.create(**{
'code': code,
'client_id': client.get_id(),
'redirect_uri': redirect_uri,
'scope': scope,
'state': state
})
def create_token(self, value, token_type, client, scope, refresh=False):
params = self._token_params(value, token_type, client, scope)
if refresh:
refresh_token = self.generate_token()
params['refresh_token'] = refresh_token
# TODO: refresh token expires ???
token = self.token_model.create(value, **params)
def get_authorization_code(self, code):
try:
return oa2_models.AuthorizationCode.objects.get(code=code)
except oa2_models.AuthorizationCode.DoesNotExist:
raise errors.OA2Error("No such authorization code")
def get_token(self, token):
try:
return oa2_models.Token.objects.get(code=token)
except oa2_models.Token.DoesNotExist:
raise errors.OA2Error("No such token")
def delete_authorization_code(self, code):
del self.code_model.ENTRIES[code]
code.delete()
logger.info('%r deleted' % code)
def delete_token(self, token):
token.delete()
logger.info('%r deleted' % token)
def check_credentials(self, client, username, secret):
if not (username == client.get_id() and secret == client.secret):
raise errors.InvalidAuthorizationRequest("Invalid credentials")
class DjangoBackend(DjangoBackendORMMixin, oa2base.SimpleBackend,
DjangoViewsMixin):
code_model = AuthorizationCode
code_model = oa2_models.AuthorizationCode.objects
token_model = oa2_models.Token.objects
client_model = oa2_models.Client.objects
def _build_response(self, oa2response):
response = http.HttpResponse()
......@@ -69,25 +87,43 @@ class DjangoBackend(DjangoBackendORMMixin, oa2base.SimpleBackend,
def build_request(self, django_request):
params = {
'method': django_request.method,
'path': django_request.path,
'GET': django_request.GET,
'POST': django_request.POST,
'META': django_request.META,
'secure': django_request.is_secure(),
'secure': settings.DEBUG or django_request.is_secure(),
#'secure': django_request.is_secure(),
}
# TODO: check for valid astakos user
if django_request.user.is_authenticated():
params['user'] = django_request.user
return oa2base.Request(**params)
def get_url_patterns(self):
sep = '/' if self.endpoints_prefix else ''
_patterns = patterns(
'',
url(r'^%s/auth/?$' % self.endpoints_prefix, self.auth_view,
url(r'^%s%sauth/?$' % (self.endpoints_prefix, sep),
self.auth_view,