From 25a59c834414bd5bc3efd2fb6ea9d2d62086d8a0 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 16 Feb 2016 17:33:12 -0300 Subject: [PATCH 1/6] Refactoring supporting OAuth2 flow. --- oidc_provider/lib/endpoints/authorize.py | 62 ++++++++++--------- oidc_provider/lib/endpoints/token.py | 32 +++++----- oidc_provider/lib/utils/token.py | 3 +- .../migrations/0010_code_is_authentication.py | 20 ++++++ oidc_provider/models.py | 1 + oidc_provider/tests/test_token_endpoint.py | 3 +- 6 files changed, 75 insertions(+), 46 deletions(-) create mode 100644 oidc_provider/migrations/0010_code_is_authentication.py diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 157748d..e75dfe8 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -29,11 +29,14 @@ class AuthorizeEndpoint(object): # Determine which flow to use. if self.params.response_type in ['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' else: 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): """ Get all the params used by the Authorization Code Flow @@ -54,36 +57,31 @@ class AuthorizeEndpoint(object): self.params.nonce = query_dict.get('nonce', '') 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: self.client = Client.objects.get(client_id=self.params.client_id) except Client.DoesNotExist: logger.error('[Authorize] Invalid client identifier: %s', self.params.client_id) 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 = urlunsplit(clean_redirect_uri._replace(query='')) if not (clean_redirect_uri in self.client.redirect_uris): logger.error('[Authorize] Invalid redirect uri: %s', self.params.redirect_uri) 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): uri = urlsplit(self.params.redirect_uri) @@ -96,7 +94,8 @@ class AuthorizeEndpoint(object): user=self.request.user, client=self.client, scope=self.params.scope, - nonce=self.params.nonce) + nonce=self.params.nonce, + is_authentication=self.is_authentication) code.save() @@ -104,10 +103,15 @@ class AuthorizeEndpoint(object): query_params['state'] = self.params.state if self.params.state else '' elif self.grant_type == 'implicit': - id_token_dic = create_id_token( - user=self.request.user, - aud=self.client.client_id, - nonce=self.params.nonce) + # We don't need id_token if it's an OAuth2 request. + if self.is_authentication: + id_token_dic = create_id_token( + 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( user=self.request.user, @@ -119,12 +123,12 @@ class AuthorizeEndpoint(object): token.save() 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 - # Check if response_type is 'id_token token' then - # add access_token to the fragment. - if self.params.response_type == 'id_token token': + # Check if response_type is an OpenID request with value 'id_token token' + # or it's an OAuth2 Implicit Flow request. + if self.params.response_type in ['id_token token', 'token']: query_fragment['access_token'] = token.access_token query_fragment['state'] = self.params.state if self.params.state else '' diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 15a92e4..a134f4f 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -64,7 +64,6 @@ class TokenEndpoint(object): def validate_params(self): try: self.client = Client.objects.get(client_id=self.params.client_id) - except Client.DoesNotExist: logger.error('[Token] Client does not exist: %s', self.params.client_id) raise TokenError('invalid_client') @@ -81,7 +80,6 @@ class TokenEndpoint(object): try: self.code = Code.objects.get(code=self.params.code) - except Code.DoesNotExist: logger.error('[Token] Code does not exist: %s', self.params.code) raise TokenError('invalid_grant') @@ -114,16 +112,16 @@ class TokenEndpoint(object): return self.create_code_response_dic() elif self.params.grant_type == 'refresh_token': 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): - id_token_dic = create_id_token( - user=self.code.user, - aud=self.client.client_id, - nonce=self.code.nonce, - ) + if self.code.is_authentication: + id_token_dic = create_id_token( + user=self.code.user, + aud=self.client.client_id, + nonce=self.code.nonce, + ) + else: + id_token_dic = {} token = create_token( user=self.code.user, @@ -148,11 +146,15 @@ class TokenEndpoint(object): return dic def create_refresh_response_dic(self): - id_token_dic = create_id_token( - user=self.token.user, - aud=self.client.client_id, - nonce=None, - ) + # If the Token has an id_token it's an Authentication request. + if self.token.id_token: + id_token_dic = create_id_token( + user=self.token.user, + aud=self.client.client_id, + nonce=None, + ) + else: + id_token_dic = {} token = create_token( user=self.token.user, diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 67bfb91..85e243c 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -89,7 +89,7 @@ def create_token(user, client, id_token_dic, scope): return token -def create_code(user, client, scope, nonce): +def create_code(user, client, scope, nonce, is_authentication): """ Create and populate a Code object. @@ -103,5 +103,6 @@ def create_code(user, client, scope, nonce): seconds=settings.get('OIDC_CODE_EXPIRE')) code.scope = scope code.nonce = nonce + code.is_authentication = is_authentication return code diff --git a/oidc_provider/migrations/0010_code_is_authentication.py b/oidc_provider/migrations/0010_code_is_authentication.py new file mode 100644 index 0000000..0ecf862 --- /dev/null +++ b/oidc_provider/migrations/0010_code_is_authentication.py @@ -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), + ), + ] diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 5cdd9ab..68ace73 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -80,6 +80,7 @@ class Code(BaseCodeTokenModel): code = models.CharField(max_length=255, unique=True) nonce = models.CharField(max_length=255, blank=True, default='') + is_authentication = models.BooleanField(default=False) class Meta: verbose_name = _(u'Authorization Code') diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 0cbc7c3..e67b3ba 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -81,7 +81,8 @@ class TokenTestCase(TestCase): user=self.user, client=self.client, scope=['openid', 'email'], - nonce=FAKE_NONCE) + nonce=FAKE_NONCE, + is_authentication=True) code.save() return code From fb4e9bd8fe53c90040c9fe15a26454cc19e05763 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 18 Feb 2016 16:03:46 -0300 Subject: [PATCH 2/6] Fix openid scope in authorize view. --- oidc_provider/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index f429ecd..01f5d1b 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -57,7 +57,8 @@ class AuthorizeView(View): # Remove `openid` from scope list # 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 = { 'client': authorize.client, @@ -117,7 +118,7 @@ class AuthorizeView(View): class TokenView(View): def post(self, request, *args, **kwargs): - + token = TokenEndpoint(request) try: From 67c06fad9b605f768a914a8707ceb8c53ad24e6d Mon Sep 17 00:00:00 2001 From: juanifioren Date: Thu, 18 Feb 2016 16:24:31 -0300 Subject: [PATCH 3/6] Add OAuth2 documentation. --- docs/index.rst | 4 ++-- docs/sections/oauth2.rst | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 docs/sections/oauth2.rst diff --git a/docs/index.rst b/docs/index.rst index f122aaf..466355f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,12 @@ 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: -* 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. @@ -24,6 +23,7 @@ Contents: sections/serverkeys sections/templates sections/claims + sections/oauth2 sections/settings sections/contribute .. diff --git a/docs/sections/oauth2.rst b/docs/sections/oauth2.rst new file mode 100644 index 0000000..bd8f545 --- /dev/null +++ b/docs/sections/oauth2.rst @@ -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) From 0faf7273515bf247c9f8806e3548fa796ad3ca2b Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Thu, 18 Feb 2016 16:52:30 -0300 Subject: [PATCH 4/6] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e6360..3667efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased] ##### Added +- Support OAuth2 requests. +- Decorator for protecting views with OAuth2. - Setting OIDC_IDTOKEN_PROCESSING_HOOK. ### [0.2.5] - 2016-02-03 From e3ccc2a8f69dfd7c6a2cc67fabde2bcfea176dd4 Mon Sep 17 00:00:00 2001 From: juanifioren Date: Tue, 23 Feb 2016 15:31:07 -0300 Subject: [PATCH 5/6] Bump version v0.3.0. --- CHANGELOG.md | 2 ++ example_project/requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3667efa..ae386af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +### [0.3.0] - 2016-02-23 + ##### Added - Support OAuth2 requests. - Decorator for protecting views with OAuth2. diff --git a/example_project/requirements.txt b/example_project/requirements.txt index 3467158..7f5a1f8 100644 --- a/example_project/requirements.txt +++ b/example_project/requirements.txt @@ -1,2 +1,2 @@ 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 diff --git a/setup.py b/setup.py index 5500690..66813ef 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-oidc-provider', - version='0.2.5', + version='0.3.0', packages=[ 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', 'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app', From 49e19e749331a6bbcbf19c5ad69ce8ccee458d4a Mon Sep 17 00:00:00 2001 From: Ilya Date: Thu, 25 Feb 2016 09:46:10 +0000 Subject: [PATCH 6/6] ID_TOKEN_PROCESSING_HOOK gets user argument --- oidc_provider/lib/utils/common.py | 4 +++- oidc_provider/lib/utils/token.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index c97c7e8..8eb0869 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -49,12 +49,14 @@ def default_after_userlogin_hook(request, user, client): """ return None -def default_idtoken_processing_hook(id_token): +def default_idtoken_processing_hook(id_token, user): """ Hook to perform some additional actions ti `id_token` dictionary just before serialization. :param id_token: dictionary contains values that going to be serialized into `id_token` :type id_token: dict + :param user: user for whom id_token is generated + :type user: User :return: custom modified dictionary of values for `id_token` :rtype dict """ diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 85e243c..4708c28 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -44,7 +44,7 @@ def create_id_token(user, aud, nonce): if nonce: dic['nonce'] = str(nonce) - dic = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK', import_str=True)(dic) + dic = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK', import_str=True)(dic, user=user) return dic