From ed194869183bb927b09b38edb79de8bea88ff5df Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Mon, 1 Aug 2016 19:17:20 -0700 Subject: [PATCH 01/29] Fix django dep range for 1.9 compat testing --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2b107d0..b6ed0b2 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ envlist= deps = django17: django>=1.7,<1.8 django18: django>=1.8,<1.9 - django19: django>=1.9,<2.0 + django19: django>=1.9,<1.10 coverage mock From ddb62a383b19d0fd006a5c80e96527049552f321 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Wed, 3 Aug 2016 23:44:17 -0600 Subject: [PATCH 02/29] Remove unused import Update mock requirement. --- oidc_provider/urls.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/oidc_provider/urls.py b/oidc_provider/urls.py index 531e3f6..6235ec3 100644 --- a/oidc_provider/urls.py +++ b/oidc_provider/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.views.decorators.csrf import csrf_exempt from oidc_provider.views import * diff --git a/setup.py b/setup.py index 6fd3783..049464d 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ setup( ], tests_require=[ 'pyjwkest==1.1.0', - 'mock==1.3.0', + 'mock==2.0.0', ], install_requires=[ From 3ef8f42cd74bb94fa418f16c22b17fd23b6722f1 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 4 Aug 2016 00:30:22 -0600 Subject: [PATCH 03/29] Test against Django v1.10 Add Django 1.10 to test matrix. --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 05d675e..5884163 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist= - clean,py{27,34}-django{17,18,19},py35-django{18,19},stats + clean,py{27,34}-django{17,18,19,110},py35-django{18,19,110},stats [testenv] @@ -9,6 +9,7 @@ deps = django17: django>=1.7,<1.8 django18: django>=1.8,<1.9 django19: django>=1.9,<1.10 + django110: django>=1.10,<1.11 coverage mock From 2573a600920bbd3b3b492972539855cf722c0b84 Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Thu, 4 Aug 2016 13:35:27 -0300 Subject: [PATCH 04/29] Fix test app urls and templates loaders in settings. --- oidc_provider/tests/app/settings.py | 16 ++++++++++++++++ oidc_provider/tests/app/urls.py | 6 +++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/oidc_provider/tests/app/settings.py b/oidc_provider/tests/app/settings.py index 113f43c..39d0c0b 100644 --- a/oidc_provider/tests/app/settings.py +++ b/oidc_provider/tests/app/settings.py @@ -19,6 +19,22 @@ MIDDLEWARE_CLASSES = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', ] +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + LOGGING = { 'version': 1, 'disable_existing_loggers': False, diff --git a/oidc_provider/tests/app/urls.py b/oidc_provider/tests/app/urls.py index abfb2c4..8c513fd 100644 --- a/oidc_provider/tests/app/urls.py +++ b/oidc_provider/tests/app/urls.py @@ -1,10 +1,10 @@ from django.contrib.auth import views as auth_views -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin from django.views.generic import TemplateView -urlpatterns = patterns('', +urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'), url(r'^accounts/login/$', auth_views.login, {'template_name': 'accounts/login.html'}, name='login'), url(r'^accounts/logout/$', auth_views.logout, {'template_name': 'accounts/logout.html'}, name='logout'), @@ -12,4 +12,4 @@ urlpatterns = patterns('', url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), url(r'^admin/', include(admin.site.urls)), -) +] From b67c35d94a9e99f591c778cf363f65cd5b9969d4 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 4 Aug 2016 11:07:34 -0600 Subject: [PATCH 05/29] Add Django 1.10 as a supported configuration. --- docs/sections/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sections/installation.rst b/docs/sections/installation.rst index 3c4f811..59b5e10 100644 --- a/docs/sections/installation.rst +++ b/docs/sections/installation.rst @@ -7,7 +7,7 @@ Requirements ============ * Python: ``2.7`` ``3.4`` ``3.5`` -* Django: ``1.7`` ``1.8`` ``1.9`` +* Django: ``1.7`` ``1.8`` ``1.9`` ``1.10`` Quick Installation ================== From d7ea6e33cdd174a2bd10ea4f7a5b184047832013 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 5 Aug 2016 13:25:28 -0300 Subject: [PATCH 06/29] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bfc26b..29a0661 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ### [Unreleased] +##### Added +- Support for Django 1.10. + ### [0.3.6] - 2016-07-07 ##### Changed From afc3a60ee7fed1988978e42c528d990b0a77ff68 Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 5 Aug 2016 13:11:01 -0600 Subject: [PATCH 07/29] Added at_hash when access token is present This is required by response type "id_token token", but can be used by other flows if they choose. --- oidc_provider/lib/endpoints/authorize.py | 44 ++++++++++++++---------- oidc_provider/lib/endpoints/token.py | 26 +++++++------- oidc_provider/lib/utils/token.py | 14 +++++--- oidc_provider/models.py | 16 ++++++++- 4 files changed, 63 insertions(+), 37 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index d2d1951..b3ea536 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -121,35 +121,41 @@ class AuthorizeEndpoint(object): query_params['state'] = self.params.state if self.params.state else '' elif self.grant_type == 'implicit': - # 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, - request=self.request) - query_fragment['id_token'] = encode_id_token(id_token_dic, self.client) - else: - id_token_dic = {} - token = create_token( user=self.request.user, client=self.client, - id_token_dic=id_token_dic, scope=self.params.scope) - # Store the token. - token.save() - - query_fragment['token_type'] = 'bearer' - # TODO: Create setting 'OIDC_TOKEN_EXPIRE'. - query_fragment['expires_in'] = 60 * 10 - # 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 + # We don't need id_token if it's an OAuth2 request. + if self.is_authentication: + kwargs = { + "user": self.request.user, + "aud": self.client.client_id, + "nonce": self.params.nonce, + "request": self.request + } + # Include at_hash when access_token is being returned. + if 'access_token' in query_fragment: + kwargs['at_hash'] = token.at_hash + id_token_dic = create_id_token(**kwargs) + query_fragment['id_token'] = encode_id_token(id_token_dic, self.client) + token.id_token = id_token_dic + else: + id_token_dic = {} + + # Store the token. + token.id_token = id_token_dic + token.save() + + query_fragment['token_type'] = 'bearer' + # TODO: Create setting 'OIDC_TOKEN_EXPIRE'. + query_fragment['expires_in'] = 60 * 10 + query_fragment['state'] = self.params.state if self.params.state else '' except Exception as error: diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 200cb2c..6aa3b83 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -131,23 +131,24 @@ class TokenEndpoint(object): return self.create_refresh_response_dic() def create_code_response_dic(self): + token = create_token( + user=self.code.user, + client=self.code.client, + scope=self.code.scope) + if self.code.is_authentication: id_token_dic = create_id_token( user=self.code.user, aud=self.client.client_id, nonce=self.code.nonce, + at_hash=token.at_hash, request=self.request, ) else: id_token_dic = {} - token = create_token( - user=self.code.user, - client=self.code.client, - id_token_dic=id_token_dic, - scope=self.code.scope) - # Store the token. + token.id_token = id_token_dic token.save() # We don't need to store the code anymore. @@ -164,24 +165,25 @@ class TokenEndpoint(object): return dic def create_refresh_response_dic(self): + token = create_token( + user=self.token.user, + client=self.token.client, + scope=self.token.scope) + # 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, + at_hash=token.at_hash, request=self.request, ) else: id_token_dic = {} - token = create_token( - user=self.token.user, - client=self.token.client, - id_token_dic=id_token_dic, - scope=self.token.scope) - # Store the token. + token.id_token = id_token_dic token.save() # Forget the old token. diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index fc0880d..83291ec 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -13,7 +13,7 @@ from oidc_provider.models import * from oidc_provider import settings -def create_id_token(user, aud, nonce, request=None): +def create_id_token(user, aud, nonce, at_hash=None, request=None): """ Receives a user object and aud (audience). Then creates the id_token dictionary. @@ -44,6 +44,9 @@ def create_id_token(user, aud, nonce, request=None): if nonce: dic['nonce'] = str(nonce) + if at_hash: + dic['at_hash'] = at_hash + processing_hook = settings.get('OIDC_IDTOKEN_PROCESSING_HOOK') if isinstance(processing_hook, (list, tuple)): @@ -73,13 +76,13 @@ def encode_id_token(payload, client): keys = [SYMKey(key=client.client_secret, alg=alg)] else: raise Exception('Unsupported key algorithm.') - + _jws = JWS(payload, alg=alg) return _jws.sign_compact(keys) -def create_token(user, client, id_token_dic, scope): +def create_token(user, client, scope, id_token_dic=None): """ Create and populate a Token object. @@ -90,7 +93,8 @@ def create_token(user, client, id_token_dic, scope): token.client = client token.access_token = uuid.uuid4().hex - token.id_token = id_token_dic + if id_token_dic is not None: + token.id_token = id_token_dic token.refresh_token = uuid.uuid4().hex token.expires_at = timezone.now() + timedelta( @@ -112,7 +116,7 @@ def create_code(user, client, scope, nonce, is_authentication, code.client = client code.code = uuid.uuid4().hex - + if code_challenge and code_challenge_method: code.code_challenge = code_challenge code.code_challenge_method = code_challenge_method diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 09b36a2..284e858 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -from hashlib import md5 +import base64 +import binascii +from hashlib import md5, sha256 import json from django.db import models @@ -117,6 +119,18 @@ class Token(BaseCodeTokenModel): verbose_name = _(u'Token') verbose_name_plural = _(u'Tokens') + @property + def at_hash(self): + # @@@ d-o-p only supports 256 bits (change this if that changes) + hashed_access_token = sha256( + self.access_token.encode('ascii') + ).hexdigest().encode('ascii') + return base64.urlsafe_b64encode( + binascii.unhexlify( + hashed_access_token[:len(hashed_access_token) // 2] + ) + ).rstrip(b'=').decode('ascii') + class UserConsent(BaseCodeTokenModel): From 8cbf5c3304fc355da5842334b251d9ddb7132c99 Mon Sep 17 00:00:00 2001 From: Arkadiy Korotaev Date: Mon, 8 Aug 2016 09:58:36 +0400 Subject: [PATCH 08/29] Cleanup urls.py - remove unused and wildcard import --- oidc_provider/urls.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/oidc_provider/urls.py b/oidc_provider/urls.py index 6235ec3..bd9e911 100644 --- a/oidc_provider/urls.py +++ b/oidc_provider/urls.py @@ -1,16 +1,14 @@ -from django.conf.urls import include, url +from django.conf.urls import url from django.views.decorators.csrf import csrf_exempt -from oidc_provider.views import * +from oidc_provider import views urlpatterns = [ + url(r'^authorize/?$', views.AuthorizeView.as_view(), name='authorize'), + url(r'^token/?$', csrf_exempt(views.TokenView.as_view()), name='token'), + url(r'^userinfo/?$', csrf_exempt(views.userinfo), name='userinfo'), + url(r'^logout/?$', views.LogoutView.as_view(), name='logout'), - url(r'^authorize/?$', AuthorizeView.as_view(), name='authorize'), - url(r'^token/?$', csrf_exempt(TokenView.as_view()), name='token'), - url(r'^userinfo/?$', csrf_exempt(userinfo), name='userinfo'), - url(r'^logout/?$', LogoutView.as_view(), name='logout'), - - url(r'^\.well-known/openid-configuration/?$', ProviderInfoView.as_view(), name='provider_info'), - url(r'^jwks/?$', JwksView.as_view(), name='jwks'), - + url(r'^\.well-known/openid-configuration/?$', views.ProviderInfoView.as_view(), name='provider_info'), + url(r'^jwks/?$', views.JwksView.as_view(), name='jwks'), ] From 5beaa6bd12b61735e90a31f3fc12a9ba42938f09 Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Mon, 8 Aug 2016 13:18:39 -0300 Subject: [PATCH 09/29] Edit CHANGELOG. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a0661..350a45f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ##### Added - Support for Django 1.10. +- Initial translation files (ES, FR). ### [0.3.6] - 2016-07-07 From ffddb69f80e49a067ff4841e933f3bccf830cec0 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Mon, 8 Aug 2016 11:24:07 -0600 Subject: [PATCH 10/29] Add tests for at_hash Ensure at_hash is present in id_token when warranted. --- .gitignore | 2 + oidc_provider/lib/endpoints/token.py | 4 +- oidc_provider/runtests.py | 0 .../tests/test_authorize_endpoint.py | 48 +---- oidc_provider/tests/test_code_flow.py | 51 +++++ oidc_provider/tests/test_implicit_flow.py | 185 ++++++++++++++++++ runtests.py | 112 +++++++++++ setup.py | 1 + 8 files changed, 357 insertions(+), 46 deletions(-) create mode 100644 oidc_provider/runtests.py create mode 100644 oidc_provider/tests/test_code_flow.py create mode 100644 oidc_provider/tests/test_implicit_flow.py create mode 100644 runtests.py diff --git a/.gitignore b/.gitignore index fa1d0dc..2c6f875 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ src/ .venv .idea docs/_build/ +.eggs/ +.python-version diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 6aa3b83..8a7832d 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -146,9 +146,9 @@ class TokenEndpoint(object): ) else: id_token_dic = {} + token.id_token = id_token_dic # Store the token. - token.id_token = id_token_dic token.save() # We don't need to store the code anymore. @@ -181,9 +181,9 @@ class TokenEndpoint(object): ) else: id_token_dic = {} + token.id_token = id_token_dic # Store the token. - token.id_token = id_token_dic token.save() # Forget the old token. diff --git a/oidc_provider/runtests.py b/oidc_provider/runtests.py new file mode 100644 index 0000000..e69de29 diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index dd22800..cb0712c 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -1,10 +1,9 @@ try: - from urllib.parse import unquote, urlencode + from urllib.parse import urlencode except ImportError: - from urllib import unquote, urlencode + from urllib import urlencode import uuid -from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import AnonymousUser from django.core.management import call_command from django.core.urlresolvers import reverse @@ -28,7 +27,6 @@ class AuthorizationCodeFlowTestCase(TestCase): self.user = create_fake_user() self.client = create_fake_client(response_type='code') self.client_public = create_fake_client(response_type='code', is_public=True) - self.client_implicit = create_fake_client(response_type='id_token token') self.state = uuid.uuid4().hex self.nonce = uuid.uuid4().hex @@ -52,7 +50,6 @@ class AuthorizationCodeFlowTestCase(TestCase): return response - def test_missing_parameters(self): """ If the request fails due to a missing, invalid, or mismatching @@ -148,7 +145,7 @@ class AuthorizationCodeFlowTestCase(TestCase): for key, value in iter(to_check.items()): is_input_ok = input_html.format(key, value) in response.content.decode('utf-8') self.assertEqual(is_input_ok, True, - msg='Hidden input for "'+key+'" fails.') + msg='Hidden input for "' + key + '" fails.') def test_user_consent_response(self): """ @@ -183,7 +180,7 @@ class AuthorizationCodeFlowTestCase(TestCase): msg='"access_denied" code is missing in query.') # Simulate user authorization. - data['allow'] = 'Accept' # Will be the value of the button. + data['allow'] = 'Accept' # Will be the value of the button. response = self._auth_request('post', data, is_user_authenticated=True) @@ -270,43 +267,6 @@ class AuthorizationCodeFlowTestCase(TestCase): self.assertEqual('Request for Permission' in response.content.decode('utf-8'), True) - def test_implicit_missing_nonce(self): - """ - The `nonce` parameter is REQUIRED if you use the Implicit Flow. - """ - data = { - 'client_id': self.client_implicit.client_id, - 'response_type': self.client_implicit.response_type, - 'redirect_uri': self.client_implicit.default_redirect_uri, - 'scope': 'openid email', - 'state': self.state, - } - - response = self._auth_request('get', data, is_user_authenticated=True) - - self.assertEqual('#error=invalid_request' in response['Location'], True) - - def test_implicit_access_token_response(self): - """ - Unlike the Authorization Code flow, in which the client makes - separate requests for authorization and for an access token, the client - receives the access token as the result of the authorization request. - """ - data = { - 'client_id': self.client_implicit.client_id, - 'redirect_uri': self.client_implicit.default_redirect_uri, - 'response_type': self.client_implicit.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertEqual('access_token' in response['Location'], True) - - def test_prompt_parameter(self): """ Specifies whether the Authorization Server prompts the End-User for reauthentication and consent. diff --git a/oidc_provider/tests/test_code_flow.py b/oidc_provider/tests/test_code_flow.py new file mode 100644 index 0000000..60a4e54 --- /dev/null +++ b/oidc_provider/tests/test_code_flow.py @@ -0,0 +1,51 @@ +try: + from urllib.parse import unquote, urlencode +except ImportError: + from urllib import unquote, urlencode +import uuid + +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.models import AnonymousUser +from django.core.management import call_command +from django.core.urlresolvers import reverse +from django.test import RequestFactory +from django.test import TestCase + +from oidc_provider.models import * +from oidc_provider.tests.app.utils import * +from oidc_provider.views import * + + +class CodeFlowTestCase(TestCase): + """ + Test cases for Authorization Code Flow. + """ + + def setUp(self): + call_command('creatersakey') + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='code') + self.client_public = create_fake_client(response_type='code', is_public=True) + self.state = uuid.uuid4().hex + self.nonce = uuid.uuid4().hex + + def _auth_request(self, method, data={}, is_user_authenticated=False): + url = reverse('oidc_provider:authorize') + + if method.lower() == 'get': + query_str = urlencode(data).replace('+', '%20') + if query_str: + url += '?' + query_str + request = self.factory.get(url) + elif method.lower() == 'post': + request = self.factory.post(url, data=data) + else: + raise Exception('Method unsupported for an Authorization Request.') + + # Simulate that the user is logged. + request.user = self.user if is_user_authenticated else AnonymousUser() + + response = AuthorizeView.as_view()(request) + + return response diff --git a/oidc_provider/tests/test_implicit_flow.py b/oidc_provider/tests/test_implicit_flow.py new file mode 100644 index 0000000..1f52579 --- /dev/null +++ b/oidc_provider/tests/test_implicit_flow.py @@ -0,0 +1,185 @@ +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs +import uuid + +from django.contrib.auth.models import AnonymousUser +from django.core.management import call_command +from django.core.urlresolvers import reverse +from django.test import RequestFactory +from django.test import TestCase +from jwkest.jwt import JWT + +from oidc_provider.models import * +from oidc_provider.tests.app.utils import * +from oidc_provider.views import * + + +class ImplicitFlowTestCase(TestCase): + """ + Test cases for Authorization Implicit Flow. + """ + + def setUp(self): + call_command('creatersakey') + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='id_token token') + self.client_public = create_fake_client(response_type='id_token token', is_public=True) + self.client_no_access = create_fake_client(response_type='id_token') + self.client_public_no_access = create_fake_client(response_type='id_token', is_public=True) + self.state = uuid.uuid4().hex + self.nonce = uuid.uuid4().hex + + def _auth_request(self, method, data={}, is_user_authenticated=False): + url = reverse('oidc_provider:authorize') + + if method.lower() == 'get': + query_str = urlencode(data).replace('+', '%20') + if query_str: + url += '?' + query_str + request = self.factory.get(url) + elif method.lower() == 'post': + request = self.factory.post(url, data=data) + else: + raise Exception('Method unsupported for an Authorization Request.') + + # Simulate that the user is logged. + request.user = self.user if is_user_authenticated else AnonymousUser() + + response = AuthorizeView.as_view()(request) + + return response + + def test_missing_nonce(self): + """ + The `nonce` parameter is REQUIRED if you use the Implicit Flow. + """ + data = { + 'client_id': self.client.client_id, + 'response_type': self.client.response_type, + 'redirect_uri': self.client.default_redirect_uri, + 'scope': 'openid email', + 'state': self.state, + } + + response = self._auth_request('get', data, is_user_authenticated=True) + + self.assertEqual('#error=invalid_request' in response['Location'], True) + + def test_id_token_token_response(self): + """ + Implicit client requesting `id_token token` receives both id token + and access token as the result of the authorization request. + """ + data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': self.client.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + # same for public client + data['client_id'] = self.client_public.client_id, + data['redirect_uri'] = self.client_public.default_redirect_uri, + data['response_type'] = self.client_public.response_type, + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + def test_id_token_response(self): + """ + Implicit client requesting `id_token` receives + only an id token as the result of the authorization request. + """ + data = { + 'client_id': self.client_no_access.client_id, + 'redirect_uri': self.client_no_access.default_redirect_uri, + 'response_type': self.client_no_access.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertNotIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + # same for public client + data['client_id'] = self.client_public_no_access.client_id, + data['redirect_uri'] = self.client_public_no_access.default_redirect_uri, + data['response_type'] = self.client_public_no_access.response_type, + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertNotIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + def test_id_token_token_at_hash(self): + """ + Implicit client requesting `id_token token` receives + `at_hash` in `id_token`. + """ + data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': self.client.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('id_token', response['Location']) + + # obtain `id_token` portion of Location + components = urlsplit(response['Location']) + fragment = parse_qs(components[4]) + id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() + + self.assertIn('at_hash', id_token) + + def test_id_token_at_hash(self): + """ + Implicit client requesting `id_token` should not receive + `at_hash` in `id_token`. + """ + data = { + 'client_id': self.client_no_access.client_id, + 'redirect_uri': self.client_no_access.default_redirect_uri, + 'response_type': self.client_no_access.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('id_token', response['Location']) + + # obtain `id_token` portion of Location + components = urlsplit(response['Location']) + fragment = parse_qs(components[4]) + id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() + + self.assertNotIn('at_hash', id_token) diff --git a/runtests.py b/runtests.py new file mode 100644 index 0000000..388ce99 --- /dev/null +++ b/runtests.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +import os +import sys + +import django + +from django.conf import settings + + +DEFAULT_SETTINGS = dict( + + DEBUG = False, + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } + }, + + SITE_ID = 1, + + MIDDLEWARE_CLASSES = [ + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ], + + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, + ], + + LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + }, + }, + 'loggers': { + 'oidc_provider': { + 'handlers': ['console'], + 'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'), + }, + }, + }, + + INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.admin', + 'oidc_provider', + ], + + SECRET_KEY = 'this-should-be-top-secret', + + ROOT_URLCONF = 'oidc_provider.tests.app.urls', + + TEMPLATE_DIRS = [ + 'oidc_provider/tests/templates', + ], + + USE_TZ = True, + + # OIDC Provider settings. + + SITE_URL = 'http://localhost:8000', + OIDC_USERINFO = 'oidc_provider.tests.app.utils.userinfo', + +) + + +def runtests(*test_args): + if not settings.configured: + settings.configure(**DEFAULT_SETTINGS) + + django.setup() + + parent = os.path.dirname(os.path.abspath(__file__)) + sys.path.insert(0, parent) + + try: + from django.test.runner import DiscoverRunner + runner_class = DiscoverRunner + test_args = ["oidc_provider.tests"] + except ImportError: + from django.test.simple import DjangoTestSuiteRunner + runner_class = DjangoTestSuiteRunner + test_args = ["tests"] + + failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args) + sys.exit(failures) + + +if __name__ == "__main__": + runtests(*sys.argv[1:]) diff --git a/setup.py b/setup.py index 049464d..cfde4c4 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ setup( 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], + test_suite="runtests.runtests", tests_require=[ 'pyjwkest==1.1.0', 'mock==2.0.0', From e04d42fedf82f28bfe6a8fa15601b43c1a0f3ff9 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Mon, 8 Aug 2016 11:54:40 -0600 Subject: [PATCH 11/29] flake8 fixes --- oidc_provider/models.py | 11 ++++++++++- oidc_provider/tests/test_code_flow.py | 5 ++--- oidc_provider/tests/test_implicit_flow.py | 4 ++-- oidc_provider/views.py | 6 +++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/oidc_provider/models.py b/oidc_provider/models.py index 284e858..0adda40 100644 --- a/oidc_provider/models.py +++ b/oidc_provider/models.py @@ -26,6 +26,7 @@ JWT_ALGS = [ ('RS256', 'RS256'), ] + class Client(models.Model): name = models.CharField(max_length=100, default='', verbose_name=_(u'Name')) @@ -51,8 +52,10 @@ class Client(models.Model): def redirect_uris(): def fget(self): return self._redirect_uris.splitlines() + def fset(self, value): self._redirect_uris = '\n'.join(value) + return locals() redirect_uris = property(**redirect_uris()) @@ -71,8 +74,10 @@ class BaseCodeTokenModel(models.Model): def scope(): def fget(self): return self._scope.split() + def fset(self, value): self._scope = ' '.join(value) + return locals() scope = property(**scope()) @@ -107,11 +112,15 @@ class Token(BaseCodeTokenModel): access_token = models.CharField(max_length=255, unique=True, verbose_name=_(u'Access Token')) refresh_token = models.CharField(max_length=255, unique=True, null=True, verbose_name=_(u'Refresh Token')) _id_token = models.TextField(verbose_name=_(u'ID Token')) + def id_token(): + def fget(self): return json.loads(self._id_token) + def fset(self, value): self._id_token = json.dumps(value) + return locals() id_token = property(**id_token()) @@ -156,4 +165,4 @@ class RSAKey(models.Model): @property def kid(self): - return u'{0}'.format(md5(self.key.encode('utf-8')).hexdigest() if self.key else '') + return u'{0}'.format(md5(self.key.encode('utf-8')).hexdigest() if self.key else '') diff --git a/oidc_provider/tests/test_code_flow.py b/oidc_provider/tests/test_code_flow.py index 60a4e54..349d02e 100644 --- a/oidc_provider/tests/test_code_flow.py +++ b/oidc_provider/tests/test_code_flow.py @@ -1,10 +1,9 @@ try: - from urllib.parse import unquote, urlencode + from urllib.parse import urlencode except ImportError: - from urllib import unquote, urlencode + from urllib import urlencode import uuid -from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth.models import AnonymousUser from django.core.management import call_command from django.core.urlresolvers import reverse diff --git a/oidc_provider/tests/test_implicit_flow.py b/oidc_provider/tests/test_implicit_flow.py index 1f52579..121d621 100644 --- a/oidc_provider/tests/test_implicit_flow.py +++ b/oidc_provider/tests/test_implicit_flow.py @@ -3,9 +3,9 @@ try: except ImportError: from urllib import urlencode try: - from urllib.parse import urlparse, parse_qs + from urllib.parse import parse_qs, urlsplit except ImportError: - from urlparse import urlparse, parse_qs + from urlparse import parse_qs, urlsplit import uuid from django.contrib.auth.models import AnonymousUser diff --git a/oidc_provider/views.py b/oidc_provider/views.py index a22ca17..ae4bf69 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -193,8 +193,8 @@ class ProviderInfoView(View): # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes dic['subject_types_supported'] = ['public'] - dic['token_endpoint_auth_methods_supported'] = [ 'client_secret_post', - 'client_secret_basic' ] + dic['token_endpoint_auth_methods_supported'] = ['client_secret_post', + 'client_secret_basic'] return JsonResponse(dic) @@ -205,7 +205,7 @@ class JwksView(View): dic = dict(keys=[]) for rsakey in RSAKey.objects.all(): - public_key = RSA.importKey(rsakey.key).publickey() + public_key = RSA.importKey(rsakey.key).publickey() dic['keys'].append({ 'kty': 'RSA', 'alg': 'RS256', From e822252b6ea4d6f4dc10d9af5377e930d9c5409c Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Mon, 8 Aug 2016 12:20:47 -0600 Subject: [PATCH 12/29] Use original test files --- .../tests/test_authorize_endpoint.py | 172 +++++++++++++++- oidc_provider/tests/test_code_flow.py | 50 ----- oidc_provider/tests/test_implicit_flow.py | 185 ------------------ oidc_provider/tests/test_token_endpoint.py | 15 ++ 4 files changed, 186 insertions(+), 236 deletions(-) delete mode 100644 oidc_provider/tests/test_code_flow.py delete mode 100644 oidc_provider/tests/test_implicit_flow.py diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index cb0712c..f892ae2 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -2,6 +2,10 @@ try: from urllib.parse import urlencode except ImportError: from urllib import urlencode +try: + from urllib.parse import parse_qs, urlsplit +except ImportError: + from urlparse import parse_qs, urlsplit import uuid from django.contrib.auth.models import AnonymousUser @@ -9,6 +13,7 @@ from django.core.management import call_command from django.core.urlresolvers import reverse from django.test import RequestFactory from django.test import TestCase +from jwkest.jwt import JWT from oidc_provider import settings from oidc_provider.models import * @@ -18,7 +23,7 @@ from oidc_provider.views import * class AuthorizationCodeFlowTestCase(TestCase): """ - Test cases for Authorize Endpoint using Authorization Code Flow. + Test cases for Authorize Endpoint using Code Flow. """ def setUp(self): @@ -291,3 +296,168 @@ class AuthorizationCodeFlowTestCase(TestCase): # An error is returned if the Client does not have pre-configured consent for the requested Claims. self.assertEqual('interaction_required' in response['Location'], True) + + +class AuthorizationImplicitFlowTestCase(TestCase): + """ + Test cases for Authorization Endpoint using Implicit Flow. + """ + + def setUp(self): + call_command('creatersakey') + self.factory = RequestFactory() + self.user = create_fake_user() + self.client = create_fake_client(response_type='id_token token') + self.client_public = create_fake_client(response_type='id_token token', is_public=True) + self.client_no_access = create_fake_client(response_type='id_token') + self.client_public_no_access = create_fake_client(response_type='id_token', is_public=True) + self.state = uuid.uuid4().hex + self.nonce = uuid.uuid4().hex + + def _auth_request(self, method, data={}, is_user_authenticated=False): + url = reverse('oidc_provider:authorize') + + if method.lower() == 'get': + query_str = urlencode(data).replace('+', '%20') + if query_str: + url += '?' + query_str + request = self.factory.get(url) + elif method.lower() == 'post': + request = self.factory.post(url, data=data) + else: + raise Exception('Method unsupported for an Authorization Request.') + + # Simulate that the user is logged. + request.user = self.user if is_user_authenticated else AnonymousUser() + + response = AuthorizeView.as_view()(request) + + return response + + def test_missing_nonce(self): + """ + The `nonce` parameter is REQUIRED if you use the Implicit Flow. + """ + data = { + 'client_id': self.client.client_id, + 'response_type': self.client.response_type, + 'redirect_uri': self.client.default_redirect_uri, + 'scope': 'openid email', + 'state': self.state, + } + + response = self._auth_request('get', data, is_user_authenticated=True) + + self.assertEqual('#error=invalid_request' in response['Location'], True) + + def test_id_token_token_response(self): + """ + Implicit client requesting `id_token token` receives both id token + and access token as the result of the authorization request. + """ + data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': self.client.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + # same for public client + data['client_id'] = self.client_public.client_id, + data['redirect_uri'] = self.client_public.default_redirect_uri, + data['response_type'] = self.client_public.response_type, + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + def test_id_token_response(self): + """ + Implicit client requesting `id_token` receives + only an id token as the result of the authorization request. + """ + data = { + 'client_id': self.client_no_access.client_id, + 'redirect_uri': self.client_no_access.default_redirect_uri, + 'response_type': self.client_no_access.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertNotIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + # same for public client + data['client_id'] = self.client_public_no_access.client_id, + data['redirect_uri'] = self.client_public_no_access.default_redirect_uri, + data['response_type'] = self.client_public_no_access.response_type, + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertNotIn('access_token', response['Location']) + self.assertIn('id_token', response['Location']) + + def test_id_token_token_at_hash(self): + """ + Implicit client requesting `id_token token` receives + `at_hash` in `id_token`. + """ + data = { + 'client_id': self.client.client_id, + 'redirect_uri': self.client.default_redirect_uri, + 'response_type': self.client.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('id_token', response['Location']) + + # obtain `id_token` portion of Location + components = urlsplit(response['Location']) + fragment = parse_qs(components[4]) + id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() + + self.assertIn('at_hash', id_token) + + def test_id_token_at_hash(self): + """ + Implicit client requesting `id_token` should not receive + `at_hash` in `id_token`. + """ + data = { + 'client_id': self.client_no_access.client_id, + 'redirect_uri': self.client_no_access.default_redirect_uri, + 'response_type': self.client_no_access.response_type, + 'scope': 'openid email', + 'state': self.state, + 'nonce': self.nonce, + 'allow': 'Accept', + } + + response = self._auth_request('post', data, is_user_authenticated=True) + + self.assertIn('id_token', response['Location']) + + # obtain `id_token` portion of Location + components = urlsplit(response['Location']) + fragment = parse_qs(components[4]) + id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() + + self.assertNotIn('at_hash', id_token) diff --git a/oidc_provider/tests/test_code_flow.py b/oidc_provider/tests/test_code_flow.py deleted file mode 100644 index 349d02e..0000000 --- a/oidc_provider/tests/test_code_flow.py +++ /dev/null @@ -1,50 +0,0 @@ -try: - from urllib.parse import urlencode -except ImportError: - from urllib import urlencode -import uuid - -from django.contrib.auth.models import AnonymousUser -from django.core.management import call_command -from django.core.urlresolvers import reverse -from django.test import RequestFactory -from django.test import TestCase - -from oidc_provider.models import * -from oidc_provider.tests.app.utils import * -from oidc_provider.views import * - - -class CodeFlowTestCase(TestCase): - """ - Test cases for Authorization Code Flow. - """ - - def setUp(self): - call_command('creatersakey') - self.factory = RequestFactory() - self.user = create_fake_user() - self.client = create_fake_client(response_type='code') - self.client_public = create_fake_client(response_type='code', is_public=True) - self.state = uuid.uuid4().hex - self.nonce = uuid.uuid4().hex - - def _auth_request(self, method, data={}, is_user_authenticated=False): - url = reverse('oidc_provider:authorize') - - if method.lower() == 'get': - query_str = urlencode(data).replace('+', '%20') - if query_str: - url += '?' + query_str - request = self.factory.get(url) - elif method.lower() == 'post': - request = self.factory.post(url, data=data) - else: - raise Exception('Method unsupported for an Authorization Request.') - - # Simulate that the user is logged. - request.user = self.user if is_user_authenticated else AnonymousUser() - - response = AuthorizeView.as_view()(request) - - return response diff --git a/oidc_provider/tests/test_implicit_flow.py b/oidc_provider/tests/test_implicit_flow.py deleted file mode 100644 index 121d621..0000000 --- a/oidc_provider/tests/test_implicit_flow.py +++ /dev/null @@ -1,185 +0,0 @@ -try: - from urllib.parse import urlencode -except ImportError: - from urllib import urlencode -try: - from urllib.parse import parse_qs, urlsplit -except ImportError: - from urlparse import parse_qs, urlsplit -import uuid - -from django.contrib.auth.models import AnonymousUser -from django.core.management import call_command -from django.core.urlresolvers import reverse -from django.test import RequestFactory -from django.test import TestCase -from jwkest.jwt import JWT - -from oidc_provider.models import * -from oidc_provider.tests.app.utils import * -from oidc_provider.views import * - - -class ImplicitFlowTestCase(TestCase): - """ - Test cases for Authorization Implicit Flow. - """ - - def setUp(self): - call_command('creatersakey') - self.factory = RequestFactory() - self.user = create_fake_user() - self.client = create_fake_client(response_type='id_token token') - self.client_public = create_fake_client(response_type='id_token token', is_public=True) - self.client_no_access = create_fake_client(response_type='id_token') - self.client_public_no_access = create_fake_client(response_type='id_token', is_public=True) - self.state = uuid.uuid4().hex - self.nonce = uuid.uuid4().hex - - def _auth_request(self, method, data={}, is_user_authenticated=False): - url = reverse('oidc_provider:authorize') - - if method.lower() == 'get': - query_str = urlencode(data).replace('+', '%20') - if query_str: - url += '?' + query_str - request = self.factory.get(url) - elif method.lower() == 'post': - request = self.factory.post(url, data=data) - else: - raise Exception('Method unsupported for an Authorization Request.') - - # Simulate that the user is logged. - request.user = self.user if is_user_authenticated else AnonymousUser() - - response = AuthorizeView.as_view()(request) - - return response - - def test_missing_nonce(self): - """ - The `nonce` parameter is REQUIRED if you use the Implicit Flow. - """ - data = { - 'client_id': self.client.client_id, - 'response_type': self.client.response_type, - 'redirect_uri': self.client.default_redirect_uri, - 'scope': 'openid email', - 'state': self.state, - } - - response = self._auth_request('get', data, is_user_authenticated=True) - - self.assertEqual('#error=invalid_request' in response['Location'], True) - - def test_id_token_token_response(self): - """ - Implicit client requesting `id_token token` receives both id token - and access token as the result of the authorization request. - """ - data = { - 'client_id': self.client.client_id, - 'redirect_uri': self.client.default_redirect_uri, - 'response_type': self.client.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - # same for public client - data['client_id'] = self.client_public.client_id, - data['redirect_uri'] = self.client_public.default_redirect_uri, - data['response_type'] = self.client_public.response_type, - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - def test_id_token_response(self): - """ - Implicit client requesting `id_token` receives - only an id token as the result of the authorization request. - """ - data = { - 'client_id': self.client_no_access.client_id, - 'redirect_uri': self.client_no_access.default_redirect_uri, - 'response_type': self.client_no_access.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertNotIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - # same for public client - data['client_id'] = self.client_public_no_access.client_id, - data['redirect_uri'] = self.client_public_no_access.default_redirect_uri, - data['response_type'] = self.client_public_no_access.response_type, - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertNotIn('access_token', response['Location']) - self.assertIn('id_token', response['Location']) - - def test_id_token_token_at_hash(self): - """ - Implicit client requesting `id_token token` receives - `at_hash` in `id_token`. - """ - data = { - 'client_id': self.client.client_id, - 'redirect_uri': self.client.default_redirect_uri, - 'response_type': self.client.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('id_token', response['Location']) - - # obtain `id_token` portion of Location - components = urlsplit(response['Location']) - fragment = parse_qs(components[4]) - id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() - - self.assertIn('at_hash', id_token) - - def test_id_token_at_hash(self): - """ - Implicit client requesting `id_token` should not receive - `at_hash` in `id_token`. - """ - data = { - 'client_id': self.client_no_access.client_id, - 'redirect_uri': self.client_no_access.default_redirect_uri, - 'response_type': self.client_no_access.response_type, - 'scope': 'openid email', - 'state': self.state, - 'nonce': self.nonce, - 'allow': 'Accept', - } - - response = self._auth_request('post', data, is_user_authenticated=True) - - self.assertIn('id_token', response['Location']) - - # obtain `id_token` portion of Location - components = urlsplit(response['Location']) - fragment = parse_qs(components[4]) - id_token = JWT().unpack(fragment["id_token"][0].encode('utf-8')).payload() - - self.assertNotIn('at_hash', id_token) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 0873b23..b993a2a 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -304,6 +304,21 @@ class TokenTestCase(TestCase): self.assertEqual(id_token.get('nonce'), None) + def test_id_token_contains_at_hash(self): + """ + If access_token is included, the id_token SHOULD contain an at_hash. + """ + code = self._create_code() + + post_data = self._auth_code_post_data(code=code.code) + + response = self._post_request(post_data) + + response_dic = json.loads(response.content.decode('utf-8')) + id_token = JWT().unpack(response_dic['id_token'].encode('utf-8')).payload() + + self.assertTrue(id_token.get('at_hash')) + def test_idtoken_sign_validation(self): """ We MUST validate the signature of the ID Token according to JWS From 093003291ba7e60f69ff50f2782fb3ab542c9d34 Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Wed, 10 Aug 2016 18:37:00 -0300 Subject: [PATCH 13/29] Edit CHANGELOG. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 350a45f..eed2931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ##### Added - Support for Django 1.10. - Initial translation files (ES, FR). +- Support for at_hash parameter. ### [0.3.6] - 2016-07-07 From 53c9bf0717412531324d02de4fd89e616e7a848c Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Thu, 11 Aug 2016 12:05:52 -0300 Subject: [PATCH 14/29] Edit README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 676d50c..343b7cb 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,4 @@ We love contributions, so please feel free to fix bugs, improve things, provide * Fork the project. * Make your feature addition or bug fix. * Add tests for it inside `oidc_provider/tests`. Then run all and ensure everything is OK (read docs for how to test in all envs). -* Send pull request to the specific version branch. +* Send pull request to the `develop` branch. From b8d1d63c2883bb8f9a1b2673c8960f8422997a78 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 11 Aug 2016 11:13:23 -0600 Subject: [PATCH 15/29] Improve tox.ini envlist layout, simplified commands. Remove tests/app/settings.py in favor of settings in setup.py. Change MANIFEST.in to include README.md (which exists) instead of README.rst (which does not exist). Update .travis.yml to use tox instead of `python django_admin.py`, include Django v1.10 in the mix. --- .travis.yml | 8 +-- MANIFEST.in | 2 +- oidc_provider/tests/app/settings.py | 77 ----------------------------- tox.ini | 9 ++-- 4 files changed, 11 insertions(+), 85 deletions(-) delete mode 100644 oidc_provider/tests/app/settings.py diff --git a/.travis.yml b/.travis.yml index d947e99..32d0946 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,14 @@ env: - DJANGO=1.7 - DJANGO=1.8 - DJANGO=1.9 + - DJANGO=1.10 matrix: exclude: - python: "3.5" env: DJANGO=1.7 install: - - pip install -q django==$DJANGO - - pip install -e . + - pip install tox coveralls script: - - PYTHONPATH=$PYTHONPATH:$PWD django-admin.py test oidc_provider --settings=oidc_provider.tests.app.settings + - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO +after_success: + - coveralls diff --git a/MANIFEST.in b/MANIFEST.in index 07cb30b..de708f8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ include LICENSE -include README.rst +include README.md recursive-include oidc_provider/templates * recursive-include oidc_provider/tests/templates * diff --git a/oidc_provider/tests/app/settings.py b/oidc_provider/tests/app/settings.py deleted file mode 100644 index 39d0c0b..0000000 --- a/oidc_provider/tests/app/settings.py +++ /dev/null @@ -1,77 +0,0 @@ -import os -from datetime import timedelta - - -DEBUG = False - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - } -} - -SITE_ID = 1 - -MIDDLEWARE_CLASSES = [ - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', -] - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - }, - }, - 'loggers': { - 'oidc_provider': { - 'handlers': ['console'], - 'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'), - }, - }, -} - -INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.admin', - 'oidc_provider', -] - -SECRET_KEY = 'this-should-be-top-secret' - -ROOT_URLCONF = 'oidc_provider.tests.app.urls' - -TEMPLATE_DIRS = [ - 'oidc_provider/tests/templates', -] - -USE_TZ = True - -# OIDC Provider settings. - -SITE_URL = 'http://localhost:8000' -OIDC_USERINFO = 'oidc_provider.tests.app.utils.userinfo' diff --git a/tox.ini b/tox.ini index 5884163..849f411 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,10 @@ [tox] envlist= - clean,py{27,34}-django{17,18,19,110},py35-django{18,19,110},stats + clean, + py27-django{17,18,19,110}, + py34-django{17,18,19,110}, + py35-django{18,19,110}, [testenv] @@ -14,9 +17,7 @@ deps = mock commands = - pip uninstall --yes django-oidc-provider - pip install -e . - coverage run --omit=.tox/*,oidc_provider/tests/* {envbindir}/django-admin.py test {posargs:oidc_provider} --settings=oidc_provider.tests.app.settings + coverage run setup.py test [testenv:clean] From c24e4a48d109e95f86d06769b6cac0c62cefd371 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 11 Aug 2016 11:17:25 -0600 Subject: [PATCH 16/29] Update version to 0.3.7 after `at_hash` fix Using semantic versioning, a backward compatible bug fix deserves a PATCH increment. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cfde4c4..ba326fd 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.3.6', + version='0.3.7', packages=[ 'oidc_provider', 'oidc_provider/lib', 'oidc_provider/lib/endpoints', 'oidc_provider/lib/utils', 'oidc_provider/tests', 'oidc_provider/tests/app', From 632eb00e71f24ce70861f784466d15396049dd23 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 11 Aug 2016 11:52:44 -0600 Subject: [PATCH 17/29] Fix Travis config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 32d0946..f5fe6ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,6 @@ matrix: install: - pip install tox coveralls script: - - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO + - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-django$DJANGO after_success: - coveralls From 60cbd2680a4cbbf28769f0e6921d1e5418e751ac Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Thu, 11 Aug 2016 15:09:41 -0300 Subject: [PATCH 18/29] Fix travis config. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f5fe6ff..2245af5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,6 @@ matrix: install: - pip install tox coveralls script: - - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-django$DJANGO + - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-django${DJANGO//[.]/} after_success: - coveralls From 988cad073e593ae57a71aa766bf2cc72ba22759c Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Thu, 11 Aug 2016 16:43:30 -0300 Subject: [PATCH 19/29] Add new test for migrations. --- docs/sections/contribute.rst | 4 ++-- oidc_provider/runtests.py | 0 oidc_provider/tests/test_commands.py | 16 ++++++++++++++++ oidc_provider/tests/test_creatersakey_command.py | 12 ------------ setup.py | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) delete mode 100644 oidc_provider/runtests.py create mode 100644 oidc_provider/tests/test_commands.py delete mode 100644 oidc_provider/tests/test_creatersakey_command.py diff --git a/docs/sections/contribute.rst b/docs/sections/contribute.rst index 2811551..d5f01af 100644 --- a/docs/sections/contribute.rst +++ b/docs/sections/contribute.rst @@ -18,8 +18,8 @@ Use `tox `_ for running tests in each of the e # Run all tests. $ tox - # Run a particular test file with Python 2.7 and Django 1.9. - $ tox -e py27-django19 oidc_provider.tests.test_authorize_endpoint + # Run with Python 2.7 and Django 1.9. + $ tox -e py27-django19 If you have a Django project properly configured with the package. Then just run tests as normal:: diff --git a/oidc_provider/runtests.py b/oidc_provider/runtests.py deleted file mode 100644 index e69de29..0000000 diff --git a/oidc_provider/tests/test_commands.py b/oidc_provider/tests/test_commands.py new file mode 100644 index 0000000..cb070ec --- /dev/null +++ b/oidc_provider/tests/test_commands.py @@ -0,0 +1,16 @@ +from django.core.management import call_command +from django.test import TestCase +from django.utils.six import StringIO + + +class CommandsTest(TestCase): + + def test_creatersakey_output(self): + out = StringIO() + call_command('creatersakey', stdout=out) + self.assertIn('RSA key successfully created', out.getvalue()) + + def test_makemigrations_output(self): + out = StringIO() + call_command('makemigrations', 'oidc_provider', stdout=out) + self.assertIn('No changes detected in app', out.getvalue()) diff --git a/oidc_provider/tests/test_creatersakey_command.py b/oidc_provider/tests/test_creatersakey_command.py deleted file mode 100644 index d9424f6..0000000 --- a/oidc_provider/tests/test_creatersakey_command.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.core.management import call_command -from django.test import TestCase, override_settings -from django.utils.six import StringIO - - -class CreateRSAKeyTest(TestCase): - - @override_settings(BASE_DIR='/tmp') - def test_command_output(self): - out = StringIO() - call_command('creatersakey', stdout=out) - self.assertIn('RSA key successfully created', out.getvalue()) diff --git a/setup.py b/setup.py index ba326fd..7183e06 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ setup( 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], - test_suite="runtests.runtests", + test_suite='runtests.runtests', tests_require=[ 'pyjwkest==1.1.0', 'mock==2.0.0', From 2214ec0d70248f5f6aef62da7b0587ee555f2951 Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Thu, 11 Aug 2016 16:56:02 -0300 Subject: [PATCH 20/29] Add missing migration. --- .../migrations/0017_auto_20160811_1954.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 oidc_provider/migrations/0017_auto_20160811_1954.py diff --git a/oidc_provider/migrations/0017_auto_20160811_1954.py b/oidc_provider/migrations/0017_auto_20160811_1954.py new file mode 100644 index 0000000..de7350f --- /dev/null +++ b/oidc_provider/migrations/0017_auto_20160811_1954.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-11 19:54 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('oidc_provider', '0016_userconsent_and_verbosenames'), + ] + + operations = [ + migrations.AlterField( + model_name='client', + name='_redirect_uris', + field=models.TextField(default='', help_text='Enter each URI on a new line.', verbose_name='Redirect URIs'), + ), + migrations.AlterField( + model_name='client', + name='client_secret', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='Client SECRET'), + ), + migrations.AlterField( + model_name='client', + name='client_type', + field=models.CharField(choices=[('confidential', 'Confidential'), ('public', 'Public')], default='confidential', help_text='Confidential clients are capable of maintaining the confidentiality of their credentials. Public clients are incapable.', max_length=30, verbose_name='Client Type'), + ), + migrations.AlterField( + model_name='client', + name='name', + field=models.CharField(default='', max_length=100, verbose_name='Name'), + ), + migrations.AlterField( + model_name='client', + name='response_type', + field=models.CharField(choices=[('code', 'code (Authorization Code Flow)'), ('id_token', 'id_token (Implicit Flow)'), ('id_token token', 'id_token token (Implicit Flow)')], max_length=30, verbose_name='Response Type'), + ), + migrations.AlterField( + model_name='code', + name='_scope', + field=models.TextField(default='', verbose_name='Scopes'), + ), + migrations.AlterField( + model_name='code', + name='nonce', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='Nonce'), + ), + migrations.AlterField( + model_name='token', + name='_scope', + field=models.TextField(default='', verbose_name='Scopes'), + ), + migrations.AlterField( + model_name='userconsent', + name='_scope', + field=models.TextField(default='', verbose_name='Scopes'), + ), + ] From ba4faee6ef14b29622ad1265c8ab563ce16ee66d Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 11 Aug 2016 16:05:13 -0600 Subject: [PATCH 21/29] Fix global imports Global imports ("from X import *") are discouraged in Python. --- oidc_provider/lib/endpoints/authorize.py | 21 +++++++++-- oidc_provider/lib/endpoints/token.py | 18 +++++++-- oidc_provider/lib/utils/token.py | 6 ++- oidc_provider/tests/app/utils.py | 6 ++- .../tests/test_authorize_endpoint.py | 10 +++-- oidc_provider/tests/test_logout_endpoint.py | 3 +- .../tests/test_provider_info_endpoint.py | 4 +- oidc_provider/tests/test_token_endpoint.py | 37 ++++++++++++++----- oidc_provider/tests/test_userinfo_endpoint.py | 14 +++++-- oidc_provider/views.py | 13 +++++-- 10 files changed, 99 insertions(+), 33 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index b3ea536..ca4a584 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -1,3 +1,4 @@ +from datetime import timedelta import logging try: from urllib import urlencode @@ -8,10 +9,22 @@ except ImportError: from django.utils import timezone from oidc_provider.lib.claims import StandardScopeClaims -from oidc_provider.lib.errors import * -from oidc_provider.lib.utils.params import * -from oidc_provider.lib.utils.token import * -from oidc_provider.models import * +from oidc_provider.lib.errors import ( + AuthorizeError, + ClientIdError, + RedirectUriError, +) +from oidc_provider.lib.utils.params import Params +from oidc_provider.lib.utils.token import ( + create_code, + create_id_token, + create_token, + encode_id_token, +) +from oidc_provider.models import ( + Client, + UserConsent, +) from oidc_provider import settings diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 8a7832d..a10c508 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -9,10 +9,20 @@ except ImportError: from django.http import JsonResponse -from oidc_provider.lib.errors import * -from oidc_provider.lib.utils.params import * -from oidc_provider.lib.utils.token import * -from oidc_provider.models import * +from oidc_provider.lib.errors import ( + TokenError, +) +from oidc_provider.lib.utils.params import Params +from oidc_provider.lib.utils.token import ( + create_id_token, + create_token, + encode_id_token, +) +from oidc_provider.models import ( + Client, + Code, + Token, +) from oidc_provider import settings diff --git a/oidc_provider/lib/utils/token.py b/oidc_provider/lib/utils/token.py index 83291ec..680ee64 100644 --- a/oidc_provider/lib/utils/token.py +++ b/oidc_provider/lib/utils/token.py @@ -9,7 +9,11 @@ from jwkest.jwk import SYMKey from jwkest.jws import JWS from oidc_provider.lib.utils.common import get_issuer -from oidc_provider.models import * +from oidc_provider.models import ( + Code, + RSAKey, + Token, +) from oidc_provider import settings diff --git a/oidc_provider/tests/app/utils.py b/oidc_provider/tests/app/utils.py index 616b130..4c58071 100644 --- a/oidc_provider/tests/app/utils.py +++ b/oidc_provider/tests/app/utils.py @@ -1,4 +1,3 @@ -import os import random import string try: @@ -8,7 +7,10 @@ except ImportError: from django.contrib.auth.models import User -from oidc_provider.models import * +from oidc_provider.models import ( + Client, + Code, +) FAKE_NONCE = 'cb584e44c43ed6bd0bc2d9c7e242837d' diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index f892ae2..f323d36 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -16,9 +16,13 @@ from django.test import TestCase from jwkest.jwt import JWT from oidc_provider import settings -from oidc_provider.models import * -from oidc_provider.tests.app.utils import * -from oidc_provider.views import * +from oidc_provider.tests.app.utils import ( + create_fake_user, + create_fake_client, + FAKE_CODE_CHALLENGE, + is_code_valid, +) +from oidc_provider.views import AuthorizeView class AuthorizationCodeFlowTestCase(TestCase): diff --git a/oidc_provider/tests/test_logout_endpoint.py b/oidc_provider/tests/test_logout_endpoint.py index 40f4200..b9d1684 100644 --- a/oidc_provider/tests/test_logout_endpoint.py +++ b/oidc_provider/tests/test_logout_endpoint.py @@ -1,8 +1,7 @@ from django.core.urlresolvers import reverse from django.test import TestCase -from oidc_provider.views import * -from oidc_provider.tests.app.utils import * +from oidc_provider.tests.app.utils import create_fake_user class UserInfoTestCase(TestCase): diff --git a/oidc_provider/tests/test_provider_info_endpoint.py b/oidc_provider/tests/test_provider_info_endpoint.py index 1b205bc..0c01bc5 100644 --- a/oidc_provider/tests/test_provider_info_endpoint.py +++ b/oidc_provider/tests/test_provider_info_endpoint.py @@ -2,7 +2,7 @@ from django.core.urlresolvers import reverse from django.test import RequestFactory from django.test import TestCase -from oidc_provider.views import * +from oidc_provider.views import ProviderInfoView class ProviderInfoTestCase(TestCase): @@ -23,4 +23,4 @@ class ProviderInfoTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'] == 'application/json', True) - self.assertEqual(bool(response.content), True) \ No newline at end of file + self.assertEqual(bool(response.content), True) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index b993a2a..7c43122 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -1,3 +1,7 @@ +from datetime import timedelta +import json +import uuid + from base64 import b64encode try: from urllib.parse import urlencode @@ -5,15 +9,30 @@ except ImportError: from urllib import urlencode from django.core.management import call_command +from django.core.urlresolvers import reverse from django.test import RequestFactory, override_settings from django.test import TestCase +from django.utils import timezone from jwkest.jwk import KEYS +from jwkest.jws import JWS from jwkest.jwt import JWT from mock import patch -from oidc_provider.lib.utils.token import * -from oidc_provider.tests.app.utils import * -from oidc_provider.views import * +from oidc_provider.lib.utils.token import create_code +from oidc_provider.models import Token +from oidc_provider.tests.app.utils import ( + create_fake_user, + create_fake_client, + FAKE_CODE_CHALLENGE, + FAKE_CODE_VERIFIER, + FAKE_NONCE, + FAKE_RANDOM_STRING, +) +from oidc_provider.views import ( + JwksView, + TokenView, + userinfo, +) class TokenTestCase(TestCase): @@ -208,14 +227,14 @@ class TokenTestCase(TestCase): response = TokenView.as_view()(request) self.assertEqual(response.status_code == 405, True, - msg=request.method+' request does not return a 405 status.') + msg=request.method + ' request does not return a 405 status.') request = self.factory.post(url) response = TokenView.as_view()(request) self.assertEqual(response.status_code == 400, True, - msg=request.method+' request does not return a 400 status.') + msg=request.method + ' request does not return a 400 status.') def test_client_authentication(self): """ @@ -238,7 +257,7 @@ class TokenTestCase(TestCase): # Now, test with an invalid client_id. invalid_data = post_data.copy() - invalid_data['client_id'] = self.client.client_id * 2 # Fake id. + invalid_data['client_id'] = self.client.client_id * 2 # Fake id. # Create another grant code. code = self._create_code() @@ -264,8 +283,8 @@ class TokenTestCase(TestCase): user_pass = self.client.client_id + ':' + self.client.client_secret auth_header = b'Basic ' + b64encode(user_pass.encode('utf-8')) response = self._post_request(basicauth_data, { - 'HTTP_AUTHORIZATION': auth_header.decode('utf-8'), - }) + 'HTTP_AUTHORIZATION': auth_header.decode('utf-8'), + }) response.content.decode('utf-8') self.assertEqual('invalid_client' in response.content.decode('utf-8'), @@ -326,7 +345,7 @@ class TokenTestCase(TestCase): the JOSE Header. """ SIGKEYS = self._get_keys() - RSAKEYS = [ k for k in SIGKEYS if k.kty == 'RSA' ] + RSAKEYS = [k for k in SIGKEYS if k.kty == 'RSA'] code = self._create_code() diff --git a/oidc_provider/tests/test_userinfo_endpoint.py b/oidc_provider/tests/test_userinfo_endpoint.py index a68e011..78dc8d4 100644 --- a/oidc_provider/tests/test_userinfo_endpoint.py +++ b/oidc_provider/tests/test_userinfo_endpoint.py @@ -1,3 +1,5 @@ +import json + from datetime import timedelta try: from urllib.parse import urlencode @@ -9,9 +11,15 @@ from django.test import RequestFactory from django.test import TestCase from django.utils import timezone -from oidc_provider.lib.utils.token import * -from oidc_provider.models import * -from oidc_provider.tests.app.utils import * +from oidc_provider.lib.utils.token import ( + create_id_token, + create_token, +) +from oidc_provider.tests.app.utils import ( + create_fake_user, + create_fake_client, + FAKE_NONCE, +) from oidc_provider.views import userinfo diff --git a/oidc_provider/views.py b/oidc_provider/views.py index ae4bf69..e2a2b31 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -1,3 +1,5 @@ +import logging + from Crypto.PublicKey import RSA from django.contrib.auth.views import redirect_to_login, logout from django.core.urlresolvers import reverse @@ -9,9 +11,14 @@ from django.views.generic import View from jwkest import long_to_base64 from oidc_provider.lib.claims import StandardScopeClaims -from oidc_provider.lib.endpoints.authorize import * -from oidc_provider.lib.endpoints.token import * -from oidc_provider.lib.errors import * +from oidc_provider.lib.endpoints.authorize import AuthorizeEndpoint +from oidc_provider.lib.endpoints.token import TokenEndpoint +from oidc_provider.lib.errors import ( + AuthorizeError, + ClientIdError, + RedirectUriError, + TokenError, +) from oidc_provider.lib.utils.common import redirect, get_site_url, get_issuer from oidc_provider.lib.utils.oauth2 import protected_resource_view from oidc_provider.models import RESPONSE_TYPE_CHOICES, RSAKey From 84e80df3a8c85e57cfef2d00f5391d1fef1f3dd5 Mon Sep 17 00:00:00 2001 From: Graham Ullrich Date: Thu, 11 Aug 2016 16:19:10 -0600 Subject: [PATCH 22/29] Allow test suite specification --- runtests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/runtests.py b/runtests.py index 388ce99..1557853 100644 --- a/runtests.py +++ b/runtests.py @@ -98,11 +98,13 @@ def runtests(*test_args): try: from django.test.runner import DiscoverRunner runner_class = DiscoverRunner - test_args = ["oidc_provider.tests"] + if not test_args: + test_args = ["oidc_provider.tests"] except ImportError: from django.test.simple import DjangoTestSuiteRunner runner_class = DjangoTestSuiteRunner - test_args = ["tests"] + if not test_args: + test_args = ["tests"] failures = runner_class(verbosity=1, interactive=True, failfast=False).run_tests(test_args) sys.exit(failures) From 2c1d58247598d09592863cabbc9fb8ab12d6dba6 Mon Sep 17 00:00:00 2001 From: Florent Jouatte Date: Wed, 17 Aug 2016 12:13:33 +0200 Subject: [PATCH 23/29] #113: omit claim when empty --- oidc_provider/lib/claims.py | 5 +++- oidc_provider/tests/test_claims.py | 46 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 oidc_provider/tests/test_claims.py diff --git a/oidc_provider/lib/claims.py b/oidc_provider/lib/claims.py index 4330ee3..8c8263a 100644 --- a/oidc_provider/lib/claims.py +++ b/oidc_provider/lib/claims.py @@ -60,8 +60,11 @@ class ScopeClaims(object): if value is None or value == '': del aux_dic[key] elif type(value) is dict: + cleaned_dic = self._clean_dic(value) + if not cleaned_dic: + del aux_dic[key] + continue aux_dic[key] = self._clean_dic(value) - return aux_dic @classmethod diff --git a/oidc_provider/tests/test_claims.py b/oidc_provider/tests/test_claims.py new file mode 100644 index 0000000..92429b3 --- /dev/null +++ b/oidc_provider/tests/test_claims.py @@ -0,0 +1,46 @@ +from django.test import TestCase +from oidc_provider.lib.claims import ScopeClaims +from oidc_provider.tests.app.utils import create_fake_user + + +class ClaimsTestCase(TestCase): + + def setUp(self): + self.user = create_fake_user() + self.scopes = ['openid', 'address', 'email', 'phone', 'profile'] + self.scopeClaims = ScopeClaims(self.user, self.scopes) + + def test_clean_dic(self): + """ assert that _clean_dic function returns a clean dictionnary + (no empty claims) """ + dict_to_clean = { + 'phone_number_verified': '', + 'middle_name': '', + 'name': 'John Doe', + 'website': '', + 'profile': '', + 'family_name': 'Doe', + 'birthdate': '', + 'preferred_username': '', + 'picture': '', + 'zoneinfo': '', + 'locale': '', + 'gender': '', + 'updated_at': '', + 'address': {}, + 'given_name': 'John', + 'email_verified': '', + 'nickname': '', + 'email': u'johndoe@example.com', + 'phone_number': '', + } + clean_dict = self.scopeClaims._clean_dic(dict_to_clean) + self.assertEquals( + clean_dict, + { + 'family_name': 'Doe', + 'given_name': 'John', + 'name': 'John Doe', + 'email': u'johndoe@example.com' + } + ) From 2872d2e10b880d5f37788ecf0c5d54d1cf721cbf Mon Sep 17 00:00:00 2001 From: Florent Jouatte Date: Wed, 17 Aug 2016 12:24:00 +0200 Subject: [PATCH 24/29] #113: tiny improvement --- oidc_provider/lib/claims.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/oidc_provider/lib/claims.py b/oidc_provider/lib/claims.py index 8c8263a..16e3919 100644 --- a/oidc_provider/lib/claims.py +++ b/oidc_provider/lib/claims.py @@ -60,11 +60,11 @@ class ScopeClaims(object): if value is None or value == '': del aux_dic[key] elif type(value) is dict: - cleaned_dic = self._clean_dic(value) - if not cleaned_dic: + cleaned_dict = self._clean_dic(value) + if not cleaned_dict: del aux_dic[key] continue - aux_dic[key] = self._clean_dic(value) + aux_dic[key] = cleaned_dict return aux_dic @classmethod From d6a59b2a8847fe92acd904410bdfc1455402ed4a Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Wed, 17 Aug 2016 11:35:01 -0300 Subject: [PATCH 25/29] Improve docs. --- docs/sections/contribute.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sections/contribute.rst b/docs/sections/contribute.rst index d5f01af..7f3cb27 100644 --- a/docs/sections/contribute.rst +++ b/docs/sections/contribute.rst @@ -21,6 +21,9 @@ Use `tox `_ for running tests in each of the e # Run with Python 2.7 and Django 1.9. $ tox -e py27-django19 + # Run single test file. + $ python runtests.py oidc_provider.tests.test_authorize_endpoint + If you have a Django project properly configured with the package. Then just run tests as normal:: $ python manage.py test --settings oidc_provider.tests.app.settings oidc_provider From 9bf0c44fb43b04557bba12f2f87feaea14ac876a Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Tue, 30 Aug 2016 12:46:44 -0300 Subject: [PATCH 26/29] Edit CHANGELOG and fix docs. --- CHANGELOG.md | 3 +++ docs/sections/contribute.rst | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eed2931..c8f8a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ All notable changes to this project will be documented in this file. - Initial translation files (ES, FR). - Support for at_hash parameter. +##### Fixed +- Empty address dict in userinfo response. + ### [0.3.6] - 2016-07-07 ##### Changed diff --git a/docs/sections/contribute.rst b/docs/sections/contribute.rst index 7f3cb27..f7a07e9 100644 --- a/docs/sections/contribute.rst +++ b/docs/sections/contribute.rst @@ -24,10 +24,6 @@ Use `tox `_ for running tests in each of the e # Run single test file. $ python runtests.py oidc_provider.tests.test_authorize_endpoint -If you have a Django project properly configured with the package. Then just run tests as normal:: - - $ python manage.py test --settings oidc_provider.tests.app.settings oidc_provider - Also tests run on every commit to the project, we use `travis `_ for this. Improve Documentation From cfb3c0a1b2447d8d5772857c4dcdb96fed3be309 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Wed, 31 Aug 2016 16:57:41 -0300 Subject: [PATCH 27/29] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8f8a26..a45c483 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.7] - 2016-08-31 + ##### Added - Support for Django 1.10. - Initial translation files (ES, FR). From bb6fa54cf2f21c0dfe1efbf31c3f10d5284f0242 Mon Sep 17 00:00:00 2001 From: Ignacio Fiorentino Date: Fri, 2 Sep 2016 12:05:46 -0300 Subject: [PATCH 28/29] Change theme. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index bb4f14f..e3760f6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -106,7 +106,7 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 238a2eaaf5a1c5522fa5e846fbf8d3c0e9a1c7ae Mon Sep 17 00:00:00 2001 From: Juan Ignacio Fiorentino Date: Fri, 2 Sep 2016 15:29:22 -0300 Subject: [PATCH 29/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 343b7cb..79a04c3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Python Versions](https://img.shields.io/pypi/pyversions/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider) [![PyPI Versions](https://img.shields.io/pypi/v/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider) -[![Travis](https://travis-ci.org/juanifioren/django-oidc-provider.svg?branch=master)](https://travis-ci.org/juanifioren/django-oidc-provider) +[![Travis](https://travis-ci.org/juanifioren/django-oidc-provider.svg?branch=develop)](https://travis-ci.org/juanifioren/django-oidc-provider) [![PyPI Downloads](https://img.shields.io/pypi/dm/django-oidc-provider.svg)](https://pypi.python.org/pypi/django-oidc-provider) ## About OpenID