Add custom scope claims feature.
This commit is contained in:
parent
0b6fca3a12
commit
790c12a53c
6 changed files with 194 additions and 128 deletions
51
README.rst
51
README.rst
|
@ -7,9 +7,9 @@
|
||||||
Important things that you should know:
|
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.
|
- 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.
|
- This cover ``authorization_code`` flow and ``implicit`` flow, NO support for ``hybrid`` flow at this moment.
|
||||||
- Only support for requesting Claims using Scope Values.
|
- 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
|
Installation
|
||||||
|
@ -67,6 +67,7 @@ Add required variables to your project settings.
|
||||||
# OPTIONAL.
|
# OPTIONAL.
|
||||||
|
|
||||||
DOP_CODE_EXPIRE = 60*10 # 10 min.
|
DOP_CODE_EXPIRE = 60*10 # 10 min.
|
||||||
|
DOP_EXTRA_SCOPE_CLAIMS = MyAppScopeClaims,
|
||||||
DOP_IDTOKEN_EXPIRE = 60*10, # 10 min.
|
DOP_IDTOKEN_EXPIRE = 60*10, # 10 min.
|
||||||
DOP_TOKEN_EXPIRE = 60*60 # 1 hour.
|
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
|
Host: localhost:8000
|
||||||
Authorization: Bearer [ACCESS_TOKEN]
|
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
|
Templates
|
||||||
*********
|
*********
|
||||||
|
|
121
openid_provider/lib/claims.py
Normal file
121
openid_provider/lib/claims.py
Normal 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
|
|
@ -1,11 +1,11 @@
|
||||||
import re
|
from django.http import HttpResponse
|
||||||
|
from django.http import JsonResponse
|
||||||
from django.http import HttpResponse, JsonResponse
|
|
||||||
|
|
||||||
from openid_provider.lib.errors import *
|
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.lib.utils.params import *
|
||||||
from openid_provider.models import *
|
from openid_provider.models import *
|
||||||
|
from openid_provider import settings
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class UserInfoEndpoint(object):
|
class UserInfoEndpoint(object):
|
||||||
|
@ -57,10 +57,15 @@ class UserInfoEndpoint(object):
|
||||||
'sub': self.token.id_token.get('sub'),
|
'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())
|
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
|
return dic
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -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
|
|
|
@ -1,13 +1,18 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from openid_provider.lib.claims import AbstractScopeClaims
|
||||||
|
|
||||||
|
|
||||||
# Here goes all the package default settings.
|
# Here goes all the package default settings.
|
||||||
default_settings = {
|
default_settings = {
|
||||||
'DOP_CODE_EXPIRE': 60*10, # 10 min.
|
# Required.
|
||||||
'DOP_IDTOKEN_EXPIRE': 60*10, # 10 min.
|
|
||||||
'DOP_TOKEN_EXPIRE': 60*60, # 1 hour.
|
|
||||||
'LOGIN_URL': None,
|
'LOGIN_URL': None,
|
||||||
'SITE_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):
|
def get(name):
|
||||||
|
|
|
@ -73,7 +73,9 @@ class AuthorizeView(View):
|
||||||
return HttpResponseRedirect(uri)
|
return HttpResponseRedirect(uri)
|
||||||
|
|
||||||
except (AuthorizeError) as error:
|
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)
|
return HttpResponseRedirect(uri)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue