Merge pull request #3 from juanifioren/v0.3.x

merge v0.3.x branch
This commit is contained in:
Wojciech Bartosiak 2016-03-01 17:08:04 +00:00
commit 80512c5528
13 changed files with 118 additions and 54 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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),
),
]

View file

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

View file

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

View file

@ -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,
@ -117,7 +118,7 @@ class AuthorizeView(View):
class TokenView(View): class TokenView(View):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
token = TokenEndpoint(request) token = TokenEndpoint(request)
try: try:

View file

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