diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e6360..ae386af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ 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. - Setting OIDC_IDTOKEN_PROCESSING_HOOK. ### [0.2.5] - 2016-02-03 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) 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/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/common.py b/oidc_provider/lib/utils/common.py index 85983e7..9d37f4f 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -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` :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` :rtype dict """ diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 0faa1eb..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, user) + dic = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK', import_str=True)(dic, user=user) return dic @@ -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 24e49c2..bb9e772 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 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: 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',