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())