commit
80512c5528
|
@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### [Unreleased]
|
### [Unreleased]
|
||||||
|
|
||||||
|
### [0.3.0] - 2016-02-23
|
||||||
|
|
||||||
##### Added
|
##### Added
|
||||||
|
- Support OAuth2 requests.
|
||||||
|
- Decorator for protecting views with OAuth2.
|
||||||
- Setting OIDC_IDTOKEN_PROCESSING_HOOK.
|
- Setting OIDC_IDTOKEN_PROCESSING_HOOK.
|
||||||
|
|
||||||
### [0.2.5] - 2016-02-03
|
### [0.2.5] - 2016-02-03
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
Welcome to Django OIDC Provider Documentation!
|
Welcome to Django OIDC Provider Documentation!
|
||||||
==============================================
|
==============================================
|
||||||
|
|
||||||
Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects.
|
Django OIDC Provider can help you providing out of the box all the endpoints, data and logic needed to add OpenID Connect capabilities to your Django projects. And as a side effect a fair implementation of OAuth2.0 too.
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
Before getting started there are some important things that you should know:
|
Before getting started there are some 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.
|
* 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.
|
||||||
|
@ -24,6 +23,7 @@ Contents:
|
||||||
sections/serverkeys
|
sections/serverkeys
|
||||||
sections/templates
|
sections/templates
|
||||||
sections/claims
|
sections/claims
|
||||||
|
sections/oauth2
|
||||||
sections/settings
|
sections/settings
|
||||||
sections/contribute
|
sections/contribute
|
||||||
..
|
..
|
||||||
|
|
27
docs/sections/oauth2.rst
Normal file
27
docs/sections/oauth2.rst
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.. _oauth2:
|
||||||
|
|
||||||
|
OAuth2 Server
|
||||||
|
#############
|
||||||
|
|
||||||
|
Because OIDC is a layer on top of the OAuth 2.0 protocol, this package gives you a simple but effective OAuth2 server that you can use not only for logging in your users on multiple platforms, also to protect some resources you want to expose.
|
||||||
|
|
||||||
|
Protecting Views
|
||||||
|
================
|
||||||
|
|
||||||
|
Here we are going to protect a view with a scope called ``testscope``::
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
|
from oidc_provider.lib.utils.oauth2 import protected_resource_view
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(['GET'])
|
||||||
|
@protected_resource_view(['testscope'])
|
||||||
|
def protected_api(request, *args, **kwargs):
|
||||||
|
|
||||||
|
dic = {
|
||||||
|
'protected': 'information',
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonResponse(dic, status=200)
|
|
@ -1,2 +1,2 @@
|
||||||
django==1.9
|
django==1.9
|
||||||
https://github.com/juanifioren/django-oidc-provider/archive/v0.2.x.zip
|
https://github.com/juanifioren/django-oidc-provider/archive/v0.3.x.zip
|
||||||
|
|
|
@ -29,11 +29,14 @@ class AuthorizeEndpoint(object):
|
||||||
# Determine which flow to use.
|
# Determine which flow to use.
|
||||||
if self.params.response_type in ['code']:
|
if self.params.response_type in ['code']:
|
||||||
self.grant_type = 'authorization_code'
|
self.grant_type = 'authorization_code'
|
||||||
elif self.params.response_type in ['id_token', 'id_token token']:
|
elif self.params.response_type in ['id_token', 'id_token token', 'token']:
|
||||||
self.grant_type = 'implicit'
|
self.grant_type = 'implicit'
|
||||||
else:
|
else:
|
||||||
self.grant_type = None
|
self.grant_type = None
|
||||||
|
|
||||||
|
# Determine if it's an OpenID Authentication request (or OAuth2).
|
||||||
|
self.is_authentication = 'openid' in self.params.scope
|
||||||
|
|
||||||
def _extract_params(self):
|
def _extract_params(self):
|
||||||
"""
|
"""
|
||||||
Get all the params used by the Authorization Code Flow
|
Get all the params used by the Authorization Code Flow
|
||||||
|
@ -54,36 +57,31 @@ class AuthorizeEndpoint(object):
|
||||||
self.params.nonce = query_dict.get('nonce', '')
|
self.params.nonce = query_dict.get('nonce', '')
|
||||||
|
|
||||||
def validate_params(self):
|
def validate_params(self):
|
||||||
if not self.params.redirect_uri:
|
|
||||||
logger.error('[Authorize] Missing redirect uri.')
|
|
||||||
raise RedirectUriError()
|
|
||||||
|
|
||||||
if not ('openid' in self.params.scope):
|
|
||||||
logger.error('[Authorize] Missing openid scope.')
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_scope',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
# http://openid.net/specs/openid-connect-implicit-1_0.html#RequestParameters
|
|
||||||
if self.grant_type == 'implicit' and not self.params.nonce:
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.client = Client.objects.get(client_id=self.params.client_id)
|
self.client = Client.objects.get(client_id=self.params.client_id)
|
||||||
except Client.DoesNotExist:
|
except Client.DoesNotExist:
|
||||||
logger.error('[Authorize] Invalid client identifier: %s', self.params.client_id)
|
logger.error('[Authorize] Invalid client identifier: %s', self.params.client_id)
|
||||||
raise ClientIdError()
|
raise ClientIdError()
|
||||||
|
|
||||||
|
if self.is_authentication and not self.params.redirect_uri:
|
||||||
|
logger.error('[Authorize] Missing redirect uri.')
|
||||||
|
raise RedirectUriError()
|
||||||
|
|
||||||
|
if not self.grant_type:
|
||||||
|
logger.error('[Authorize] Invalid response type: %s', self.params.response_type)
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
|
if self.is_authentication and self.grant_type == 'implicit' and not self.params.nonce:
|
||||||
|
raise AuthorizeError(self.params.redirect_uri, 'invalid_request',
|
||||||
|
self.grant_type)
|
||||||
|
|
||||||
clean_redirect_uri = urlsplit(self.params.redirect_uri)
|
clean_redirect_uri = urlsplit(self.params.redirect_uri)
|
||||||
clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query=''))
|
clean_redirect_uri = urlunsplit(clean_redirect_uri._replace(query=''))
|
||||||
if not (clean_redirect_uri in self.client.redirect_uris):
|
if not (clean_redirect_uri in self.client.redirect_uris):
|
||||||
logger.error('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri)
|
logger.error('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri)
|
||||||
raise RedirectUriError()
|
raise RedirectUriError()
|
||||||
|
|
||||||
if not self.grant_type or not (self.params.response_type == self.client.response_type):
|
|
||||||
logger.error('[Authorize] Invalid response type: %s', self.params.response_type)
|
|
||||||
raise AuthorizeError(self.params.redirect_uri, 'unsupported_response_type',
|
|
||||||
self.grant_type)
|
|
||||||
|
|
||||||
def create_response_uri(self):
|
def create_response_uri(self):
|
||||||
uri = urlsplit(self.params.redirect_uri)
|
uri = urlsplit(self.params.redirect_uri)
|
||||||
|
@ -96,7 +94,8 @@ class AuthorizeEndpoint(object):
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
client=self.client,
|
client=self.client,
|
||||||
scope=self.params.scope,
|
scope=self.params.scope,
|
||||||
nonce=self.params.nonce)
|
nonce=self.params.nonce,
|
||||||
|
is_authentication=self.is_authentication)
|
||||||
|
|
||||||
code.save()
|
code.save()
|
||||||
|
|
||||||
|
@ -104,10 +103,15 @@ class AuthorizeEndpoint(object):
|
||||||
query_params['state'] = self.params.state if self.params.state else ''
|
query_params['state'] = self.params.state if self.params.state else ''
|
||||||
|
|
||||||
elif self.grant_type == 'implicit':
|
elif self.grant_type == 'implicit':
|
||||||
id_token_dic = create_id_token(
|
# We don't need id_token if it's an OAuth2 request.
|
||||||
user=self.request.user,
|
if self.is_authentication:
|
||||||
aud=self.client.client_id,
|
id_token_dic = create_id_token(
|
||||||
nonce=self.params.nonce)
|
user=self.request.user,
|
||||||
|
aud=self.client.client_id,
|
||||||
|
nonce=self.params.nonce)
|
||||||
|
query_fragment['id_token'] = encode_id_token(id_token_dic)
|
||||||
|
else:
|
||||||
|
id_token_dic = {}
|
||||||
|
|
||||||
token = create_token(
|
token = create_token(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
@ -119,12 +123,12 @@ class AuthorizeEndpoint(object):
|
||||||
token.save()
|
token.save()
|
||||||
|
|
||||||
query_fragment['token_type'] = 'bearer'
|
query_fragment['token_type'] = 'bearer'
|
||||||
query_fragment['id_token'] = encode_id_token(id_token_dic)
|
# TODO: Create setting 'OIDC_TOKEN_EXPIRE'.
|
||||||
query_fragment['expires_in'] = 60 * 10
|
query_fragment['expires_in'] = 60 * 10
|
||||||
|
|
||||||
# Check if response_type is 'id_token token' then
|
# Check if response_type is an OpenID request with value 'id_token token'
|
||||||
# add access_token to the fragment.
|
# or it's an OAuth2 Implicit Flow request.
|
||||||
if self.params.response_type == 'id_token token':
|
if self.params.response_type in ['id_token token', 'token']:
|
||||||
query_fragment['access_token'] = token.access_token
|
query_fragment['access_token'] = token.access_token
|
||||||
|
|
||||||
query_fragment['state'] = self.params.state if self.params.state else ''
|
query_fragment['state'] = self.params.state if self.params.state else ''
|
||||||
|
|
|
@ -64,7 +64,6 @@ class TokenEndpoint(object):
|
||||||
def validate_params(self):
|
def validate_params(self):
|
||||||
try:
|
try:
|
||||||
self.client = Client.objects.get(client_id=self.params.client_id)
|
self.client = Client.objects.get(client_id=self.params.client_id)
|
||||||
|
|
||||||
except Client.DoesNotExist:
|
except Client.DoesNotExist:
|
||||||
logger.error('[Token] Client does not exist: %s', self.params.client_id)
|
logger.error('[Token] Client does not exist: %s', self.params.client_id)
|
||||||
raise TokenError('invalid_client')
|
raise TokenError('invalid_client')
|
||||||
|
@ -81,7 +80,6 @@ class TokenEndpoint(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.code = Code.objects.get(code=self.params.code)
|
self.code = Code.objects.get(code=self.params.code)
|
||||||
|
|
||||||
except Code.DoesNotExist:
|
except Code.DoesNotExist:
|
||||||
logger.error('[Token] Code does not exist: %s', self.params.code)
|
logger.error('[Token] Code does not exist: %s', self.params.code)
|
||||||
raise TokenError('invalid_grant')
|
raise TokenError('invalid_grant')
|
||||||
|
@ -114,16 +112,16 @@ class TokenEndpoint(object):
|
||||||
return self.create_code_response_dic()
|
return self.create_code_response_dic()
|
||||||
elif self.params.grant_type == 'refresh_token':
|
elif self.params.grant_type == 'refresh_token':
|
||||||
return self.create_refresh_response_dic()
|
return self.create_refresh_response_dic()
|
||||||
else:
|
|
||||||
# Should have already been catched by validate_params
|
|
||||||
raise RuntimeError('Invalid grant type')
|
|
||||||
|
|
||||||
def create_code_response_dic(self):
|
def create_code_response_dic(self):
|
||||||
id_token_dic = create_id_token(
|
if self.code.is_authentication:
|
||||||
user=self.code.user,
|
id_token_dic = create_id_token(
|
||||||
aud=self.client.client_id,
|
user=self.code.user,
|
||||||
nonce=self.code.nonce,
|
aud=self.client.client_id,
|
||||||
)
|
nonce=self.code.nonce,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
id_token_dic = {}
|
||||||
|
|
||||||
token = create_token(
|
token = create_token(
|
||||||
user=self.code.user,
|
user=self.code.user,
|
||||||
|
@ -148,11 +146,15 @@ class TokenEndpoint(object):
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
def create_refresh_response_dic(self):
|
def create_refresh_response_dic(self):
|
||||||
id_token_dic = create_id_token(
|
# If the Token has an id_token it's an Authentication request.
|
||||||
user=self.token.user,
|
if self.token.id_token:
|
||||||
aud=self.client.client_id,
|
id_token_dic = create_id_token(
|
||||||
nonce=None,
|
user=self.token.user,
|
||||||
)
|
aud=self.client.client_id,
|
||||||
|
nonce=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
id_token_dic = {}
|
||||||
|
|
||||||
token = create_token(
|
token = create_token(
|
||||||
user=self.token.user,
|
user=self.token.user,
|
||||||
|
|
|
@ -55,7 +55,10 @@ def default_idtoken_processing_hook(id_token, user):
|
||||||
|
|
||||||
:param id_token: dictionary contains values that going to be serialized into `id_token`
|
:param id_token: dictionary contains values that going to be serialized into `id_token`
|
||||||
:type id_token: dict
|
:type id_token: dict
|
||||||
:param user: user instance
|
|
||||||
|
:param user: user for whom id_token is generated
|
||||||
|
:type user: User
|
||||||
|
|
||||||
:return: custom modified dictionary of values for `id_token`
|
:return: custom modified dictionary of values for `id_token`
|
||||||
:rtype dict
|
:rtype dict
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -44,7 +44,7 @@ def create_id_token(user, aud, nonce):
|
||||||
if nonce:
|
if nonce:
|
||||||
dic['nonce'] = str(nonce)
|
dic['nonce'] = str(nonce)
|
||||||
|
|
||||||
dic = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK', import_str=True)(dic, user)
|
dic = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK', import_str=True)(dic, user=user)
|
||||||
|
|
||||||
return dic
|
return dic
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ def create_token(user, client, id_token_dic, scope):
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def create_code(user, client, scope, nonce):
|
def create_code(user, client, scope, nonce, is_authentication):
|
||||||
"""
|
"""
|
||||||
Create and populate a Code object.
|
Create and populate a Code object.
|
||||||
|
|
||||||
|
@ -103,5 +103,6 @@ def create_code(user, client, scope, nonce):
|
||||||
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
seconds=settings.get('OIDC_CODE_EXPIRE'))
|
||||||
code.scope = scope
|
code.scope = scope
|
||||||
code.nonce = nonce
|
code.nonce = nonce
|
||||||
|
code.is_authentication = is_authentication
|
||||||
|
|
||||||
return code
|
return code
|
||||||
|
|
20
oidc_provider/migrations/0010_code_is_authentication.py
Normal file
20
oidc_provider/migrations/0010_code_is_authentication.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9 on 2016-02-16 20:32
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('oidc_provider', '0009_auto_20160202_1945'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='code',
|
||||||
|
name='is_authentication',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -80,6 +80,7 @@ class Code(BaseCodeTokenModel):
|
||||||
|
|
||||||
code = models.CharField(max_length=255, unique=True)
|
code = models.CharField(max_length=255, unique=True)
|
||||||
nonce = models.CharField(max_length=255, blank=True, default='')
|
nonce = models.CharField(max_length=255, blank=True, default='')
|
||||||
|
is_authentication = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _(u'Authorization Code')
|
verbose_name = _(u'Authorization Code')
|
||||||
|
|
|
@ -81,7 +81,8 @@ class TokenTestCase(TestCase):
|
||||||
user=self.user,
|
user=self.user,
|
||||||
client=self.client,
|
client=self.client,
|
||||||
scope=['openid', 'email'],
|
scope=['openid', 'email'],
|
||||||
nonce=FAKE_NONCE)
|
nonce=FAKE_NONCE,
|
||||||
|
is_authentication=True)
|
||||||
code.save()
|
code.save()
|
||||||
|
|
||||||
return code
|
return code
|
||||||
|
|
|
@ -57,7 +57,8 @@ class AuthorizeView(View):
|
||||||
|
|
||||||
# Remove `openid` from scope list
|
# Remove `openid` from scope list
|
||||||
# since we don't need to print it.
|
# since we don't need to print it.
|
||||||
authorize.params.scope.remove('openid')
|
if 'openid' in authorize.params.scope:
|
||||||
|
authorize.params.scope.remove('openid')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'client': authorize.client,
|
'client': authorize.client,
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -7,7 +7,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-oidc-provider',
|
name='django-oidc-provider',
|
||||||
version='0.2.5',
|
version='0.3.0',
|
||||||
packages=[
|
packages=[
|
||||||
'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints',
|
'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints',
|
||||||
'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app',
|
'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app',
|
||||||
|
|
Loading…
Reference in a new issue