Refactoring userinfo endpoint. Create decorator "oauth2.protected_resource_view".
This commit is contained in:
parent
58482a6585
commit
8eb0877d89
|
@ -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
|
|
|
@ -99,10 +99,12 @@ class AuthorizeError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class TokenError(Exception):
|
class TokenError(Exception):
|
||||||
|
"""
|
||||||
|
OAuth2 token endpoint errors.
|
||||||
|
https://tools.ietf.org/html/rfc6749#section-5.2
|
||||||
|
"""
|
||||||
|
|
||||||
_errors = {
|
_errors = {
|
||||||
# Oauth2 errors.
|
|
||||||
# https://tools.ietf.org/html/rfc6749#section-5.2
|
|
||||||
'invalid_request': 'The request is otherwise malformed',
|
'invalid_request': 'The request is otherwise malformed',
|
||||||
|
|
||||||
'invalid_client': 'Client authentication failed (e.g., unknown client, '
|
'invalid_client': 'Client authentication failed (e.g., unknown client, '
|
||||||
|
@ -137,10 +139,13 @@ class TokenError(Exception):
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
|
|
||||||
class UserInfoError(Exception):
|
class BearerTokenError(Exception):
|
||||||
|
"""
|
||||||
|
OAuth2 errors.
|
||||||
|
https://tools.ietf.org/html/rfc6750#section-3.1
|
||||||
|
"""
|
||||||
|
|
||||||
_errors = {
|
_errors = {
|
||||||
# Oauth2 errors.
|
|
||||||
# https://tools.ietf.org/html/rfc6750#section-3.1
|
|
||||||
'invalid_request': (
|
'invalid_request': (
|
||||||
'The request is otherwise malformed', 400
|
'The request is otherwise malformed', 400
|
||||||
),
|
),
|
||||||
|
@ -155,7 +160,6 @@ class UserInfoError(Exception):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, code):
|
def __init__(self, code):
|
||||||
|
|
||||||
self.code = code
|
self.code = code
|
||||||
error_tuple = self._errors.get(code, ('', ''))
|
error_tuple = self._errors.get(code, ('', ''))
|
||||||
self.description = error_tuple[0]
|
self.description = error_tuple[0]
|
||||||
|
|
64
oidc_provider/lib/utils/oauth2.py
Normal file
64
oidc_provider/lib/utils/oauth2.py
Normal file
|
@ -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
|
|
@ -15,7 +15,9 @@ from oidc_provider.lib.endpoints.token import *
|
||||||
from oidc_provider.lib.endpoints.userinfo import *
|
from oidc_provider.lib.endpoints.userinfo import *
|
||||||
from oidc_provider.lib.errors import *
|
from oidc_provider.lib.errors import *
|
||||||
from oidc_provider.lib.utils.common import redirect, get_issuer
|
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.models import Client, RSAKey
|
||||||
|
from oidc_provider import settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -130,22 +132,33 @@ class TokenView(View):
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(['GET', 'POST'])
|
@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)
|
Return a diccionary.
|
||||||
|
"""
|
||||||
try:
|
token = kwargs['token']
|
||||||
userinfo.validate_params()
|
|
||||||
|
|
||||||
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:
|
dic.update(standard_claims.create_response_dic())
|
||||||
return UserInfoEndpoint.error_response(
|
|
||||||
error.code,
|
extra_claims = settings.get('OIDC_EXTRA_SCOPE_CLAIMS', import_str=True)(
|
||||||
error.description,
|
token.user, token.scope)
|
||||||
error.status)
|
|
||||||
|
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):
|
class ProviderInfoView(View):
|
||||||
|
|
Loading…
Reference in a new issue