Add custom scope claims feature.

This commit is contained in:
juanifioren 2015-01-30 17:20:36 -03:00
parent 0b6fca3a12
commit 790c12a53c
6 changed files with 194 additions and 128 deletions

View file

@ -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_<SCOPE_NAME>(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
*********

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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)