From 8eb0877d89e6da0c278b7b98a99e24864c138946 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Mon, 15 Feb 2016 17:13:19 -0300 Subject: [PATCH] Refactoring userinfo endpoint. Create decorator "oauth2.protected_resource_view". --- oidc_provider/lib/endpoints/userinfo.py | 97 ------------------------- oidc_provider/lib/errors.py | 16 ++-- oidc_provider/lib/utils/oauth2.py | 64 ++++++++++++++++ oidc_provider/views.py | 37 +++++++--- 4 files changed, 99 insertions(+), 115 deletions(-) delete mode 100644 oidc_provider/lib/endpoints/userinfo.py create mode 100644 oidc_provider/lib/utils/oauth2.py diff --git a/oidc_provider/lib/endpoints/userinfo.py b/oidc_provider/lib/endpoints/userinfo.py deleted file mode 100644 index eb91ca0..0000000 --- a/oidc_provider/lib/endpoints/userinfo.py +++ /dev/null @@ -1,97 +0,0 @@ -import logging -import re - -from django.http import HttpResponse -from django.http import JsonResponse - -from oidc_provider.lib.errors import * -from oidc_provider.lib.claims import * -from oidc_provider.lib.utils.params import * -from oidc_provider.models import * -from oidc_provider import settings - - -logger = logging.getLogger(__name__) - - -class UserInfoEndpoint(object): - - def __init__(self, request): - self.request = request - self.params = Params() - self._extract_params() - - def _extract_params(self): - # TODO: Maybe add other ways of passing access token - # http://tools.ietf.org/html/rfc6750#section-2 - self.params.access_token = self._get_access_token() - - def _get_access_token(self): - """ - Get the access token using Authorization Request Header Field method. - Or try getting via GET. - See: http://tools.ietf.org/html/rfc6750#section-2.1 - - Return a string. - """ - auth_header = self.request.META.get('HTTP_AUTHORIZATION', '') - - if re.compile('^Bearer\s{1}.+$').match(auth_header): - access_token = auth_header.split()[1] - else: - access_token = self.request.GET.get('access_token', '') - - return access_token - - def validate_params(self): - try: - self.token = Token.objects.get(access_token=self.params.access_token) - - if self.token.has_expired(): - logger.error('[UserInfo] Token has expired: %s', self.params.access_token) - raise UserInfoError('invalid_token') - - if not ('openid' in self.token.scope): - logger.error('[UserInfo] Missing openid scope.') - raise UserInfoError('insufficient_scope') - - except Token.DoesNotExist: - logger.error('[UserInfo] Token does not exist: %s', self.params.access_token) - raise UserInfoError('invalid_token') - - def create_response_dic(self): - """ - Create a diccionary with all the requested claims about the End-User. - See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse - - Return a diccionary. - """ - dic = { - 'sub': self.token.id_token.get('sub'), - } - - standard_claims = StandardScopeClaims(self.token.user, self.token.scope) - - dic.update(standard_claims.create_response_dic()) - - extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)( - self.token.user, self.token.scope) - - dic.update(extra_claims.create_response_dic()) - - return dic - - @classmethod - def response(cls, dic): - response = JsonResponse(dic, status=200) - response['Cache-Control'] = 'no-store' - response['Pragma'] = 'no-cache' - - return response - - @classmethod - def error_response(cls, code, description, status): - response = HttpResponse(status=status) - response['WWW-Authenticate'] = 'error="{0}", error_description="{1}"'.format(code, description) - - return response diff --git a/oidc_provider/lib/errors.py b/oidc_provider/lib/errors.py index b541b48..ce84811 100644 --- a/oidc_provider/lib/errors.py +++ b/oidc_provider/lib/errors.py @@ -99,10 +99,12 @@ class AuthorizeError(Exception): class TokenError(Exception): + """ + OAuth2 token endpoint errors. + https://tools.ietf.org/html/rfc6749#section-5.2 + """ _errors = { - # Oauth2 errors. - # https://tools.ietf.org/html/rfc6749#section-5.2 'invalid_request': 'The request is otherwise malformed', 'invalid_client': 'Client authentication failed (e.g., unknown client, ' @@ -137,10 +139,13 @@ class TokenError(Exception): return dic -class UserInfoError(Exception): +class BearerTokenError(Exception): + """ + OAuth2 errors. + https://tools.ietf.org/html/rfc6750#section-3.1 + """ + _errors = { - # Oauth2 errors. - # https://tools.ietf.org/html/rfc6750#section-3.1 'invalid_request': ( 'The request is otherwise malformed', 400 ), @@ -155,7 +160,6 @@ class UserInfoError(Exception): } def __init__(self, code): - self.code = code error_tuple = self._errors.get(code, ('', '')) self.description = error_tuple[0] diff --git a/oidc_provider/lib/utils/oauth2.py b/oidc_provider/lib/utils/oauth2.py new file mode 100644 index 0000000..9d54e75 --- /dev/null +++ b/oidc_provider/lib/utils/oauth2.py @@ -0,0 +1,64 @@ +import logging +import re + +from django.http import HttpResponse + +from oidc_provider.lib.errors import BearerTokenError +from oidc_provider.models import Token + + +logger = logging.getLogger(__name__) + + +def extract_access_token(request): + """ + Get the access token using Authorization Request Header Field method. + Or try getting via GET. + See: http://tools.ietf.org/html/rfc6750#section-2.1 + + Return a string. + """ + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + + if re.compile('^Bearer\s{1}.+$').match(auth_header): + access_token = auth_header.split()[1] + else: + access_token = request.GET.get('access_token', '') + + return access_token + + +def protected_resource_view(scopes=[]): + """ + View decorator. The client accesses protected resources by presenting the + access token to the resource server. + https://tools.ietf.org/html/rfc6749#section-7 + """ + def wrapper(view): + def view_wrapper(request, *args, **kwargs): + access_token = extract_access_token(request) + + try: + try: + kwargs['token'] = Token.objects.get(access_token=access_token) + except Token.DoesNotExist: + logger.error('[UserInfo] Token does not exist: %s', access_token) + raise BearerTokenError('invalid_token') + + if kwargs['token'].has_expired(): + logger.error('[UserInfo] Token has expired: %s', access_token) + raise BearerTokenError('invalid_token') + + if not set(scopes).issubset(set(kwargs['token'].scope)): + logger.error('[UserInfo] Missing openid scope.') + raise BearerTokenError('insufficient_scope') + except (BearerTokenError) as error: + response = HttpResponse(status=error.status) + response['WWW-Authenticate'] = 'error="{0}", error_description="{1}"'.format(error.code, error.description) + return response + + return view(request, *args, **kwargs) + + return view_wrapper + + return wrapper diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 6d6fa17..ebdb7d0 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -15,7 +15,9 @@ from oidc_provider.lib.endpoints.token import * from oidc_provider.lib.endpoints.userinfo import * from oidc_provider.lib.errors import * from oidc_provider.lib.utils.common import redirect, get_issuer +from oidc_provider.lib.utils.oauth2 import protected_resource_view from oidc_provider.models import Client, RSAKey +from oidc_provider import settings logger = logging.getLogger(__name__) @@ -130,22 +132,33 @@ class TokenView(View): @require_http_methods(['GET', 'POST']) -def userinfo(request): +@protected_resource_view(['openid']) +def userinfo(request, *args, **kwargs): + """ + Create a diccionary with all the requested claims about the End-User. + See: http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse - userinfo = UserInfoEndpoint(request) - - try: - userinfo.validate_params() + Return a diccionary. + """ + token = kwargs['token'] - dic = userinfo.create_response_dic() + dic = { + 'sub': token.id_token.get('sub'), + } - return UserInfoEndpoint.response(dic) + standard_claims = StandardScopeClaims(token.user, token.scope) - except (UserInfoError) as error: - return UserInfoEndpoint.error_response( - error.code, - error.description, - error.status) + dic.update(standard_claims.create_response_dic()) + + extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)( + token.user, token.scope) + + dic.update(extra_claims.create_response_dic()) + + response = JsonResponse(dic, status=200) + response['Cache-Control'] = 'no-store' + response['Pragma'] = 'no-cache' + return response class ProviderInfoView(View):