From 790c12a53c4ba3d58c7db7319356904afd1fdfaa Mon Sep 17 00:00:00 2001 From: juanifioren Date: Fri, 30 Jan 2015 17:20:36 -0300 Subject: [PATCH] Add custom scope claims feature. --- README.rst | 51 ++++++++- openid_provider/lib/claims.py | 121 ++++++++++++++++++++++ openid_provider/lib/endpoints/userinfo.py | 19 ++-- openid_provider/lib/scopes.py | 116 --------------------- openid_provider/settings.py | 11 +- openid_provider/views.py | 4 +- 6 files changed, 194 insertions(+), 128 deletions(-) create mode 100644 openid_provider/lib/claims.py delete mode 100644 openid_provider/lib/scopes.py diff --git a/README.rst b/README.rst index 5a80490..1474d00 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,9 @@ Important things that you should know: - Although OpenID was built on top of OAuth2, this isn't an OAuth2 server. Maybe in a future it will be. +- Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. - This cover ``authorization_code`` flow and ``implicit`` flow, NO support for ``hybrid`` flow at this moment. - Only support for requesting Claims using Scope Values. -- Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. ************ Installation @@ -67,6 +67,7 @@ Add required variables to your project settings. # OPTIONAL. DOP_CODE_EXPIRE = 60*10 # 10 min. + DOP_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims, DOP_IDTOKEN_EXPIRE = 60*10, # 10 min. DOP_TOKEN_EXPIRE = 60*60 # 1 hour. @@ -127,6 +128,54 @@ The ``code`` param will be use it to obtain access token. Host: localhost:8000 Authorization: Bearer [ACCESS_TOKEN] +*************** +Claims & Scopes +*************** + +OpenID Connect Clients will use scope values to specify what access privileges are being requested for Access Tokens. + +Here you have the standard scopes defined by the protocol. +http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims + +If you need to add extra scopes specific for your app you can add them using the ``DOP_EXTRA_SCOPE_CLAIMS`` settings variable. +This class MUST inherit ``AbstractScopeClaims``. + +Check out an example: + +.. code:: python + + from openid_provider.lib.claims import AbstractScopeClaims + + class MyAppScopeClaims(AbstractScopeClaims): + + def __init__(self, user, scopes): + # Don't forget this. + super(StandardScopeClaims, self).__init__(user, scopes) + + # Here you can load models that will be used + # in more than one scope for example. + try: + self.some_model = SomeModel.objects.get(user=self.user) + except UserInfo.DoesNotExist: + # Create an empty model object. + self.some_model = SomeModel() + + def scope_books(self, user): + + # Here you can search books for this user. + # Remember that you have "self.some_model" also. + + dic = { + 'books_readed': books_readed_count, + } + + return dic + +See how we create our own scopes using the convention ``def scope_(self, user):``. +If a field is empty or ``None`` will be cleaned from the response. + +**Don't forget to add your class into your app settings.** + ********* Templates ********* diff --git a/openid_provider/lib/claims.py b/openid_provider/lib/claims.py new file mode 100644 index 0000000..7b0b97e --- /dev/null +++ b/openid_provider/lib/claims.py @@ -0,0 +1,121 @@ +from django.utils.translation import ugettext as _ +from openid_provider.models import UserInfo + + +class AbstractScopeClaims(object): + + def __init__(self, user, scopes): + self.user = user + self.scopes = scopes + + def create_response_dic(self): + """ + Generate the dic that will be jsonify. Checking scopes given vs + registered. + + Returns a dic. + """ + dic = {} + + for scope in self.scopes: + + if scope in self._scopes_registered(): + dic.update(getattr(self, 'scope_' + scope)(self.user)) + + dic = self._clean_dic(dic) + + return dic + + def _scopes_registered(self): + """ + Return a list that contains all the scopes registered + in the class. + """ + scopes = [] + + for name in self.__class__.__dict__: + + if name.startswith('scope_'): + scope = name.split('scope_')[1] + scopes.append(scope) + + return scopes + + def _clean_dic(self, dic): + """ + Clean recursively all empty or None values inside a dict. + """ + aux_dic = dic.copy() + for key, value in dic.iteritems(): + + if not value: + del aux_dic[key] + elif type(value) is dict: + aux_dic[key] = clean_dic(value) + + return aux_dic + +class StandardScopeClaims(AbstractScopeClaims): + """ + Based on OpenID Standard Claims. + See: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + """ + + def __init__(self, user, scopes): + super(StandardScopeClaims, self).__init__(user, scopes) + + try: + self.userinfo = UserInfo.objects.get(user=self.user) + except UserInfo.DoesNotExist: + # Create an empty model object. + self.userinfo = UserInfo() + + def scope_profile(self, user): + dic = { + 'name': self.userinfo.name, + 'given_name': self.userinfo.given_name, + 'family_name': self.userinfo.family_name, + 'middle_name': self.userinfo.middle_name, + 'nickname': self.userinfo.nickname, + 'preferred_username': self.userinfo.preferred_username, + 'profile': self.userinfo.profile, + 'picture': self.userinfo.picture, + 'website': self.userinfo.website, + 'gender': self.userinfo.gender, + 'birthdate': self.userinfo.birthdate, + 'zoneinfo': self.userinfo.zoneinfo, + 'locale': self.userinfo.locale, + 'updated_at': self.userinfo.updated_at, + } + + return dic + + def scope_email(self, user): + dic = { + 'email': self.user.email, + 'email_verified': self.userinfo.email_verified, + } + + return dic + + def scope_phone(self, user): + dic = { + 'phone_number': self.userinfo.phone_number, + 'phone_number_verified': self.userinfo.phone_number_verified, + } + + return dic + + def scope_address(self, user): + dic = { + 'address': { + 'formatted': self.userinfo.address_formatted, + 'street_address': self.userinfo.address_street_address, + 'locality': self.userinfo.address_locality, + 'region': self.userinfo.address_region, + 'postal_code': self.userinfo.address_postal_code, + 'country': self.userinfo.address_country, + } + } + + return dic diff --git a/openid_provider/lib/endpoints/userinfo.py b/openid_provider/lib/endpoints/userinfo.py index a4b379e..41c4b22 100644 --- a/openid_provider/lib/endpoints/userinfo.py +++ b/openid_provider/lib/endpoints/userinfo.py @@ -1,11 +1,11 @@ -import re - -from django.http import HttpResponse, JsonResponse - +from django.http import HttpResponse +from django.http import JsonResponse from openid_provider.lib.errors import * -from openid_provider.lib.scopes import * +from openid_provider.lib.claims import * from openid_provider.lib.utils.params import * from openid_provider.models import * +from openid_provider import settings +import re class UserInfoEndpoint(object): @@ -57,10 +57,15 @@ class UserInfoEndpoint(object): 'sub': self.token.id_token.get('sub'), } - standard_claims = StandardClaims(self.token.user, self.token.scope) - + standard_claims = StandardScopeClaims(self.token.user, self.token.scope) + dic.update(standard_claims.create_response_dic()) + extra_claims = settings.get('DOP_EXTRA_SCOPE_CLAIMS')( + self.token.user, self.token.scope) + + dic.update(extra_claims.create_response_dic()) + return dic @classmethod diff --git a/openid_provider/lib/scopes.py b/openid_provider/lib/scopes.py deleted file mode 100644 index 530d0b7..0000000 --- a/openid_provider/lib/scopes.py +++ /dev/null @@ -1,116 +0,0 @@ -from django.utils.translation import ugettext as _ - -from openid_provider.models import UserInfo - - -# Standard Claims -# http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - -class StandardClaims(object): - - __model__ = UserInfo - - def __init__(self, user, scopes): - self.user = user - self.scopes = scopes - - try: - self.model = self.__model__.objects.get(user=self.user) - except self.__model__.DoesNotExist: - self.model = self.__model__() - - def create_response_dic(self): - - dic = {} - - for scope in self.scopes: - - if scope in self._scopes_registered(): - dic.update(getattr(self, 'scope_' + scope)) - - dic = self._clean_dic(dic) - - return dic - - def _scopes_registered(self): - """ - Return a list that contains all the scopes registered - in the class. - """ - scopes = [] - - for name in self.__class__.__dict__: - - if name.startswith('scope_'): - scope = name.split('scope_')[1] - scopes.append(scope) - - return scopes - - def _clean_dic(self, dic): - """ - Clean recursively all empty or None values inside a dict. - """ - aux_dic = dic.copy() - for key, value in dic.iteritems(): - - if not value: - del aux_dic[key] - elif type(value) is dict: - aux_dic[key] = clean_dic(value) - - return aux_dic - - @property - def scope_profile(self): - dic = { - 'name': self.model.name, - 'given_name': self.model.given_name, - 'family_name': self.model.family_name, - 'middle_name': self.model.middle_name, - 'nickname': self.model.nickname, - 'preferred_username': self.model.preferred_username, - 'profile': self.model.profile, - 'picture': self.model.picture, - 'website': self.model.website, - 'gender': self.model.gender, - 'birthdate': self.model.birthdate, - 'zoneinfo': self.model.zoneinfo, - 'locale': self.model.locale, - 'updated_at': self.model.updated_at, - } - - return dic - - @property - def scope_email(self): - dic = { - 'email': self.user.email, - 'email_verified': self.model.email_verified, - } - - return dic - - @property - def scope_phone(self): - dic = { - 'phone_number': self.model.phone_number, - 'phone_number_verified': self.model.phone_number_verified, - } - - return dic - - @property - def scope_address(self): - dic = { - 'address': { - 'formatted': self.model.address_formatted, - 'street_address': self.model.address_street_address, - 'locality': self.model.address_locality, - 'region': self.model.address_region, - 'postal_code': self.model.address_postal_code, - 'country': self.model.address_country, - } - } - - return dic diff --git a/openid_provider/settings.py b/openid_provider/settings.py index 6accd8c..7348596 100644 --- a/openid_provider/settings.py +++ b/openid_provider/settings.py @@ -1,13 +1,18 @@ from django.conf import settings +from openid_provider.lib.claims import AbstractScopeClaims # Here goes all the package default settings. default_settings = { - 'DOP_CODE_EXPIRE': 60*10, # 10 min. - 'DOP_IDTOKEN_EXPIRE': 60*10, # 10 min. - 'DOP_TOKEN_EXPIRE': 60*60, # 1 hour. + # Required. 'LOGIN_URL': None, 'SITE_URL': None, + + # Optional. + 'DOP_CODE_EXPIRE': 60*10, + 'DOP_EXTRA_SCOPE_CLAIMS': AbstractScopeClaims, + 'DOP_IDTOKEN_EXPIRE': 60*10, + 'DOP_TOKEN_EXPIRE': 60*60, } def get(name): diff --git a/openid_provider/views.py b/openid_provider/views.py index a80cde0..d4a7f9b 100644 --- a/openid_provider/views.py +++ b/openid_provider/views.py @@ -73,7 +73,9 @@ class AuthorizeView(View): return HttpResponseRedirect(uri) except (AuthorizeError) as error: - uri = error.create_uri(authorize.params.redirect_uri, authorize.params.state) + uri = error.create_uri( + authorize.params.redirect_uri, + authorize.params.state) return HttpResponseRedirect(uri)