From 80441aaf3272b0f174a7b3cdda2f9d361e2c6014 Mon Sep 17 00:00:00 2001 From: Bono Date: Fri, 18 Nov 2016 18:08:37 +0100 Subject: [PATCH 01/31] Log create_uri_response exceptions to logger.exception --- oidc_provider/lib/endpoints/authorize.py | 2 +- .../tests/test_authorize_endpoint.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 4be87c0..d2a805c 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -207,7 +207,7 @@ class AuthorizeEndpoint(object): query_fragment['session_state'] = session_state except Exception as error: - logger.debug('[Authorize] Error when trying to create response uri: %s', error) + logger.exception('[Authorize] Error when trying to create response uri: %s', error) raise AuthorizeError(self.params['redirect_uri'], 'server_error', self.grant_type) uri = uri._replace(query=urlencode(query_params, doseq=True)) diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 030d50b..80aab38 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -7,6 +7,7 @@ try: except ImportError: from urlparse import parse_qs, urlsplit import uuid +from mock import patch from django.contrib.auth.models import AnonymousUser from django.core.management import call_command @@ -26,6 +27,7 @@ from oidc_provider.tests.app.utils import ( is_code_valid, ) from oidc_provider.views import AuthorizeView +from oidc_provider.lib.endpoints.authorize import AuthorizeEndpoint class AuthorizeEndpointMixin(object): @@ -498,3 +500,40 @@ class AuthorizationHybridFlowTestCase(TestCase, AuthorizeEndpointMixin): response = self._auth_request('post', self.data, is_user_authenticated=True) self.assertIn('expires_in=36000', response['Location']) + + +class TestCreateResponseURI(TestCase): + def setUp(self): + url = reverse('oidc_provider:authorize') + user = create_fake_user() + client = create_fake_client(response_type='code', is_public=True) + + # Base data to create a uri response + data = { + 'client_id': client.client_id, + 'redirect_uri': client.default_redirect_uri, + 'response_type': client.response_type, + } + + factory = RequestFactory() + self.request = factory.post(url, data=data) + self.request.user = user + + @patch('oidc_provider.lib.endpoints.authorize.create_code') + @patch('oidc_provider.lib.endpoints.authorize.logger.exception') + def test_create_response_uri_logs_to_error(self, log_exception, create_code): + """ + A lot can go wrong when creating a response uri and this is caught with a general Exception error. The + information contained within this error should show up in the error log so production servers have something + to work with when things don't work as expected. + """ + exception = Exception("Something went wrong!") + create_code.side_effect = exception + + authorization_endpoint = AuthorizeEndpoint(self.request) + authorization_endpoint.validate_params() + + with self.assertRaises(Exception): + authorization_endpoint.create_response_uri() + + log_exception.assert_called_once_with('[Authorize] Error when trying to create response uri: %s', exception) From a58e54d55fbbef7525d2587b90065228d756d03b Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Thu, 1 Dec 2016 15:49:25 +0100 Subject: [PATCH 02/31] Support grant type password - basics --- oidc_provider/lib/endpoints/token.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 0bf52da..cf1e10c 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -27,12 +27,10 @@ from oidc_provider.models import ( ) from oidc_provider import settings - logger = logging.getLogger(__name__) class TokenEndpoint(object): - def __init__(self, request): self.request = request self.params = {} @@ -53,6 +51,9 @@ class TokenEndpoint(object): # PKCE parameter. self.params['code_verifier'] = self.request.POST.get('code_verifier') + self.params['username'] = self.request.POST.get('username', '') + self.params['password'] = self.request.POST.get('password', '') + def _extract_client_auth(self): """ Get client credentials using HTTP Basic Authentication method. @@ -120,6 +121,25 @@ class TokenEndpoint(object): if not (new_code_challenge == self.code.code_challenge): raise TokenError('invalid_grant') + elif self.params['grant_type'] == 'password': + from django.contrib.auth import authenticate + user = authenticate(username=self.params['username'], password=self.params['password']) + if not user: + raise TokenError('Invalid user credentials') + + self.token = create_token(user, self.client, self.params['scope'].split(' ')) + + self.token.id_token = create_id_token( + user=user, + aud=self.client.client_id, + nonce='self.code.nonce', + at_hash=self.token.at_hash, + request=self.request, + scope=self.params['scope'], + ) + + self.token.save() + elif self.params['grant_type'] == 'refresh_token': if not self.params['refresh_token']: logger.debug('[Token] Missing refresh token') @@ -142,6 +162,8 @@ class TokenEndpoint(object): return self.create_code_response_dic() elif self.params['grant_type'] == 'refresh_token': return self.create_refresh_response_dic() + elif self.params['grant_type'] == 'password': + return {'access_token': self.token.access_token} def create_code_response_dic(self): token = create_token( From 2e0072cad7e5c7de2cc7d1f5ec6be398f0f35d19 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Wed, 7 Dec 2016 10:05:33 +0100 Subject: [PATCH 03/31] Add tests for Resource Owner Password Credentials Flow --- oidc_provider/tests/test_token_endpoint.py | 86 ++++++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index de7972e..50f556e 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -51,6 +51,14 @@ class TokenTestCase(TestCase): self.user = create_fake_user() self.client = create_fake_client(response_type='code') + def _password_grant_post_data(self): + return { + 'username': 'johndoe', + 'password': '1234', + 'grant_type': 'password', + 'scope': 'openid email', + } + def _auth_code_post_data(self, code): """ All the data that will be POSTed to the Token Endpoint. @@ -128,6 +136,77 @@ class TokenTestCase(TestCase): return userinfo(request) + def _auth_header(self): + user_pass = self.client.client_id + ':' + self.client.client_secret + auth = b'Basic ' + b64encode(user_pass.encode('utf-8')) + auth_header = {'HTTP_AUTHORIZATION': auth.decode('utf-8')} + return auth_header + + # Resource Owner Password Credentials Grant + # requirements to satisfy in all test_password_grant methods + # https://tools.ietf.org/html/rfc6749#section-4.3.2 + # + # grant_type + # REQUIRED. Value MUST be set to "password". + # username + # REQUIRED. The resource owner username. + # password + # REQUIRED. The resource owner password. + # scope + # OPTIONAL. The scope of the access request as described by + # Section 3.3. + # + # The authorization server MUST: + # o require client authentication for confidential clients or for any + # client that was issued client credentials (or with other + # authentication requirements), + # o authenticate the client if client authentication is included, and + # o validate the resource owner password credentials using its + # existing password validation algorithm. + + def test_password_grant_get_access_token_without_scope(self): + post_data = self._password_grant_post_data() + del (post_data['scope']) + + response = self._post_request( + post_data=post_data, + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + self.assertIn('access_token', response_dict) + + def test_password_grant_get_access_token_with_scope(self): + response = self._post_request( + post_data=self._password_grant_post_data(), + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + self.assertIn('access_token', response_dict) + + def test_password_grant_get_access_token_invalid_user_credentials(self): + invalid_post = self._password_grant_post_data() + invalid_post['password'] = 'wrong!' + + response = self._post_request( + post_data=invalid_post, + extras=self._auth_header() + ) + + self.assertEqual(400, response.status_code) + + def test_password_grant_get_access_token_invalid_client_credentials(self): + self.client.client_id = 'foo' + self.client.client_secret = 'bar' + + response = self._post_request( + post_data=self._password_grant_post_data(), + extras=self._auth_header() + ) + + self.assertEqual(400, response.status_code) + @override_settings(OIDC_TOKEN_EXPIRE=720) def test_authorization_code(self): """ @@ -307,12 +386,7 @@ class TokenTestCase(TestCase): del basicauth_data['client_id'] del basicauth_data['client_secret'] - # Generate HTTP Basic Auth header with id and secret. - 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'), - }) + response = self._post_request(basicauth_data, self._auth_header()) response.content.decode('utf-8') self.assertEqual('invalid_client' in response.content.decode('utf-8'), From 0633b664a017a00815ee06933e0bb17314bb2316 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Wed, 7 Dec 2016 11:51:24 +0100 Subject: [PATCH 04/31] Password Grant -Response according to specification --- oidc_provider/lib/endpoints/token.py | 48 ++++++++++++++-------- oidc_provider/tests/test_token_endpoint.py | 25 ++++++++++- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index cf1e10c..327ed81 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -2,7 +2,7 @@ from base64 import b64decode, urlsafe_b64encode import hashlib import logging import re - +from django.contrib.auth import authenticate from oidc_provider.lib.utils.common import cleanup_url_from_query_string try: @@ -34,6 +34,7 @@ class TokenEndpoint(object): def __init__(self, request): self.request = request self.params = {} + self.user = None self._extract_params() def _extract_params(self): @@ -122,23 +123,15 @@ class TokenEndpoint(object): raise TokenError('invalid_grant') elif self.params['grant_type'] == 'password': - from django.contrib.auth import authenticate - user = authenticate(username=self.params['username'], password=self.params['password']) + user = authenticate( + username=self.params['username'], + password=self.params['password'] + ) + if not user: raise TokenError('Invalid user credentials') - self.token = create_token(user, self.client, self.params['scope'].split(' ')) - - self.token.id_token = create_id_token( - user=user, - aud=self.client.client_id, - nonce='self.code.nonce', - at_hash=self.token.at_hash, - request=self.request, - scope=self.params['scope'], - ) - - self.token.save() + self.user = user elif self.params['grant_type'] == 'refresh_token': if not self.params['refresh_token']: @@ -163,7 +156,30 @@ class TokenEndpoint(object): elif self.params['grant_type'] == 'refresh_token': return self.create_refresh_response_dic() elif self.params['grant_type'] == 'password': - return {'access_token': self.token.access_token} + return self.create_access_token_response_dic() + + def create_access_token_response_dic(self): + token = create_token( + self.user, + self.client, + self.params['scope'].split(' ')) + + token.id_token = create_id_token( + user=self.user, + aud=self.client.client_id, + nonce='self.code.nonce', + at_hash=token.at_hash, + request=self.request, + scope=self.params['scope'], + ) + + token.save() + return { + 'access_token': token.access_token, + 'refresh_token': token.refresh_token, + 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), + 'token_type': 'bearer' + } def create_code_response_dic(self): token = create_token( diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 50f556e..1da2c07 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -19,7 +19,7 @@ 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 mock import patch, Mock from oidc_provider.lib.utils.token import create_code from oidc_provider.models import Token @@ -207,6 +207,29 @@ class TokenTestCase(TestCase): self.assertEqual(400, response.status_code) + @patch('oidc_provider.lib.utils.token.uuid') + @override_settings(OIDC_TOKEN_EXPIRE=120) + def test_password_grant_full_response(self, mock_uuid): + test_hex = 'fake_token' + mock_uuid4 = Mock(spec=uuid.uuid4) + mock_uuid4.hex = test_hex + mock_uuid.uuid4.return_value = mock_uuid4 + + response = self._post_request( + post_data=self._password_grant_post_data(), + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + expected_response_dic = { + "access_token": 'fake_token', + "refresh_token": 'fake_token', + "expires_in": 120, + "token_type": "bearer", + } + + self.assertDictEqual(expected_response_dic, response_dict) + @override_settings(OIDC_TOKEN_EXPIRE=720) def test_authorization_code(self): """ From 39111a83884ed5b5cad4878e787e156918bb89f9 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Tue, 13 Dec 2016 13:40:14 +0100 Subject: [PATCH 05/31] Better tests for errors, disable grant type password by default --- oidc_provider/lib/endpoints/token.py | 6 ++++- oidc_provider/lib/errors.py | 15 +++++++++++++ oidc_provider/settings.py | 16 +++++++++++++ oidc_provider/tests/test_token_endpoint.py | 26 +++++++++++++++++++++- oidc_provider/views.py | 4 ++-- 5 files changed, 63 insertions(+), 4 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 327ed81..5503b4c 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -14,6 +14,7 @@ from django.http import JsonResponse from oidc_provider.lib.errors import ( TokenError, + UserAuthError, ) from oidc_provider.lib.utils.token import ( create_id_token, @@ -123,13 +124,16 @@ class TokenEndpoint(object): raise TokenError('invalid_grant') elif self.params['grant_type'] == 'password': + if not settings.get('OIDC_GRANT_TYPE_PASSWORD_ENABLE'): + raise TokenError('unsupported_grant_type') + user = authenticate( username=self.params['username'], password=self.params['password'] ) if not user: - raise TokenError('Invalid user credentials') + raise UserAuthError() self.user = user diff --git a/oidc_provider/lib/errors.py b/oidc_provider/lib/errors.py index ce84811..47f4b10 100644 --- a/oidc_provider/lib/errors.py +++ b/oidc_provider/lib/errors.py @@ -16,6 +16,21 @@ class ClientIdError(Exception): description = 'The client identifier (client_id) is missing or invalid.' +class UserAuthError(Exception): + """ + Specific to the Resource Owner Password Credentials flow when + the Resource Owners credentials are not valid. + """ + error = 'access_denied' + description = 'The resource owner or authorization server denied ' \ + 'the request' + + def create_dict(self): + return { + 'error': self.error, + 'error_description': self.description, + } + class AuthorizeError(Exception): _errors = { diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 610534c..c277d3c 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -115,6 +115,22 @@ class DefaultSettings(object): """ return 'oidc_provider.lib.utils.common.default_idtoken_processing_hook' + @property + def OIDC_GRANT_TYPE_PASSWORD_ENABLE(self): + """ + OPTIONAL. A boolean to set whether to allow the Resource Owner Password + Credentials Grant. https://tools.ietf.org/html/rfc6749#section-4.3 + + From the specification: + Since this access token request utilizes the resource owner's + password, the authorization server MUST protect the endpoint + against brute force attacks (e.g., using rate-limitation or + generating alerts). + + How you do this, is up to you. + """ + return False + default_settings = DefaultSettings() diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 1da2c07..8a3ca22 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -164,6 +164,20 @@ class TokenTestCase(TestCase): # o validate the resource owner password credentials using its # existing password validation algorithm. + def test_default_setting_does_not_allow_grant_type_password(self): + post_data = self._password_grant_post_data() + + response = self._post_request( + post_data=post_data, + extras=self._auth_header() + ) + + response_dict = json.loads(response.content.decode('utf-8')) + + self.assertEqual(400, response.status_code) + self.assertEqual('unsupported_grant_type', response_dict['error']) + + @override_settings(OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_get_access_token_without_scope(self): post_data = self._password_grant_post_data() del (post_data['scope']) @@ -176,6 +190,7 @@ class TokenTestCase(TestCase): response_dict = json.loads(response.content.decode('utf-8')) self.assertIn('access_token', response_dict) + @override_settings(OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_get_access_token_with_scope(self): response = self._post_request( post_data=self._password_grant_post_data(), @@ -185,6 +200,7 @@ class TokenTestCase(TestCase): response_dict = json.loads(response.content.decode('utf-8')) self.assertIn('access_token', response_dict) + @override_settings(OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_get_access_token_invalid_user_credentials(self): invalid_post = self._password_grant_post_data() invalid_post['password'] = 'wrong!' @@ -194,7 +210,11 @@ class TokenTestCase(TestCase): extras=self._auth_header() ) + response_dict = json.loads(response.content.decode('utf-8')) + print(response_dict) + self.assertEqual(400, response.status_code) + self.assertEqual('access_denied', response_dict['error']) def test_password_grant_get_access_token_invalid_client_credentials(self): self.client.client_id = 'foo' @@ -205,10 +225,14 @@ class TokenTestCase(TestCase): extras=self._auth_header() ) + response_dict = json.loads(response.content.decode('utf-8')) + self.assertEqual(400, response.status_code) + self.assertEqual('invalid_client', response_dict['error']) @patch('oidc_provider.lib.utils.token.uuid') - @override_settings(OIDC_TOKEN_EXPIRE=120) + @override_settings(OIDC_TOKEN_EXPIRE=120, + OIDC_GRANT_TYPE_PASSWORD_ENABLE=True) def test_password_grant_full_response(self, mock_uuid): test_hex = 'fake_token' mock_uuid4 = Mock(spec=uuid.uuid4) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 6bf142f..1c94d05 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -28,7 +28,7 @@ from oidc_provider.lib.errors import ( ClientIdError, RedirectUriError, TokenError, -) + UserAuthError) from oidc_provider.lib.utils.common import ( redirect, get_site_url, @@ -167,7 +167,7 @@ class TokenView(View): return TokenEndpoint.response(dic) - except (TokenError) as error: + except (TokenError, UserAuthError) as error: return TokenEndpoint.response(error.create_dict(), status=400) From f7908bb1b9018a710dc0613691bb479b4e26b4e6 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Tue, 13 Dec 2016 14:13:57 +0100 Subject: [PATCH 06/31] Add documentation for grant type password --- docs/index.rst | 2 ++ docs/sections/settings.rst | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index cb4816a..1a15854 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Also implements the following specifications: * `OpenID Connect Discovery 1.0 `_ * `OpenID Connect Session Management 1.0 `_ * `OAuth 2.0 for Native Apps `_ +* `OAuth 2.0 Resource Owner Password Credentials Grant `_ * `Proof Key for Code Exchange by OAuth Public Clients `_ -------------------------------------------------------------------------------- @@ -16,6 +17,7 @@ Before getting started there are some important things that you should know: * Despite that implementation MUST support TLS. You can make request without using SSL. There is no control on that. * Supports only for requesting Claims using Scope values. +* If you enable the Resource Owner Password Credentials Grant, you MUST implement protection against brute force attacks on the token endpoint -------------------------------------------------------------------------------- diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index bba57d7..2a6c85b 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -147,3 +147,20 @@ Example usage:: .. note:: Please **DO NOT** add extra keys or delete the existing ones in the ``claims`` dict. If you want to add extra claims to some scopes you can use the ``OIDC_EXTRA_SCOPE_CLAIMS`` setting. + +OIDC_GRANT_TYPE_PASSWORD_ENABLE +=============================== +OPTIONAL. A boolean to set whether to allow the Resource Owner Password +Credentials Grant. https://tools.ietf.org/html/rfc6749#section-4.3 + +.. important:: + From the specification: + "Since this access token request utilizes the resource owner's + password, the authorization server **MUST** protect the endpoint + against brute force attacks (e.g., using rate-limitation or + generating alerts)." + + There are many ways to implement brute force attack prevention. We cannot + decide what works best for you, so you will have to implement a solution for + this that suits your needs. + From 1a31bc65548a25a59f297dc696c2ce0acc8ebbf5 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Wed, 21 Dec 2016 15:58:05 +0100 Subject: [PATCH 07/31] User authentication failure to return 403 --- oidc_provider/tests/test_token_endpoint.py | 1 + oidc_provider/views.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 8a3ca22..72c1080 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -214,6 +214,7 @@ class TokenTestCase(TestCase): print(response_dict) self.assertEqual(400, response.status_code) + self.assertEqual(403, response.status_code) self.assertEqual('access_denied', response_dict['error']) def test_password_grant_get_access_token_invalid_client_credentials(self): diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 1c94d05..b1eb420 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -167,8 +167,10 @@ class TokenView(View): return TokenEndpoint.response(dic) - except (TokenError, UserAuthError) as error: + except TokenError as error: return TokenEndpoint.response(error.create_dict(), status=400) + except UserAuthError as error: + return TokenEndpoint.response(error.create_dict(), status=403) @require_http_methods(['GET', 'POST']) From 7b9f08c46c5ac9ea5a686ff7348b87671e244557 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Wed, 21 Dec 2016 15:58:37 +0100 Subject: [PATCH 08/31] Add id_token to response --- oidc_provider/lib/endpoints/token.py | 7 +++++-- oidc_provider/tests/test_token_endpoint.py | 16 ++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index 5503b4c..cacebd9 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -168,7 +168,7 @@ class TokenEndpoint(object): self.client, self.params['scope'].split(' ')) - token.id_token = create_id_token( + id_token_dic = create_id_token( user=self.user, aud=self.client.client_id, nonce='self.code.nonce', @@ -177,12 +177,15 @@ class TokenEndpoint(object): scope=self.params['scope'], ) + token.id_token = id_token_dic token.save() + return { 'access_token': token.access_token, 'refresh_token': token.refresh_token, 'expires_in': settings.get('OIDC_TOKEN_EXPIRE'), - 'token_type': 'bearer' + 'token_type': 'bearer', + 'id_token': encode_id_token(id_token_dic, token.client), } def create_code_response_dic(self): diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 72c1080..9c5743e 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -213,7 +213,6 @@ class TokenTestCase(TestCase): response_dict = json.loads(response.content.decode('utf-8')) print(response_dict) - self.assertEqual(400, response.status_code) self.assertEqual(403, response.status_code) self.assertEqual('access_denied', response_dict['error']) @@ -246,14 +245,15 @@ class TokenTestCase(TestCase): ) response_dict = json.loads(response.content.decode('utf-8')) - expected_response_dic = { - "access_token": 'fake_token', - "refresh_token": 'fake_token', - "expires_in": 120, - "token_type": "bearer", - } + id_token = JWS().verify_compact(response_dict['id_token'].encode('utf-8'), self._get_keys()) + print(id_token) - self.assertDictEqual(expected_response_dic, response_dict) + self.assertEqual(response_dict['access_token'], 'fake_token') + self.assertEqual(response_dict['refresh_token'], 'fake_token') + self.assertEqual(response_dict['expires_in'], 120) + self.assertEqual(response_dict['token_type'], 'bearer') + self.assertEqual(id_token['sub'], str(self.user.id)) + self.assertEqual(id_token['aud'], self.client.client_id); @override_settings(OIDC_TOKEN_EXPIRE=720) def test_authorization_code(self): From d9d9bffdd1014f22ce9863a4d642c5893b709e09 Mon Sep 17 00:00:00 2001 From: Brian Richter Date: Fri, 6 Jan 2017 11:20:40 -0800 Subject: [PATCH 09/31] skipping consent only works for confidential clients --- docs/sections/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index bba57d7..8c1fba2 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -101,7 +101,7 @@ Default is ``False``. OIDC_SKIP_CONSENT_ALWAYS ======================== -OPTIONAL. ``bool``. If enabled, the Server will NEVER ask the user for consent. +OPTIONAL. ``bool``. If enabled, the Server will NEVER ask the user for consent if the client is confidential. Default is ``False``. From 78845a7b9dd146fd18bbdd010ecae3a05620f961 Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Wed, 25 Jan 2017 11:02:54 +0100 Subject: [PATCH 10/31] fix URI fragment example not working URL `http://localhost:8100/#/auth/callback/` --- oidc_provider/lib/endpoints/authorize.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index 58be181..698d87e 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -112,7 +112,7 @@ class AuthorizeEndpoint(object): def create_response_uri(self): uri = urlsplit(self.params['redirect_uri']) query_params = parse_qs(uri.query) - query_fragment = parse_qs(uri.fragment) + query_fragment = {} try: if self.grant_type in ['authorization_code', 'hybrid']: @@ -177,8 +177,7 @@ class AuthorizeEndpoint(object): logger.debug('[Authorize] Error when trying to create response uri: %s', error) raise AuthorizeError(self.params['redirect_uri'], 'server_error', self.grant_type) - uri = uri._replace(query=urlencode(query_params, doseq=True)) - uri = uri._replace(fragment=urlencode(query_fragment, doseq=True)) + uri = uri._replace(query=urlencode(query_params, doseq=True), fragment=uri.fragment + urlencode(query_fragment, doseq=True)) return urlunsplit(uri) From 7496b2f0360ce6db42eb408888b6fbc96ab2b4f0 Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Fri, 27 Jan 2017 09:05:25 +0100 Subject: [PATCH 11/31] OIDC_POST_END_SESSION_HOOK + tests --- oidc_provider/lib/utils/common.py | 27 +++++++++++++++++++ oidc_provider/settings.py | 8 ++++++ .../tests/test_end_session_endpoint.py | 8 ++++++ oidc_provider/views.py | 11 ++++++++ 4 files changed, 54 insertions(+) diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index b7a6676..52bb962 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -84,6 +84,33 @@ def default_after_userlogin_hook(request, user, client): """ return None + +def default_post_end_session_hook(request, id_token=None, post_logout_redirect_uri=None, state=None, client=None, next_page=None): + """ + Default function for setting OIDC_POST_END_SESSION_HOOK. + + :param request: Django request object + :type request: django.http.HttpRequest + + :param id_token: token passed by `id_token_hint` url query param - do NOT trust this param or validate token + :type id_token: str + + :param post_logout_redirect_uri: redirect url from url query param - do NOT trust this param + :type post_logout_redirect_uri: str + + :param state: state param from url query params + :type state: str + + :param client: If id_token has `aud` param and associated Client exists, this is an instance of it - do NOT trust this param + :type client: oidc_provider.models.Client + + :param next_page: calculated next_page redirection target + :type next_page: str + :return: + """ + return None + + def default_idtoken_processing_hook(id_token, user): """ Hook to perform some additional actions ti `id_token` dictionary just before serialization. diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 610534c..20f2f78 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -30,6 +30,14 @@ class DefaultSettings(object): """ return 'oidc_provider.lib.utils.common.default_after_userlogin_hook' + @property + def OIDC_POST_END_SESSION_HOOK(self): + """ + OPTIONAL. Provide a way to plug into the end session process just before calling + Django's logout function, typically to perform some business logic. + """ + return 'oidc_provider.lib.utils.common.default_post_end_session_hook' + @property def OIDC_CODE_EXPIRE(self): """ diff --git a/oidc_provider/tests/test_end_session_endpoint.py b/oidc_provider/tests/test_end_session_endpoint.py index 46d9cc0..6982704 100644 --- a/oidc_provider/tests/test_end_session_endpoint.py +++ b/oidc_provider/tests/test_end_session_endpoint.py @@ -11,6 +11,7 @@ from oidc_provider.tests.app.utils import ( create_fake_client, create_fake_user, ) +import mock class EndSessionTestCase(TestCase): @@ -44,3 +45,10 @@ class EndSessionTestCase(TestCase): response = self.client.get(self.url, query_params) self.assertRedirects(response, self.LOGOUT_URL, fetch_redirect_response=False) + + @mock.patch(settings.get('OIDC_POST_END_SESSION_HOOK')) + def test_call_post_end_session_hook(self, hook_function): + self.client.get(self.url) + self.assertTrue(hook_function.called, 'OIDC_POST_END_SESSION_HOOK should be called') + self.assertTrue(hook_function.call_count == 1, 'OIDC_POST_END_SESSION_HOOK should be called once but was {}'.format(hook_function.call_count)) + diff --git a/oidc_provider/views.py b/oidc_provider/views.py index 6bf142f..ffc60c4 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -264,8 +264,10 @@ class EndSessionView(View): id_token_hint = request.GET.get('id_token_hint', '') post_logout_redirect_uri = request.GET.get('post_logout_redirect_uri', '') state = request.GET.get('state', '') + client = None next_page = settings.get('LOGIN_URL') + post_end_session_hook = settings.get('OIDC_POST_END_SESSION_HOOK', import_str=True) if id_token_hint: client_id = client_id_from_id_token(id_token_hint) @@ -283,6 +285,15 @@ class EndSessionView(View): except Client.DoesNotExist: pass + post_end_session_hook( + request=request, + id_token=id_token_hint, + post_logout_redirect_uri=post_logout_redirect_uri, + state=state, + client=client, + next_page=next_page + ) + return logout(request, next_page=next_page) From ed3f9988aa2b5802d2dc0b8c6b105105a08ad391 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Tue, 28 Mar 2017 10:37:02 +0200 Subject: [PATCH 12/31] Explicit function naming --- oidc_provider/tests/test_token_endpoint.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 9c5743e..59b36c1 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -136,7 +136,7 @@ class TokenTestCase(TestCase): return userinfo(request) - def _auth_header(self): + def _password_grant_auth_header(self): user_pass = self.client.client_id + ':' + self.client.client_secret auth = b'Basic ' + b64encode(user_pass.encode('utf-8')) auth_header = {'HTTP_AUTHORIZATION': auth.decode('utf-8')} @@ -169,7 +169,7 @@ class TokenTestCase(TestCase): response = self._post_request( post_data=post_data, - extras=self._auth_header() + extras=self._password_grant_auth_header() ) response_dict = json.loads(response.content.decode('utf-8')) @@ -184,7 +184,7 @@ class TokenTestCase(TestCase): response = self._post_request( post_data=post_data, - extras=self._auth_header() + extras=self._password_grant_auth_header() ) response_dict = json.loads(response.content.decode('utf-8')) @@ -194,7 +194,7 @@ class TokenTestCase(TestCase): def test_password_grant_get_access_token_with_scope(self): response = self._post_request( post_data=self._password_grant_post_data(), - extras=self._auth_header() + extras=self._password_grant_auth_header() ) response_dict = json.loads(response.content.decode('utf-8')) @@ -207,7 +207,7 @@ class TokenTestCase(TestCase): response = self._post_request( post_data=invalid_post, - extras=self._auth_header() + extras=self._password_grant_auth_header() ) response_dict = json.loads(response.content.decode('utf-8')) @@ -222,7 +222,7 @@ class TokenTestCase(TestCase): response = self._post_request( post_data=self._password_grant_post_data(), - extras=self._auth_header() + extras=self._password_grant_auth_header() ) response_dict = json.loads(response.content.decode('utf-8')) @@ -241,7 +241,7 @@ class TokenTestCase(TestCase): response = self._post_request( post_data=self._password_grant_post_data(), - extras=self._auth_header() + extras=self._password_grant_auth_header() ) response_dict = json.loads(response.content.decode('utf-8')) @@ -434,7 +434,7 @@ class TokenTestCase(TestCase): del basicauth_data['client_id'] del basicauth_data['client_secret'] - response = self._post_request(basicauth_data, self._auth_header()) + response = self._post_request(basicauth_data, self._password_grant_auth_header()) response.content.decode('utf-8') self.assertEqual('invalid_client' in response.content.decode('utf-8'), From a6403581828dcd92ccb67264b5b017276a36fd14 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Tue, 28 Mar 2017 10:37:31 +0200 Subject: [PATCH 13/31] Remove print statements --- oidc_provider/tests/test_token_endpoint.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 59b36c1..35c4e16 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -211,7 +211,6 @@ class TokenTestCase(TestCase): ) response_dict = json.loads(response.content.decode('utf-8')) - print(response_dict) self.assertEqual(403, response.status_code) self.assertEqual('access_denied', response_dict['error']) @@ -246,7 +245,6 @@ class TokenTestCase(TestCase): response_dict = json.loads(response.content.decode('utf-8')) id_token = JWS().verify_compact(response_dict['id_token'].encode('utf-8'), self._get_keys()) - print(id_token) self.assertEqual(response_dict['access_token'], 'fake_token') self.assertEqual(response_dict['refresh_token'], 'fake_token') From 82dee87ebee1d0a166bddc891c86230e92901f53 Mon Sep 17 00:00:00 2001 From: Niels van Huijstee Date: Tue, 28 Mar 2017 10:38:43 +0200 Subject: [PATCH 14/31] No need for semicolons, this is Python --- oidc_provider/tests/test_token_endpoint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oidc_provider/tests/test_token_endpoint.py b/oidc_provider/tests/test_token_endpoint.py index 35c4e16..40c5aaf 100644 --- a/oidc_provider/tests/test_token_endpoint.py +++ b/oidc_provider/tests/test_token_endpoint.py @@ -251,7 +251,7 @@ class TokenTestCase(TestCase): self.assertEqual(response_dict['expires_in'], 120) self.assertEqual(response_dict['token_type'], 'bearer') self.assertEqual(id_token['sub'], str(self.user.id)) - self.assertEqual(id_token['aud'], self.client.client_id); + self.assertEqual(id_token['aud'], self.client.client_id) @override_settings(OIDC_TOKEN_EXPIRE=720) def test_authorization_code(self): @@ -276,7 +276,7 @@ class TokenTestCase(TestCase): self.assertEqual(response_dic['token_type'], 'bearer') self.assertEqual(response_dic['expires_in'], 720) self.assertEqual(id_token['sub'], str(self.user.id)) - self.assertEqual(id_token['aud'], self.client.client_id); + self.assertEqual(id_token['aud'], self.client.client_id) def test_refresh_token(self): """ From 2eca82a5b78bf8228dbbc1a726f5c276a650431e Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Tue, 28 Mar 2017 10:02:29 +0100 Subject: [PATCH 15/31] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b10bb..f687d32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,13 @@ All notable changes to this project will be documented in this file. ##### Added - Signals when user accept/decline the authorization page. +- `OIDC_POST_END_SESSION_HOOK` setting for additional bussiness logic +- Feature granttype password ##### Fixed - Timestamps with unixtime (instead of django timezone). - Field refresh_token cannot be primary key if null. +- `create_uri_exceptions` are now being logged at `Exception` level not `DEBUG` ### [0.4.4] - 2016-11-29 From 30bb06d4789099535588711654c8416a90f149ea Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Thu, 30 Mar 2017 21:45:14 +0100 Subject: [PATCH 16/31] fixed logger message --- oidc_provider/lib/endpoints/token.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oidc_provider/lib/endpoints/token.py b/oidc_provider/lib/endpoints/token.py index cacebd9..ae1eb98 100644 --- a/oidc_provider/lib/endpoints/token.py +++ b/oidc_provider/lib/endpoints/token.py @@ -106,8 +106,7 @@ class TokenEndpoint(object): if not (self.code.client == self.client) \ or self.code.has_expired(): - logger.debug('[Token] Invalid code: invalid client or code has expired', - self.params['redirect_uri']) + logger.debug('[Token] Invalid code: invalid client or code has expired') raise TokenError('invalid_grant') # Validate PKCE parameters. From 721342fcb2c3feaf3d5552680dd813943eb0d04f Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Thu, 30 Mar 2017 23:18:21 +0100 Subject: [PATCH 17/31] Improved `exp` value calculation --- oidc_provider/tests/test_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oidc_provider/tests/test_utils.py b/oidc_provider/tests/test_utils.py index fd09c40..16c987a 100644 --- a/oidc_provider/tests/test_utils.py +++ b/oidc_provider/tests/test_utils.py @@ -7,6 +7,7 @@ from django.utils import timezone from oidc_provider.lib.utils.common import get_issuer from oidc_provider.lib.utils.token import create_id_token from oidc_provider.tests.app.utils import create_fake_user +from django.test import override_settings class Request(object): @@ -59,6 +60,7 @@ class TokenTest(TestCase): def setUp(self): self.user = create_fake_user() + @override_settings(OIDC_IDTOKEN_EXPIRE=600) def test_create_id_token(self): start_time = int(time.time()) login_timestamp = start_time - 1234 From 9ddbdbf294b17fa833097d240e550910707f57cd Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Fri, 31 Mar 2017 13:00:24 +0100 Subject: [PATCH 18/31] rename OIDC_POST_END_SESSION_HOOK to OIDC_AFTER_END_SESSION_HOOK --- CHANGELOG.md | 2 +- oidc_provider/lib/utils/common.py | 4 ++-- oidc_provider/settings.py | 4 ++-- oidc_provider/tests/test_end_session_endpoint.py | 6 +++--- oidc_provider/views.py | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f687d32..8364258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ##### Added - Signals when user accept/decline the authorization page. -- `OIDC_POST_END_SESSION_HOOK` setting for additional bussiness logic +- `OIDC_AFTER_END_SESSION_HOOK` setting for additional business logic - Feature granttype password ##### Fixed diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index 52bb962..1a69deb 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -85,9 +85,9 @@ def default_after_userlogin_hook(request, user, client): return None -def default_post_end_session_hook(request, id_token=None, post_logout_redirect_uri=None, state=None, client=None, next_page=None): +def default_after_end_session_hook(request, id_token=None, post_logout_redirect_uri=None, state=None, client=None, next_page=None): """ - Default function for setting OIDC_POST_END_SESSION_HOOK. + Default function for setting OIDC_AFTER_END_SESSION_HOOK. :param request: Django request object :type request: django.http.HttpRequest diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index c6de458..3421d1b 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -31,12 +31,12 @@ class DefaultSettings(object): return 'oidc_provider.lib.utils.common.default_after_userlogin_hook' @property - def OIDC_POST_END_SESSION_HOOK(self): + def OIDC_AFTER_END_SESSION_HOOK(self): """ OPTIONAL. Provide a way to plug into the end session process just before calling Django's logout function, typically to perform some business logic. """ - return 'oidc_provider.lib.utils.common.default_post_end_session_hook' + return 'oidc_provider.lib.utils.common.default_after_end_session_hook' @property def OIDC_CODE_EXPIRE(self): diff --git a/oidc_provider/tests/test_end_session_endpoint.py b/oidc_provider/tests/test_end_session_endpoint.py index 6982704..89f0d8c 100644 --- a/oidc_provider/tests/test_end_session_endpoint.py +++ b/oidc_provider/tests/test_end_session_endpoint.py @@ -46,9 +46,9 @@ class EndSessionTestCase(TestCase): response = self.client.get(self.url, query_params) self.assertRedirects(response, self.LOGOUT_URL, fetch_redirect_response=False) - @mock.patch(settings.get('OIDC_POST_END_SESSION_HOOK')) + @mock.patch(settings.get('OIDC_AFTER_END_SESSION_HOOK')) def test_call_post_end_session_hook(self, hook_function): self.client.get(self.url) - self.assertTrue(hook_function.called, 'OIDC_POST_END_SESSION_HOOK should be called') - self.assertTrue(hook_function.call_count == 1, 'OIDC_POST_END_SESSION_HOOK should be called once but was {}'.format(hook_function.call_count)) + self.assertTrue(hook_function.called, 'OIDC_AFTER_END_SESSION_HOOK should be called') + self.assertTrue(hook_function.call_count == 1, 'OIDC_AFTER_END_SESSION_HOOK should be called once but was {}'.format(hook_function.call_count)) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index c2e8cb7..b2c4d80 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -269,7 +269,7 @@ class EndSessionView(View): client = None next_page = settings.get('LOGIN_URL') - post_end_session_hook = settings.get('OIDC_POST_END_SESSION_HOOK', import_str=True) + after_end_session_hook = settings.get('OIDC_AFTER_END_SESSION_HOOK', import_str=True) if id_token_hint: client_id = client_id_from_id_token(id_token_hint) @@ -287,7 +287,7 @@ class EndSessionView(View): except Client.DoesNotExist: pass - post_end_session_hook( + after_end_session_hook( request=request, id_token=id_token_hint, post_logout_redirect_uri=post_logout_redirect_uri, From ca98c33a7d61382410e1e702e0df82120e99f809 Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Fri, 31 Mar 2017 13:00:33 +0100 Subject: [PATCH 19/31] added docs for OIDC_AFTER_END_SESSION_HOOK --- docs/sections/settings.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 667157c..80480f1 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -36,6 +36,18 @@ Return ``None`` if you want to continue with the flow. The typical situation will be checking some state of the user or maybe redirect him somewhere. With request you have access to all OIDC parameters. Remember that if you redirect the user to another place then you need to take him back to the authorize endpoint (use ``request.get_full_path()`` as the value for a "next" parameter). +OIDC_AFTER_END_SESSION_HOOK +=========================== + +OPTIONAL. ``str``. A string with the location of your function. Provide a way to plug into the log out process just before calling Django's log out function, typically to perform some business logic. + +Default is:: + + def default_after_end_session_hook(request, id_token=None, post_logout_redirect_uri=None, state=None, client=None, next_page=None): + return None + +Return ``None`` if you want to continue with the flow. + OIDC_CODE_EXPIRE ================ From bddbb68614c29f666c340cbdcf5b46b27a5f4a94 Mon Sep 17 00:00:00 2001 From: kaveh Date: Fri, 31 Mar 2017 13:41:26 -0700 Subject: [PATCH 20/31] Replaces `LOGIN_URL` with `OIDC_LOGIN_URL` so users can use a different login path for their oidc requests. --- docs/sections/settings.rst | 6 +++--- oidc_provider/settings.py | 10 ++++------ oidc_provider/tests/test_authorize_endpoint.py | 2 +- oidc_provider/tests/test_end_session_endpoint.py | 2 +- oidc_provider/views.py | 6 +++--- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 80480f1..8e28b8c 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -5,12 +5,12 @@ Settings Customize your provider so fit your project needs. -LOGIN_URL +OIDC_LOGIN_URL ========= -REQUIRED. ``str``. Used to log the user in. `Read more in Django docs `_ +OPTIONAL. ``str``. Used to log the user in. By default Django's ``LOGIN_URL`` will be used. `Read more in Django docs `_ -``str``. Default is ``/accounts/login/``. +``str``. Default is ``/accounts/login/`` (Django's ``LOGIN_URL``). SITE_URL ======== diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 3421d1b..f25adf1 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -4,16 +4,14 @@ from django.conf import settings class DefaultSettings(object): - required_attrs = ( - 'LOGIN_URL', - ) + required_attrs = () @property - def LOGIN_URL(self): + def OIDC_LOGIN_URL(self): """ - REQUIRED. Used to log the user in. + REQUIRED. Used to log the user in. By default Django's LOGIN_URL will be used. """ - return None + return settings.LOGIN_URL @property def SITE_URL(self): diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index 80aab38..f691783 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -122,7 +122,7 @@ class AuthorizationCodeFlowTestCase(TestCase, AuthorizeEndpointMixin): response = self._auth_request('get', data) # Check if user was redirected to the login view. - self.assertIn(settings.get('LOGIN_URL'), response['Location']) + self.assertIn(settings.get('OIDC_LOGIN_URL'), response['Location']) def test_user_consent_inputs(self): """ diff --git a/oidc_provider/tests/test_end_session_endpoint.py b/oidc_provider/tests/test_end_session_endpoint.py index 89f0d8c..b416762 100644 --- a/oidc_provider/tests/test_end_session_endpoint.py +++ b/oidc_provider/tests/test_end_session_endpoint.py @@ -36,7 +36,7 @@ class EndSessionTestCase(TestCase): } response = self.client.get(self.url, query_params) # With no id_token the OP MUST NOT redirect to the requested redirect_uri. - self.assertRedirects(response, settings.get('LOGIN_URL'), fetch_redirect_response=False) + self.assertRedirects(response, settings.get('OIDC_LOGIN_URL'), fetch_redirect_response=False) id_token_dic = create_id_token(user=self.user, aud=self.oidc_client.client_id) id_token = encode_id_token(id_token_dic, self.oidc_client) diff --git a/oidc_provider/views.py b/oidc_provider/views.py index b2c4d80..a0afbbb 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -79,7 +79,7 @@ class AuthorizeView(View): raise AuthorizeError(authorize.params['redirect_uri'], 'interaction_required', authorize.grant_type) if authorize.params['prompt'] == 'login': - return redirect_to_login(request.get_full_path()) + return redirect_to_login(request.get_full_path(), settings.get('OIDC_LOGIN_URL')) if authorize.params['prompt'] == 'select_account': # TODO: see how we can support multiple accounts for the end-user. @@ -108,7 +108,7 @@ class AuthorizeView(View): if authorize.params['prompt'] == 'none': raise AuthorizeError(authorize.params['redirect_uri'], 'login_required', authorize.grant_type) - return redirect_to_login(request.get_full_path()) + return redirect_to_login(request.get_full_path(), settings.get('OIDC_LOGIN_URL')) except (ClientIdError, RedirectUriError) as error: context = { @@ -268,7 +268,7 @@ class EndSessionView(View): state = request.GET.get('state', '') client = None - next_page = settings.get('LOGIN_URL') + next_page = settings.get('OIDC_LOGIN_URL') after_end_session_hook = settings.get('OIDC_AFTER_END_SESSION_HOOK', import_str=True) if id_token_hint: From 41003e1e83f96826784372259bd88cff8e600f8d Mon Sep 17 00:00:00 2001 From: kaveh Date: Fri, 31 Mar 2017 14:25:42 -0700 Subject: [PATCH 21/31] Adds a setting variable for custom template paths --- docs/sections/settings.rst | 12 ++++++++++++ docs/sections/templates.rst | 3 +++ oidc_provider/settings.py | 18 ++++++++++++++++-- oidc_provider/views.py | 6 ++++-- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 80480f1..7893bb2 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -176,3 +176,15 @@ Credentials Grant. https://tools.ietf.org/html/rfc6749#section-4.3 decide what works best for you, so you will have to implement a solution for this that suits your needs. +OIDC_TEMPLATES +============== +OPTIONAL. A dictionary pointing to templates for authorize and error pages. +Default is:: + + { + 'authorize': 'oidc_provider/authorize.html', + 'error': 'oidc_provider/error.html' + } + +.. note:: + The templates that are not specified here will use the default ones. diff --git a/docs/sections/templates.rst b/docs/sections/templates.rst index 8047c2b..bd9cef5 100644 --- a/docs/sections/templates.rst +++ b/docs/sections/templates.rst @@ -33,3 +33,6 @@ You can copy the sample html here and edit them with your own styles.

{{ error }}

{{ description }}

+ +You can also customize paths to your custom templates by putting them in ``OIDC_TEMPLATES`` in the settings. + diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 3421d1b..25675bb 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -139,6 +139,13 @@ class DefaultSettings(object): """ return False + @property + def OIDC_TEMPLATES(self): + return { + 'authorize': 'oidc_provider/authorize.html', + 'error': 'oidc_provider/error.html' + } + default_settings = DefaultSettings() @@ -161,12 +168,19 @@ def get(name, import_str=False): Helper function to use inside the package. """ value = None + default_value = getattr(default_settings, name) + try: - value = getattr(default_settings, name) value = getattr(settings, name) except AttributeError: - if value is None and name in default_settings.required_attrs: + if name in default_settings.required_attrs: raise Exception('You must set ' + name + ' in your settings.') + finally: + if isinstance(default_value, dict) and value: + default_value.update(value) + value = default_value + + value = value or default_value value = import_from_str(value) if import_str else value diff --git a/oidc_provider/views.py b/oidc_provider/views.py index b2c4d80..888bc60 100644 --- a/oidc_provider/views.py +++ b/oidc_provider/views.py @@ -47,6 +47,8 @@ from oidc_provider import signals logger = logging.getLogger(__name__) +OIDC_TEMPLATES = settings.get('OIDC_TEMPLATES') + class AuthorizeView(View): @@ -103,7 +105,7 @@ class AuthorizeView(View): 'scopes': authorize.get_scopes_information(), } - return render(request, 'oidc_provider/authorize.html', context) + return render(request, OIDC_TEMPLATES['authorize'], context) else: if authorize.params['prompt'] == 'none': raise AuthorizeError(authorize.params['redirect_uri'], 'login_required', authorize.grant_type) @@ -116,7 +118,7 @@ class AuthorizeView(View): 'description': error.description, } - return render(request, 'oidc_provider/error.html', context) + return render(request, OIDC_TEMPLATES['error'], context) except (AuthorizeError) as error: uri = error.create_uri( From cca8c81c672b0b5d5656f51997289dd682cd9584 Mon Sep 17 00:00:00 2001 From: kaveh Date: Fri, 7 Apr 2017 16:54:31 -0700 Subject: [PATCH 22/31] Updates documentation --- docs/sections/settings.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 8e28b8c..c5fb449 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -6,7 +6,7 @@ Settings Customize your provider so fit your project needs. OIDC_LOGIN_URL -========= +============== OPTIONAL. ``str``. Used to log the user in. By default Django's ``LOGIN_URL`` will be used. `Read more in Django docs `_ From 959c7a09298ab474522ba82c8ee0b3811a9d2603 Mon Sep 17 00:00:00 2001 From: kaveh Date: Fri, 7 Apr 2017 16:59:40 -0700 Subject: [PATCH 23/31] Fixed bad try/except/finally block --- oidc_provider/settings.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index 25675bb..f358826 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -175,13 +175,12 @@ def get(name, import_str=False): except AttributeError: if name in default_settings.required_attrs: raise Exception('You must set ' + name + ' in your settings.') - finally: - if isinstance(default_value, dict) and value: - default_value.update(value) - value = default_value + if isinstance(default_value, dict) and value: + default_value.update(value) + value = default_value + else: value = value or default_value - - value = import_from_str(value) if import_str else value + value = import_from_str(value) if import_str else value return value From e090db2d6c0c6e350bac44d717c2938685731d23 Mon Sep 17 00:00:00 2001 From: kaveh Date: Tue, 11 Apr 2017 15:20:37 -0700 Subject: [PATCH 24/31] Adds test for OIDC_TEMPLATES settings --- docs/sections/settings.rst | 16 ++++++++++++++++ oidc_provider/tests/test_settings.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 oidc_provider/tests/test_settings.py diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 7893bb2..e60730e 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -186,5 +186,21 @@ Default is:: 'error': 'oidc_provider/error.html' } +The following contexts will be passed to the ``authorize`` and ``error`` templates respectively:: + + # For authorize template + { + 'client': 'an instance of Client for the auth request', + 'hidden_inputs': 'a rendered html with all the hidden inputs needed for AuthorizeEndpoint', + 'params': 'a dict containing the params in the auth request', + 'scopes': 'a list of scopes' + } + + # For error template + { + 'error': 'string stating the error', + 'description': 'string stating description of the error' + } + .. note:: The templates that are not specified here will use the default ones. diff --git a/oidc_provider/tests/test_settings.py b/oidc_provider/tests/test_settings.py new file mode 100644 index 0000000..db6f812 --- /dev/null +++ b/oidc_provider/tests/test_settings.py @@ -0,0 +1,15 @@ +from django.test import TestCase, override_settings + +from oidc_provider import settings + +CUSTOM_TEMPLATES = { + 'authorize': 'custom/authorize.html', + 'error': 'custom/error.html' +} + + +class TokenTest(TestCase): + + @override_settings(OIDC_TEMPLATES=CUSTOM_TEMPLATES) + def test_override_templates(self): + self.assertEqual(settings.get('OIDC_TEMPLATES'), CUSTOM_TEMPLATES) From 62a0a4867819425c544cd7a269e348891ce554a4 Mon Sep 17 00:00:00 2001 From: Gertjan Oude Lohuis Date: Tue, 18 Apr 2017 11:25:16 +0200 Subject: [PATCH 25/31] Determine value for op_browser_state from session_key or default --- oidc_provider/lib/utils/common.py | 11 ++++++++ oidc_provider/middleware.py | 12 ++++----- oidc_provider/settings.py | 17 ++++++++++++ oidc_provider/tests/test_middleware.py | 36 ++++++++++++++++++++++++++ oidc_provider/tests/test_settings.py | 12 ++++++++- oidc_provider/tests/test_utils.py | 24 ++++++++++++++--- 6 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 oidc_provider/tests/test_middleware.py diff --git a/oidc_provider/lib/utils/common.py b/oidc_provider/lib/utils/common.py index 1a69deb..c4778bd 100644 --- a/oidc_provider/lib/utils/common.py +++ b/oidc_provider/lib/utils/common.py @@ -1,3 +1,5 @@ +from hashlib import sha224 + from django.core.urlresolvers import reverse from django.http import HttpResponse @@ -50,6 +52,7 @@ def get_site_url(site_url=None, request=None): 'or set `SITE_URL` in settings, ' 'or pass `request` object.') + def get_issuer(site_url=None, request=None): """ Construct the issuer full url. Basically is the site url with some path @@ -125,3 +128,11 @@ def default_idtoken_processing_hook(id_token, user): :rtype dict """ return id_token + + +def get_browser_state_or_default(request): + """ + Determine value to use as session state. + """ + key = request.session.session_key or settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY') + return sha224(key.encode('utf-8')).hexdigest() diff --git a/oidc_provider/middleware.py b/oidc_provider/middleware.py index a359a95..06f4d82 100644 --- a/oidc_provider/middleware.py +++ b/oidc_provider/middleware.py @@ -1,17 +1,17 @@ -from hashlib import sha224 - -from django.conf import settings as django_settings from django.utils.deprecation import MiddlewareMixin +from oidc_provider import settings +from oidc_provider.lib.utils.common import get_browser_state_or_default + class SessionManagementMiddleware(MiddlewareMixin): """ Maintain a `op_browser_state` cookie along with the `sessionid` cookie that represents the End-User's login state at the OP. If the user is not logged - in then use `SECRET_KEY` value. + in then use the value of settings.OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY. """ def process_response(self, request, response): - session_state = sha224((request.session.session_key or django_settings.SECRET_KEY).encode('utf-8')).hexdigest() - response.set_cookie('op_browser_state', session_state) + if settings.get('OIDC_SESSION_MANAGEMENT_ENABLE'): + response.set_cookie('op_browser_state', get_browser_state_or_default(request)) return response diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index dd227a3..68c38e1 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -1,4 +1,6 @@ import importlib +import random +import string from django.conf import settings @@ -6,6 +8,9 @@ from django.conf import settings class DefaultSettings(object): required_attrs = () + def __init__(self): + self._unauthenticated_session_management_key = None + @property def OIDC_LOGIN_URL(self): """ @@ -74,6 +79,18 @@ class DefaultSettings(object): """ return False + @property + def OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY(self): + """ + OPTIONAL. Supply a fixed string to use as browser-state key for unauthenticated clients. + """ + + # Memoize generated value + if not self._unauthenticated_session_management_key: + self._unauthenticated_session_management_key = ''.join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(100)) + return self._unauthenticated_session_management_key + @property def OIDC_SKIP_CONSENT_ALWAYS(self): """ diff --git a/oidc_provider/tests/test_middleware.py b/oidc_provider/tests/test_middleware.py new file mode 100644 index 0000000..9464ec2 --- /dev/null +++ b/oidc_provider/tests/test_middleware.py @@ -0,0 +1,36 @@ +from django.conf.urls import url +from django.test import TestCase, override_settings +from django.views import View +from mock import mock + + +class StubbedViews: + class SampleView(View): + pass + + urlpatterns = [url('^test/', SampleView.as_view())] + + +@override_settings(ROOT_URLCONF=StubbedViews, + MIDDLEWARE=('django.contrib.sessions.middleware.SessionMiddleware', + 'oidc_provider.middleware.SessionManagementMiddleware'), + OIDC_SESSION_MANAGEMENT_ENABLE=True) +class MiddlewareTestCase(TestCase): + + def setUp(self): + patcher = mock.patch('oidc_provider.middleware.get_browser_state_or_default') + self.mock_get_state = patcher.start() + + def test_session_management_middleware_sets_cookie_on_response(self): + response = self.client.get('/test/') + + self.assertIn('op_browser_state', response.cookies) + self.assertEqual(response.cookies['op_browser_state'].value, + str(self.mock_get_state.return_value)) + self.mock_get_state.assert_called_once_with(response.wsgi_request) + + @override_settings(OIDC_SESSION_MANAGEMENT_ENABLE=False) + def test_session_management_middleware_does_not_set_cookie_if_session_management_disabled(self): + response = self.client.get('/test/') + + self.assertNotIn('op_browser_state', response.cookies) diff --git a/oidc_provider/tests/test_settings.py b/oidc_provider/tests/test_settings.py index db6f812..e8c252a 100644 --- a/oidc_provider/tests/test_settings.py +++ b/oidc_provider/tests/test_settings.py @@ -8,8 +8,18 @@ CUSTOM_TEMPLATES = { } -class TokenTest(TestCase): +class SettingsTest(TestCase): @override_settings(OIDC_TEMPLATES=CUSTOM_TEMPLATES) def test_override_templates(self): self.assertEqual(settings.get('OIDC_TEMPLATES'), CUSTOM_TEMPLATES) + + def test_unauthenticated_session_management_key_has_default(self): + key = settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY') + self.assertRegexpMatches(key, r'[a-zA-Z0-9]+') + self.assertGreater(len(key), 50) + + def test_unauthenticated_session_management_key_has_constant_value(self): + key1 = settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY') + key2 = settings.get('OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY') + self.assertEqual(key1, key2) diff --git a/oidc_provider/tests/test_utils.py b/oidc_provider/tests/test_utils.py index 16c987a..b09ff46 100644 --- a/oidc_provider/tests/test_utils.py +++ b/oidc_provider/tests/test_utils.py @@ -1,13 +1,15 @@ import time from datetime import datetime +from hashlib import sha224 -from django.test import TestCase +from django.http import HttpRequest +from django.test import TestCase, override_settings from django.utils import timezone +from mock import mock -from oidc_provider.lib.utils.common import get_issuer +from oidc_provider.lib.utils.common import get_issuer, get_browser_state_or_default from oidc_provider.lib.utils.token import create_id_token from oidc_provider.tests.app.utils import create_fake_user -from django.test import override_settings class Request(object): @@ -78,3 +80,19 @@ class TokenTest(TestCase): 'iss': 'http://localhost:8000/openid', 'sub': str(self.user.id), }) + + +class BrowserStateTest(TestCase): + + @override_settings(OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY='my_static_key') + def test_get_browser_state_uses_value_from_settings_to_calculate_browser_state(self): + request = HttpRequest() + request.session = mock.Mock(session_key=None) + state = get_browser_state_or_default(request) + self.assertEqual(state, sha224('my_static_key'.encode('utf-8')).hexdigest()) + + def test_get_browser_state_uses_session_key_to_calculate_browser_state_if_available(self): + request = HttpRequest() + request.session = mock.Mock(session_key='my_session_key') + state = get_browser_state_or_default(request) + self.assertEqual(state, sha224('my_session_key'.encode('utf-8')).hexdigest()) From 542479a227941072039651ee5432c04527400642 Mon Sep 17 00:00:00 2001 From: Gertjan Oude Lohuis Date: Tue, 18 Apr 2017 13:37:48 +0200 Subject: [PATCH 26/31] Do not use cookie for browser_state. It may not yet be there --- oidc_provider/lib/endpoints/authorize.py | 5 ++--- oidc_provider/tests/test_authorize_endpoint.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/oidc_provider/lib/endpoints/authorize.py b/oidc_provider/lib/endpoints/authorize.py index a050cd7..36b4b2d 100644 --- a/oidc_provider/lib/endpoints/authorize.py +++ b/oidc_provider/lib/endpoints/authorize.py @@ -30,8 +30,7 @@ from oidc_provider.models import ( UserConsent, ) from oidc_provider import settings -from oidc_provider.lib.utils.common import cleanup_url_from_query_string - +from oidc_provider.lib.utils.common import cleanup_url_from_query_string, get_browser_state_or_default logger = logging.getLogger(__name__) @@ -197,7 +196,7 @@ class AuthorizeEndpoint(object): session_state = '{client_id} {origin} {browser_state} {salt}'.format( client_id=self.client.client_id, origin=client_origin, - browser_state=self.request.COOKIES['op_browser_state'], + browser_state=get_browser_state_or_default(self.request), salt=salt) session_state = sha256(session_state.encode('utf-8')).hexdigest() session_state += '.' + salt diff --git a/oidc_provider/tests/test_authorize_endpoint.py b/oidc_provider/tests/test_authorize_endpoint.py index f691783..08acbf6 100644 --- a/oidc_provider/tests/test_authorize_endpoint.py +++ b/oidc_provider/tests/test_authorize_endpoint.py @@ -7,7 +7,7 @@ try: except ImportError: from urlparse import parse_qs, urlsplit import uuid -from mock import patch +from mock import patch, mock from django.contrib.auth.models import AnonymousUser from django.core.management import call_command @@ -537,3 +537,14 @@ class TestCreateResponseURI(TestCase): authorization_endpoint.create_response_uri() log_exception.assert_called_once_with('[Authorize] Error when trying to create response uri: %s', exception) + + @override_settings(OIDC_SESSION_MANAGEMENT_ENABLE=True) + def test_create_response_uri_generates_session_state_if_session_management_enabled(self): + # RequestFactory doesn't support sessions, so we mock it + self.request.session = mock.Mock(session_key=None) + + authorization_endpoint = AuthorizeEndpoint(self.request) + authorization_endpoint.validate_params() + + uri = authorization_endpoint.create_response_uri() + self.assertIn('session_state=', uri) From 35532634fadff50f8ec1d67d40afd3b0809579e3 Mon Sep 17 00:00:00 2001 From: Gertjan Oude Lohuis Date: Tue, 18 Apr 2017 13:50:28 +0200 Subject: [PATCH 27/31] Add docs on new setting OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY --- docs/sections/sessionmanagement.rst | 4 ++++ docs/sections/settings.rst | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/docs/sections/sessionmanagement.rst b/docs/sections/sessionmanagement.rst index c33cd5b..3c182b7 100644 --- a/docs/sections/sessionmanagement.rst +++ b/docs/sections/sessionmanagement.rst @@ -18,6 +18,10 @@ Somewhere in your Django ``settings.py``:: OIDC_SESSION_MANAGEMENT_ENABLE = True + +If you're in a multi-server setup, you might also want to add ``OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY`` to your settings and set it to some random but fixed string. While authenticated clients have a session that can be used to calculate the browser state, there is no such thing for unauthenticated clients. Hence this value. By default a value is generated randomly on startup, so this will be different on each server. To get a consistent value across all servers you should set this yourself. + + Example RP iframe ================= diff --git a/docs/sections/settings.rst b/docs/sections/settings.rst index 392615a..4b0e410 100644 --- a/docs/sections/settings.rst +++ b/docs/sections/settings.rst @@ -110,6 +110,13 @@ OPTIONAL. ``bool``. Enables OpenID Connect Session Management 1.0 in your provid Default is ``False``. +OIDC_UNAUTHENTICATED_SESSION_MANAGEMENT_KEY +=========================================== + +OPTIONAL. Supply a fixed string to use as browser-state key for unauthenticated clients. Read :ref:`sessionmanagement` section. + +Default is a string generated at startup. + OIDC_SKIP_CONSENT_ALWAYS ======================== From 26b2efe361f562321b642c32fb72844f2a968d40 Mon Sep 17 00:00:00 2001 From: Gertjan Oude Lohuis Date: Tue, 18 Apr 2017 14:24:21 +0200 Subject: [PATCH 28/31] Fix compatibility for older versions of Django --- oidc_provider/middleware.py | 6 +++++- oidc_provider/tests/test_middleware.py | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/oidc_provider/middleware.py b/oidc_provider/middleware.py index 06f4d82..3516bc4 100644 --- a/oidc_provider/middleware.py +++ b/oidc_provider/middleware.py @@ -1,4 +1,8 @@ -from django.utils.deprecation import MiddlewareMixin +try: + # https://docs.djangoproject.com/en/1.10/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware + from django.utils.deprecation import MiddlewareMixin +except ImportError: + MiddlewareMixin = object from oidc_provider import settings from oidc_provider.lib.utils.common import get_browser_state_or_default diff --git a/oidc_provider/tests/test_middleware.py b/oidc_provider/tests/test_middleware.py index 9464ec2..c2a02df 100644 --- a/oidc_provider/tests/test_middleware.py +++ b/oidc_provider/tests/test_middleware.py @@ -1,6 +1,6 @@ from django.conf.urls import url from django.test import TestCase, override_settings -from django.views import View +from django.views.generic import View from mock import mock @@ -10,10 +10,13 @@ class StubbedViews: urlpatterns = [url('^test/', SampleView.as_view())] +MW_CLASSES = ('django.contrib.sessions.middleware.SessionMiddleware', + 'oidc_provider.middleware.SessionManagementMiddleware') + @override_settings(ROOT_URLCONF=StubbedViews, - MIDDLEWARE=('django.contrib.sessions.middleware.SessionMiddleware', - 'oidc_provider.middleware.SessionManagementMiddleware'), + MIDDLEWARE=MW_CLASSES, + MIDDLEWARE_CLASSES=MW_CLASSES, OIDC_SESSION_MANAGEMENT_ENABLE=True) class MiddlewareTestCase(TestCase): From 38ee8f15fd85e7895e55a840f042365d4e40379a Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Fri, 5 May 2017 05:10:38 +0200 Subject: [PATCH 29/31] solved merging typo for missing @property --- oidc_provider/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oidc_provider/settings.py b/oidc_provider/settings.py index b04b6a7..fc2b4c9 100644 --- a/oidc_provider/settings.py +++ b/oidc_provider/settings.py @@ -91,6 +91,7 @@ class DefaultSettings(object): random.choice(string.ascii_uppercase + string.digits) for _ in range(100)) return self._unauthenticated_session_management_key + @property def OIDC_SKIP_CONSENT_EXPIRE(self): """ OPTIONAL. User consent expiration after been granted. From baad8246c790b23caba246600e189441b97b414a Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Fri, 5 May 2017 05:43:39 +0200 Subject: [PATCH 30/31] added TOX for Django 1.11 and PYthon 3.6 --- tox.ini | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 849f411..ef539ff 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,10 @@ envlist= clean, - py27-django{17,18,19,110}, - py34-django{17,18,19,110}, - py35-django{18,19,110}, + py27-django{17,18,19,110,111}, + py34-django{17,18,19,110,111}, + py35-django{18,19,110,111}, + py36-django{18,19,110,111}, [testenv] @@ -13,6 +14,7 @@ deps = django18: django>=1.8,<1.9 django19: django>=1.9,<1.10 django110: django>=1.10,<1.11 + django111: django>=1.11,<1.12 coverage mock From 2e36d2a16177b5eeaa9b2192737d8758300cc83f Mon Sep 17 00:00:00 2001 From: Wojciech Bartosiak Date: Mon, 8 May 2017 16:25:44 +0200 Subject: [PATCH 31/31] added python 3.6 and django 1.11 --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2245af5..6c39c62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,19 @@ python: - "2.7" - "3.4" - "3.5" + - "3.6" env: - DJANGO=1.7 - DJANGO=1.8 - DJANGO=1.9 - DJANGO=1.10 + - DJANGO=1.11 matrix: exclude: - python: "3.5" env: DJANGO=1.7 + - python: "3.6" + env: DJANGO=1.7 install: - pip install tox coveralls script: