From 162a177b14b0fb0d86ab279bb4c914ccc3c99c3e Mon Sep 17 00:00:00 2001 From: Germano Guerrini Date: Tue, 12 Mar 2019 12:11:29 +0100 Subject: [PATCH] Add RefreshOIDCAccessToken middleware The OP can provide a refresh_token to the client on authentication. This can later be used to get a new access_token. Typically refresh_tokens have a longer TTL than access_tokens and represent the total allowed session length. As a bonus, the refresh happens in the background and does not require taking the user to a new location (which also makes it more compatible with e.g., XHR). If there is no refresh token stored, making refreshing impossible, OR the refresh request fails with a 401, indicating the OP session has terminated, the user is taken through a refresh flow similar to the SessionRefresh middleware. If any error occurs during refresh, the middleware aborts, but does not perform any cleanup on the session. Co-Authored-By: Jason Anderson --- docs/installation.rst | 50 ++++++++- docs/settings.rst | 10 +- mozilla_django_oidc/auth.py | 30 ++++-- mozilla_django_oidc/middleware.py | 163 +++++++++++++++++++++++++----- mozilla_django_oidc/views.py | 10 +- tests/test_middleware.py | 150 +++++++++++++++++++++++---- 6 files changed, 350 insertions(+), 63 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index a0efee5e..465834f9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -143,8 +143,8 @@ Next, edit your ``urls.py`` and add the following: .. code-block:: python - from django.urls import path, include - + from django.urls import path + urlpatterns = [ # ... path('oidc/', include('mozilla_django_oidc.urls')), @@ -220,8 +220,50 @@ check to see if the user's id token has expired and if so, redirect to the OIDC provider's authentication endpoint for a silent re-auth. That will redirect back to the page the user was going to. -The length of time it takes for an id token to expire is set in -``settings.OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS`` which defaults to 15 minutes. +The length of time it takes for a token to expire is set in +``settings.OIDC_RENEW_TOKEN_EXPIRY_SECONDS``, which defaults to 15 minutes. + + +Getting a new access token using the refresh token +-------------------------------------------------- + +Alternatively, if the OIDC Provider supplies a refresh token during the +authorization phase, it can be stored in the session by setting +``settings.OIDC_STORE_REFRESH_TOKEN`` to `True`. +It will be then used by the +:py:class:`mozilla_django_oidc.middleware.RefreshOIDCAccessToken` middleware. + +The middleware will check if the user's access token has expired with the same +logic of :py:class:`mozilla_django_oidc.middleware.SessionRefresh` but, instead +of taking the user through a browser-based authentication flow, it will request +a new access token from the OP in the background. + +.. warning:: + + Using this middleware will effectively cause ID tokens to no longer be stored + in the request session, e.g., ``oidc_id_token`` will no longer be available + to Django. This is due to the fact that secure verification of the ID token + is currently not possible in the refresh flow due to not enough information + about the initial authentication being preserved in the session backend. + + If you rely on ID tokens, do not use this middleware. It is only useful if + you are relying instead on access tokens. + +To add it to your site, put it in the settings:: + + MIDDLEWARE_CLASSES = [ + # middleware involving session and authentication must come first + # ... + 'mozilla_django_oidc.middleware.RefreshOIDCAccessToken', + # ... + ] + +The length of time it takes for a token to expire is set in +``settings.OIDC_RENEW_TOKEN_EXPIRY_SECONDS``, which defaults to 15 minutes. + +.. seealso:: + + https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens Connecting OIDC user identities to Django users diff --git a/docs/settings.rst b/docs/settings.rst index 125ba36e..5acf3bdc 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -85,7 +85,8 @@ of ``mozilla-django-oidc``. This is a list of absolute url paths, regular expressions for url paths, or Django view names. This plus the mozilla-django-oidc urls are exempted from - the session renewal by the ``SessionRefresh`` middleware. + the session renewal by the ``SessionRefresh`` or ``RefreshOIDCAccessToken`` + middlewares. .. py:attribute:: OIDC_CREATE_USER @@ -174,6 +175,13 @@ of ``mozilla-django-oidc``. Controls whether the OpenID Connect client stores the OIDC ``id_token`` in the user session. The session key used to store the data is ``oidc_id_token``. +.. py:attribute:: OIDC_STORE_REFRESH_TOKEN + + :default: ``False`` + + Controls whether the OpenID Connect client stores the OIDC ``refresh_token`` in the user session. + The session key used to store the data is ``oidc_refresh_token``. + .. py:attribute:: OIDC_AUTH_REQUEST_EXTRA_PARAMS :default: `{}` diff --git a/mozilla_django_oidc/auth.py b/mozilla_django_oidc/auth.py index 54f89a8a..e2dc84a1 100644 --- a/mozilla_django_oidc/auth.py +++ b/mozilla_django_oidc/auth.py @@ -40,6 +40,17 @@ def default_username_algo(email): return smart_str(username) +def store_tokens(session, access_token, id_token, refresh_token): + if import_from_settings('OIDC_STORE_ACCESS_TOKEN', False): + session['oidc_access_token'] = access_token + + if import_from_settings('OIDC_STORE_ID_TOKEN', False): + session['oidc_id_token'] = id_token + + if import_from_settings('OIDC_STORE_REFRESH_TOKEN', False): + session['oidc_refresh_token'] = refresh_token + + class OIDCAuthenticationBackend(ModelBackend): """Override Django's authentication.""" @@ -279,12 +290,12 @@ def authenticate(self, request, **kwargs): token_info = self.get_token(token_payload) id_token = token_info.get('id_token') access_token = token_info.get('access_token') + refresh_token = token_info.get('refresh_token') # Validate the token payload = self.verify_token(id_token, nonce=nonce) - if payload: - self.store_tokens(access_token, id_token) + self.store_tokens(access_token, id_token, refresh_token) try: return self.get_or_create_user(access_token, id_token, payload) except SuspiciousOperation as exc: @@ -293,15 +304,14 @@ def authenticate(self, request, **kwargs): return None - def store_tokens(self, access_token, id_token): + def store_tokens(self, access_token, id_token, refresh_token): """Store OIDC tokens.""" - session = self.request.session - - if self.get_settings('OIDC_STORE_ACCESS_TOKEN', False): - session['oidc_access_token'] = access_token - - if self.get_settings('OIDC_STORE_ID_TOKEN', False): - session['oidc_id_token'] = id_token + return store_tokens( + self.request.session, + access_token, + id_token, + refresh_token + ) def get_or_create_user(self, access_token, id_token, payload): """Returns a User instance if 1 user is found. Creates a user if not found diff --git a/mozilla_django_oidc/middleware.py b/mozilla_django_oidc/middleware.py index eee73e82..3b7ee94f 100644 --- a/mozilla_django_oidc/middleware.py +++ b/mozilla_django_oidc/middleware.py @@ -1,15 +1,18 @@ +import json import logging import time -from django.contrib.auth import BACKEND_SESSION_KEY +from django.contrib import auth from django.http import HttpResponseRedirect, JsonResponse from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.deprecation import MiddlewareMixin from django.utils.functional import cached_property from django.utils.module_loading import import_string +import requests +from requests.auth import HTTPBasicAuth -from mozilla_django_oidc.auth import OIDCAuthenticationBackend +from mozilla_django_oidc.auth import OIDCAuthenticationBackend, store_tokens from mozilla_django_oidc.utils import (absolutify, add_state_and_nonce_to_session, import_from_settings) @@ -95,6 +98,11 @@ def exempt_url_patterns(self): exempt_patterns.add(url_pattern) return exempt_patterns + @property + def logout_redirect_url(self): + """Return the logout url defined in settings.""" + return self.get_settings('LOGOUT_REDIRECT_URL', '/') + def is_refreshable_url(self, request): """Takes a request and returns whether it triggers a refresh examination @@ -104,7 +112,7 @@ def is_refreshable_url(self, request): """ # Do not attempt to refresh the session if the OIDC backend is not used - backend_session = request.session.get(BACKEND_SESSION_KEY) + backend_session = request.session.get(auth.BACKEND_SESSION_KEY) is_oidc_enabled = True if backend_session: auth_backend = import_string(backend_session) @@ -118,27 +126,71 @@ def is_refreshable_url(self, request): not any(pat.match(request.path) for pat in self.exempt_url_patterns) ) - def process_request(self, request): + def is_expired(self, request): if not self.is_refreshable_url(request): LOGGER.debug('request is not refreshable') - return + return False - expiration = request.session.get('oidc_id_token_expiration', 0) + expiration = request.session.get('oidc_token_expiration', 0) now = time.time() if expiration > now: # The id_token is still valid, so we don't have to do anything. LOGGER.debug('id token is still valid (%s > %s)', expiration, now) + return False + + return True + + def process_request(self, request): + if not self.is_expired(request): return LOGGER.debug('id token has expired') - # The id_token has expired, so we have to re-authenticate silently. + return self.finish(request, prompt_reauth=True) + + def finish(self, request, prompt_reauth=True): + """Finish request handling and handle sending downstream responses for XHR. + + This function should only be run if the session is determind to + be expired. + + Almost all XHR request handling in client-side code struggles + with redirects since redirecting to a page where the user + is supposed to do something is extremely unlikely to work + in an XHR request. Make a special response for these kinds + of requests. + + The use of 403 Forbidden is to match the fact that this + middleware doesn't really want the user in if they don't + refresh their session. + """ + default_response = None + xhr_response_json = {'error': 'the authentication session has expired'} + if prompt_reauth: + # The id_token has expired, so we have to re-authenticate silently. + refresh_url = self._prepare_reauthorization(request) + default_response = HttpResponseRedirect(refresh_url) + xhr_response_json['refresh_url'] = refresh_url + + if request.headers.get('x-requested-with') == 'XMLHttpRequest': + xhr_response = JsonResponse(xhr_response_json, status=403) + if 'refresh_url' in xhr_response_json: + xhr_response['refresh_url'] = xhr_response_json['refresh_url'] + return xhr_response + else: + return default_response + + def _prepare_reauthorization(self, request): + # Constructs a new authorization grant request to refresh the session. + # Besides constructing the request, the state and nonce included in the + # request are registered in the current session in preparation for the + # client following through with the authorization flow. auth_url = self.OIDC_OP_AUTHORIZATION_ENDPOINT client_id = self.OIDC_RP_CLIENT_ID state = get_random_string(self.OIDC_STATE_SIZE) # Build the parameters as if we were doing a real auth handoff, except # we also include prompt=none. - params = { + auth_params = { 'response_type': 'code', 'client_id': client_id, 'redirect_uri': absolutify( @@ -152,26 +204,85 @@ def process_request(self, request): if self.OIDC_USE_NONCE: nonce = get_random_string(self.OIDC_NONCE_SIZE) - params.update({ + auth_params.update({ 'nonce': nonce }) - add_state_and_nonce_to_session(request, state, params) - + # Register the one-time parameters in the session + add_state_and_nonce_to_session(request, state, auth_params) request.session['oidc_login_next'] = request.get_full_path() - query = urlencode(params, quote_via=quote) - redirect_url = '{url}?{query}'.format(url=auth_url, query=query) - if request.headers.get('x-requested-with') == 'XMLHttpRequest': - # Almost all XHR request handling in client-side code struggles - # with redirects since redirecting to a page where the user - # is supposed to do something is extremely unlikely to work - # in an XHR request. Make a special response for these kinds - # of requests. - # The use of 403 Forbidden is to match the fact that this - # middleware doesn't really want the user in if they don't - # refresh their session. - response = JsonResponse({'refresh_url': redirect_url}, status=403) - response['refresh_url'] = redirect_url - return response - return HttpResponseRedirect(redirect_url) + query = urlencode(auth_params, quote_via=quote) + return '{auth_url}?{query}'.format(auth_url=auth_url, query=query) + + +class RefreshOIDCAccessToken(SessionRefresh): + """ + A middleware that will refresh the access token following proper OIDC protocol: + https://auth0.com/docs/tokens/refresh-token/current + """ + def process_request(self, request): + if not self.is_expired(request): + return + + token_url = import_from_settings('OIDC_OP_TOKEN_ENDPOINT') + client_id = import_from_settings('OIDC_RP_CLIENT_ID') + client_secret = import_from_settings('OIDC_RP_CLIENT_SECRET') + refresh_token = request.session.get('oidc_refresh_token') + + if not refresh_token: + LOGGER.debug('no refresh token stored') + return self.finish(request, prompt_reauth=True) + + token_payload = { + 'grant_type': 'refresh_token', + 'client_id': client_id, + 'client_secret': client_secret, + 'refresh_token': refresh_token, + } + + req_auth = None + if self.get_settings('OIDC_TOKEN_USE_BASIC_AUTH', False): + # When Basic auth is defined, create the Auth Header and remove secret from payload. + user = token_payload.get('client_id') + pw = token_payload.get('client_secret') + + req_auth = HTTPBasicAuth(user, pw) + del token_payload['client_secret'] + + try: + response = requests.post( + token_url, + auth=req_auth, + data=token_payload, + verify=import_from_settings('OIDC_VERIFY_SSL', True) + ) + response.raise_for_status() + token_info = response.json() + except requests.exceptions.Timeout: + LOGGER.debug('timed out refreshing access token') + # Don't prompt for reauth as this could be a temporary problem + return self.finish(request, prompt_reauth=False) + except requests.exceptions.HTTPError as exc: + status_code = exc.response.status_code + LOGGER.debug('http error %s when refreshing access token', status_code) + # OAuth error response will be a 400 for various situations, including + # an expired token. https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 + return self.finish(request, prompt_reauth=(status_code == 400)) + except json.JSONDecodeError: + LOGGER.debug('malformed response when refreshing access token') + # Don't prompt for reauth as this could be a temporary problem + return self.finish(request, prompt_reauth=False) + except Exception as exc: + LOGGER.debug( + 'unknown error occurred when refreshing access token: %s', exc) + # Don't prompt for reauth as this could be a temporary problem + return self.finish(request, prompt_reauth=False) + + # Until we can properly validate an ID token on the refresh response + # per the spec[1], we intentionally drop the id_token. + # [1]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse + id_token = None + access_token = token_info.get('access_token') + refresh_token = token_info.get('refresh_token') + store_tokens(request.session, access_token, id_token, refresh_token) diff --git a/mozilla_django_oidc/views.py b/mozilla_django_oidc/views.py index 5173b66b..f50d1dae 100644 --- a/mozilla_django_oidc/views.py +++ b/mozilla_django_oidc/views.py @@ -49,9 +49,13 @@ def login_success(self): auth.login(self.request, self.user) # Figure out when this id_token will expire. This is ignored unless you're - # using the RenewIDToken middleware. - expiration_interval = self.get_settings('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 60 * 15) - self.request.session['oidc_id_token_expiration'] = time.time() + expiration_interval + # using the SessionRefresh or RefreshOIDCAccessToken middlewares. + expiration_interval = self.get_settings( + 'OIDC_RENEW_TOKEN_EXPIRY_SECONDS', + # Handle old configuration value + self.get_settings('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 60 * 15) + ) + self.request.session['oidc_token_expiration'] = time.time() + expiration_interval return HttpResponseRedirect(self.success_url) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 8fc8995a..59c68ae0 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -4,7 +4,7 @@ from urllib.parse import parse_qs -from mock import patch +from mock import Mock, patch from django.urls import path from django.contrib.auth import get_user_model @@ -16,7 +16,7 @@ from django.test import Client, RequestFactory, TestCase, override_settings from django.test.client import ClientHandler -from mozilla_django_oidc.middleware import SessionRefresh +from mozilla_django_oidc.middleware import SessionRefresh, RefreshOIDCAccessToken from mozilla_django_oidc.urls import urlpatterns as orig_urlpatterns @@ -25,7 +25,7 @@ @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='http://example.com/authorize') @override_settings(OIDC_RP_CLIENT_ID='foo') -@override_settings(OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS=120) +@override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) @patch('mozilla_django_oidc.middleware.get_random_string') class SessionRefreshTokenMiddlewareTestCase(TestCase): def setUp(self): @@ -113,7 +113,7 @@ def test_expired_token_forces_renewal(self, mock_middleware_random): request = self.factory.get('/foo') request.user = self.user request.session = { - 'oidc_id_token_expiration': time.time() - 10 + 'oidc_token_expiration': time.time() - 10 } response = self.middleware.process_request(request) @@ -133,6 +133,65 @@ def test_expired_token_forces_renewal(self, mock_middleware_random): self.assertEqual(expected_query, parse_qs(qs)) +@override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='http://example.com/authorize') +@override_settings(OIDC_RP_CLIENT_ID='foo') +@override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) +class RefreshOIDCAccessTokenMiddlewareTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + self.middleware = RefreshOIDCAccessToken() + self.user = User.objects.create_user('example_username') + + def test_anonymous(self): + request = self.factory.get('/foo') + request.session = {} + request.user = AnonymousUser() + response = self.middleware.process_request(request) + self.assertTrue(not response) + + def test_is_oidc_path(self): + request = self.factory.get('/oidc/callback/') + request.user = AnonymousUser() + request.session = {} + response = self.middleware.process_request(request) + self.assertTrue(not response) + + def test_is_POST(self): + request = self.factory.post('/foo') + request.user = AnonymousUser() + request.session = {} + response = self.middleware.process_request(request) + self.assertTrue(not response) + + @override_settings(OIDC_OP_TOKEN_ENDPOINT='https://server.example.com/token') + @override_settings(OIDC_RP_CLIENT_ID='foo') + @override_settings(OIDC_RP_CLIENT_SECRET='client_secret') + @override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) + @patch('mozilla_django_oidc.middleware.get_random_string') + def test_no_refresh_token_expiration_forces_renewal(self, mock_random_string): + mock_random_string.return_value = 'examplestring' + + request = self.factory.get('/foo') + request.user = self.user + request.session = {} + + response = self.middleware.process_request(request) + + self.assertEqual(response.status_code, 302) + url, qs = response.url.split('?') + self.assertEqual(url, 'http://example.com/authorize') + expected_query = { + 'response_type': ['code'], + 'redirect_uri': ['http://testserver/callback/'], + 'client_id': ['foo'], + 'nonce': ['examplestring'], + 'prompt': ['none'], + 'scope': ['openid email'], + 'state': ['examplestring'], + } + self.assertEqual(expected_query, parse_qs(qs)) + + # This adds a "home page" we can test against. def fakeview(req): return HttpResponse('Win!') @@ -143,12 +202,14 @@ def fakeview(req): ] -def override_middleware(fun): - classes = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'mozilla_django_oidc.middleware.SessionRefresh', - ] - return override_settings(MIDDLEWARE=classes)(fun) +def override_middleware(middleware): + def wrap(fun): + classes = [ + 'django.contrib.sessions.middleware.SessionMiddleware', + middleware, + ] + return override_settings(MIDDLEWARE=classes)(fun) + return wrap class UserifiedClientHandler(ClientHandler): @@ -197,11 +258,11 @@ def login(self, **credentials): @override_settings(OIDC_RP_CLIENT_ID='foo') -@override_settings(OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS=120) +@override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='http://example.com/authorize') @override_settings(ROOT_URLCONF='tests.test_middleware') -@override_middleware -class MiddlewareTestCase(TestCase): +@override_middleware('mozilla_django_oidc.middleware.SessionRefresh') +class SessionRefreshMiddlewareTestCase(TestCase): """These tests test the middleware as part of the request/response cycle""" def setUp(self): self.factory = RequestFactory() @@ -262,14 +323,14 @@ def test_anonymous(self): @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='http://example.com/authorize') @override_settings(OIDC_RP_CLIENT_ID='foo') - @override_settings(OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS=120) + @override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) def test_authenticated_user(self): client = ClientWithUser() client.login(username=self.user.username, password='password') # Set the expiration to some time in the future so this user is valid session = client.session - session['oidc_id_token_expiration'] = time.time() + 100 + session['oidc_token_expiration'] = time.time() + 100 session.save() resp = client.get('/mdo_fake_view/') @@ -277,7 +338,7 @@ def test_authenticated_user(self): @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='http://example.com/authorize') @override_settings(OIDC_RP_CLIENT_ID='foo') - @override_settings(OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS=120) + @override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) @patch('mozilla_django_oidc.middleware.get_random_string') def test_expired_token_redirects_to_sso(self, mock_middleware_random): mock_middleware_random.return_value = 'examplestring' @@ -287,7 +348,7 @@ def test_expired_token_redirects_to_sso(self, mock_middleware_random): # Set expiration to some time in the past session = client.session - session['oidc_id_token_expiration'] = time.time() - 100 + session['oidc_token_expiration'] = time.time() - 100 session['_auth_user_backend'] = 'mozilla_django_oidc.auth.OIDCAuthenticationBackend' session.save() @@ -309,7 +370,7 @@ def test_expired_token_redirects_to_sso(self, mock_middleware_random): @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='http://example.com/authorize') @override_settings(OIDC_RP_CLIENT_ID='foo') - @override_settings(OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS=120) + @override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) @patch('mozilla_django_oidc.middleware.get_random_string') def test_refresh_fails_for_already_signed_in_user(self, mock_random_string): mock_random_string.return_value = 'examplestring' @@ -336,7 +397,7 @@ def logged_out(sender, user=None, **kwargs): # Set expiration to some time in the past session = client.session - session['oidc_id_token_expiration'] = time.time() - 100 + session['oidc_token_expiration'] = time.time() - 100 session['_auth_user_backend'] = 'mozilla_django_oidc.auth.OIDCAuthenticationBackend' session.save() @@ -367,3 +428,54 @@ def logged_out(sender, user=None, **kwargs): # The signal we registered should have fired for this user. self.assertEqual(client.user, logged_out_users[0]) + + +@override_settings(ROOT_URLCONF='tests.test_middleware') +@override_middleware('mozilla_django_oidc.middleware.RefreshOIDCAccessToken') +class RefreshOIDCAccessTokenTestCase(TestCase): + + def setUp(self): + self.user = User.objects.create_user(username='example_username', password='password') + cache.clear() + + @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='http://example.com/authorize') + @override_settings(OIDC_OP_TOKEN_ENDPOINT='https://server.example.com/token') + @override_settings(OIDC_RP_CLIENT_ID='foo') + @override_settings(OIDC_RP_CLIENT_SECRET='client_secret') + @override_settings(OIDC_RENEW_TOKEN_EXPIRY_SECONDS=120) + @override_settings(OIDC_STORE_REFRESH_TOKEN=True) + @patch('mozilla_django_oidc.middleware.get_random_string') + @patch('mozilla_django_oidc.middleware.requests') + def test_refresh_token_forces_renewal(self, request_mock, mock_random_string): + mock_random_string.return_value = 'examplestring' + + post_json_mock = Mock() + post_json_mock.json.return_value = { + 'id_token': 'id_token', + 'accesss_token': 'access_token', + 'refresh_token': 'new_refresh_token' + } + request_mock.post.return_value = post_json_mock + + client = ClientWithUser() + # First confirm that the home page is a public page. + resp = client.get('/') + # At least security doesn't kick you out. + self.assertEqual(resp.status_code, 404) + # Also check that this page doesn't force you to redirect + # to authenticate. + resp = client.get('/mdo_fake_view/') + self.assertEqual(resp.status_code, 200) + client.login(username=self.user.username, password='password') + + # Set expiration to some time in the past + session = client.session + session['oidc_token_expiration'] = time.time() - 100 + session['oidc_refresh_token'] = 'examplerefreshtoken' + session['_auth_user_backend'] = 'mozilla_django_oidc.auth.OIDCAuthenticationBackend' + session.save() + + # Confirm that the session value has been refreshed. + resp = client.get('/mdo_fake_view/') + self.assertEqual(resp.status_code, 200) + self.assertEqual(client.session['oidc_refresh_token'], 'new_refresh_token')