diff --git a/mozilla_django_oidc/middleware.py b/mozilla_django_oidc/middleware.py index 1b050325..85bc57a5 100644 --- a/mozilla_django_oidc/middleware.py +++ b/mozilla_django_oidc/middleware.py @@ -1,27 +1,22 @@ import logging import time from re import Pattern as re_Pattern -from urllib.parse import quote, urlencode +from urllib.parse import quote from django.contrib.auth import BACKEND_SESSION_KEY 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 from mozilla_django_oidc.auth import OIDCAuthenticationBackend -from mozilla_django_oidc.utils import ( - absolutify, - add_state_and_verifier_and_nonce_to_session, - import_from_settings, -) +from mozilla_django_oidc.utils import AuthorizationCodeRequestMixin LOGGER = logging.getLogger(__name__) -class SessionRefresh(MiddlewareMixin): +class SessionRefresh(MiddlewareMixin, AuthorizationCodeRequestMixin): """Refreshes the session with the OIDC RP after expiry seconds For users authenticated with the OIDC RP, verify tokens are still valid and @@ -30,24 +25,9 @@ class SessionRefresh(MiddlewareMixin): """ def __init__(self, get_response): - super(SessionRefresh, self).__init__(get_response) + super().__init__(get_response) + self.init_settings_for_authorization_code_request() self.OIDC_EXEMPT_URLS = self.get_settings("OIDC_EXEMPT_URLS", []) - self.OIDC_OP_AUTHORIZATION_ENDPOINT = self.get_settings( - "OIDC_OP_AUTHORIZATION_ENDPOINT" - ) - self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID") - self.OIDC_STATE_SIZE = self.get_settings("OIDC_STATE_SIZE", 32) - self.OIDC_AUTHENTICATION_CALLBACK_URL = self.get_settings( - "OIDC_AUTHENTICATION_CALLBACK_URL", - "oidc_authentication_callback", - ) - self.OIDC_RP_SCOPES = self.get_settings("OIDC_RP_SCOPES", "openid email") - self.OIDC_USE_NONCE = self.get_settings("OIDC_USE_NONCE", True) - self.OIDC_NONCE_SIZE = self.get_settings("OIDC_NONCE_SIZE", 32) - - @staticmethod - def get_settings(attr, *args): - return import_from_settings(attr, *args) @cached_property def exempt_urls(self): @@ -115,6 +95,11 @@ def is_refreshable_url(self, request): and not any(pat.match(request.path) for pat in self.exempt_url_patterns) ) + def get_extra_params(self, request): + extra = super().get_extra_params(request) + extra.update(prompt="none") + return extra + def process_request(self, request): if not self.is_refreshable_url(request): LOGGER.debug("request is not refreshable") @@ -129,35 +114,11 @@ def process_request(self, request): LOGGER.debug("id token has expired") # The id_token has expired, so we have to re-authenticate silently. - 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 = { - "response_type": "code", - "client_id": client_id, - "redirect_uri": absolutify( - request, reverse(self.OIDC_AUTHENTICATION_CALLBACK_URL) - ), - "state": state, - "scope": self.OIDC_RP_SCOPES, - "prompt": "none", - } - - params.update(self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", {})) - - if self.OIDC_USE_NONCE: - nonce = get_random_string(self.OIDC_NONCE_SIZE) - params.update({"nonce": nonce}) - - add_state_and_verifier_and_nonce_to_session(request, state, params) - + redirect_url = self.get_url_for_authorization_code_request( + request, quote_via=quote + ) 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 diff --git a/mozilla_django_oidc/utils.py b/mozilla_django_oidc/utils.py index 6e70ee95..e03268f0 100644 --- a/mozilla_django_oidc/utils.py +++ b/mozilla_django_oidc/utils.py @@ -2,12 +2,15 @@ import time import warnings from hashlib import sha256 +from urllib.parse import urlencode from urllib.request import parse_http_list, parse_keqv_list # Make it obvious that these aren't the usual base64 functions import josepy.b64 from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.urls import reverse +from django.utils.crypto import get_random_string LOGGER = logging.getLogger(__name__) @@ -159,3 +162,88 @@ def add_state_and_verifier_and_nonce_to_session( "nonce": nonce, "added_on": time.time(), } + + +class AuthorizationCodeRequestMixin: + """ + Class that encapsulates the functionality required to make an authorization code request. + """ + + @staticmethod + def get_settings(attr, *args): + return import_from_settings(attr, *args) + + def init_settings_for_authorization_code_request(self): + self.OIDC_OP_AUTH_ENDPOINT = self.get_settings("OIDC_OP_AUTHORIZATION_ENDPOINT") + self.OIDC_OP_AUTHORIZATION_ENDPOINT = self.OIDC_OP_AUTH_ENDPOINT + self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID") + self.OIDC_STATE_SIZE = self.get_settings("OIDC_STATE_SIZE", 32) + self.OIDC_AUTHENTICATION_CALLBACK_URL = self.get_settings( + "OIDC_AUTHENTICATION_CALLBACK_URL", + "oidc_authentication_callback", + ) + self.OIDC_RP_SCOPES = self.get_settings("OIDC_RP_SCOPES", "openid email") + self.OIDC_USE_NONCE = self.get_settings("OIDC_USE_NONCE", True) + self.OIDC_NONCE_SIZE = self.get_settings("OIDC_NONCE_SIZE", 32) + self.OIDC_USE_PKCE = self.get_settings("OIDC_USE_PKCE", False) + self.OIDC_PKCE_CODE_VERIFIER_SIZE = self.get_settings( + "OIDC_PKCE_CODE_VERIFIER_SIZE", 64 + ) + + if not (43 <= self.OIDC_PKCE_CODE_VERIFIER_SIZE <= 128): + # Check that OIDC_PKCE_CODE_VERIFIER_SIZE is between the min and max length + # defined in https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 + raise ImproperlyConfigured( + "OIDC_PKCE_CODE_VERIFIER_SIZE must be between 43 and 128" + ) + + self.OIDC_PKCE_CODE_CHALLENGE_METHOD = self.get_settings( + "OIDC_PKCE_CODE_CHALLENGE_METHOD", "S256" + ) + + if self.OIDC_PKCE_CODE_CHALLENGE_METHOD not in ("plain", "S256"): + raise ImproperlyConfigured( + "OIDC_PKCE_CODE_CHALLENGE_METHOD must be 'plain' or 'S256'" + ) + + def get_extra_params(self, request): + return self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", {}) + + def get_url_for_authorization_code_request(self, request, **urlencode_kwargs): + """ + Builds and returns the URL required for the authorization code request, and + also adds the state, nonce, and code verifier (if using PKCE) to the session. + """ + state = get_random_string(self.OIDC_STATE_SIZE) + + params = { + "response_type": "code", + "scope": self.OIDC_RP_SCOPES, + "client_id": self.OIDC_RP_CLIENT_ID, + "redirect_uri": absolutify( + request, reverse(self.OIDC_AUTHENTICATION_CALLBACK_URL) + ), + "state": state, + } + + params.update(self.get_extra_params(request)) + + if self.OIDC_USE_NONCE: + params.update(nonce=get_random_string(self.OIDC_NONCE_SIZE)) + + if self.OIDC_USE_PKCE: + code_verifier = get_random_string(self.OIDC_PKCE_CODE_VERIFIER_SIZE) + params.update( + code_challenge=generate_code_challenge( + code_verifier, self.OIDC_PKCE_CODE_CHALLENGE_METHOD + ), + code_challenge_method=self.OIDC_PKCE_CODE_CHALLENGE_METHOD, + ) + else: + code_verifier = None + + add_state_and_verifier_and_nonce_to_session( + request, state, params, code_verifier + ) + + return f"{self.OIDC_OP_AUTHORIZATION_ENDPOINT}?{urlencode(params, **urlencode_kwargs)}" diff --git a/mozilla_django_oidc/views.py b/mozilla_django_oidc/views.py index 0f05b8b2..40ace4c2 100644 --- a/mozilla_django_oidc/views.py +++ b/mozilla_django_oidc/views.py @@ -1,20 +1,15 @@ import time -from urllib.parse import urlencode from django.contrib import auth from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseNotAllowed, HttpResponseRedirect from django.shortcuts import resolve_url -from django.urls import reverse -from django.utils.crypto import get_random_string from django.utils.http import url_has_allowed_host_and_scheme from django.utils.module_loading import import_string from django.views.generic import View from mozilla_django_oidc.utils import ( - absolutify, - add_state_and_verifier_and_nonce_to_session, - generate_code_challenge, + AuthorizationCodeRequestMixin, import_from_settings, ) @@ -159,85 +154,26 @@ def get_next_url(request, redirect_field_name): return None -class OIDCAuthenticationRequestView(View): +class OIDCAuthenticationRequestView(View, AuthorizationCodeRequestMixin): """OIDC client authentication HTTP endpoint""" http_method_names = ["get"] def __init__(self, *args, **kwargs): - super(OIDCAuthenticationRequestView, self).__init__(*args, **kwargs) - - self.OIDC_OP_AUTH_ENDPOINT = self.get_settings("OIDC_OP_AUTHORIZATION_ENDPOINT") - self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID") - - @staticmethod - def get_settings(attr, *args): - return import_from_settings(attr, *args) + super().__init__(*args, **kwargs) + self.init_settings_for_authorization_code_request() + self.OIDC_REDIRECT_FIELD_NAME = self.get_settings( + "OIDC_REDIRECT_FIELD_NAME", "next" + ) def get(self, request): """OIDC client authentication initialization HTTP endpoint""" - state = get_random_string(self.get_settings("OIDC_STATE_SIZE", 32)) - redirect_field_name = self.get_settings("OIDC_REDIRECT_FIELD_NAME", "next") - reverse_url = self.get_settings( - "OIDC_AUTHENTICATION_CALLBACK_URL", "oidc_authentication_callback" - ) - - params = { - "response_type": "code", - "scope": self.get_settings("OIDC_RP_SCOPES", "openid email"), - "client_id": self.OIDC_RP_CLIENT_ID, - "redirect_uri": absolutify(request, reverse(reverse_url)), - "state": state, - } - - params.update(self.get_extra_params(request)) - - if self.get_settings("OIDC_USE_NONCE", True): - nonce = get_random_string(self.get_settings("OIDC_NONCE_SIZE", 32)) - params.update({"nonce": nonce}) - - if self.get_settings("OIDC_USE_PKCE", False): - code_verifier_length = self.get_settings("OIDC_PKCE_CODE_VERIFIER_SIZE", 64) - # Check that code_verifier_length is between the min and max length - # defined in https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 - if not (43 <= code_verifier_length <= 128): - raise ValueError("code_verifier_length must be between 43 and 128") - - # Generate code_verifier and code_challenge pair - code_verifier = get_random_string(code_verifier_length) - code_challenge_method = self.get_settings( - "OIDC_PKCE_CODE_CHALLENGE_METHOD", "S256" - ) - code_challenge = generate_code_challenge( - code_verifier, code_challenge_method - ) - - # Append code_challenge to authentication request parameters - params.update( - { - "code_challenge": code_challenge, - "code_challenge_method": code_challenge_method, - } - ) - - else: - code_verifier = None - - add_state_and_verifier_and_nonce_to_session( - request, state, params, code_verifier - ) - - request.session["oidc_login_next"] = get_next_url(request, redirect_field_name) - - query = urlencode(params) - redirect_url = "{url}?{query}".format( - url=self.OIDC_OP_AUTH_ENDPOINT, query=query + redirect_url = self.get_url_for_authorization_code_request(request) + request.session["oidc_login_next"] = get_next_url( + request, self.OIDC_REDIRECT_FIELD_NAME ) return HttpResponseRedirect(redirect_url) - def get_extra_params(self, request): - return self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", {}) - class OIDCLogoutView(View): """Logout helper view""" diff --git a/tests/test_middleware.py b/tests/test_middleware.py index ac1b708f..cb258677 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -23,7 +23,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) -@patch("mozilla_django_oidc.middleware.get_random_string") +@patch("mozilla_django_oidc.utils.get_random_string") class SessionRefreshTokenMiddlewareTestCase(TestCase): def setUp(self): self.factory = RequestFactory() @@ -78,6 +78,36 @@ def test_is_ajax(self, mock_middleware_random): json_payload = json.loads(response.content.decode("utf-8")) self.assertEqual(json_payload["refresh_url"], response["refresh_url"]) + @override_settings(OIDC_USE_PKCE=True) + def test_is_ajax_with_pkce(self, mock_middleware_random): + mock_middleware_random.return_value = "examplestring" + + request = self.factory.get("/foo", HTTP_X_REQUESTED_WITH="XMLHttpRequest") + request.session = {} + request.user = self.user + + response = SessionRefresh(MagicMock).process_request(request) + self.assertEqual(response.status_code, 403) + # The URL to go to is available both as a header and as a key + # in the JSON response. + self.assertTrue(response["refresh_url"]) + url, qs = response["refresh_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"], + "code_challenge_method": ["S256"], + "code_challenge": ["m8yog7rVNdOd7hYIoUg6yl5mk_IYauWdSIBUjoPJHB0"], + } + self.assertEqual(expected_query, parse_qs(qs)) + json_payload = json.loads(response.content.decode("utf-8")) + self.assertEqual(json_payload["refresh_url"], response["refresh_url"]) + def test_no_oidc_token_expiration_forces_renewal(self, mock_middleware_random): mock_middleware_random.return_value = "examplestring" @@ -101,6 +131,34 @@ def test_no_oidc_token_expiration_forces_renewal(self, mock_middleware_random): } self.assertEqual(expected_query, parse_qs(qs)) + @override_settings(OIDC_USE_PKCE=True) + def test_no_oidc_token_expiration_forces_renewal_with_pkce( + self, mock_middleware_random + ): + mock_middleware_random.return_value = "examplestring" + + request = self.factory.get("/foo") + request.user = self.user + request.session = {} + + response = SessionRefresh(MagicMock).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"], + "code_challenge_method": ["S256"], + "code_challenge": ["m8yog7rVNdOd7hYIoUg6yl5mk_IYauWdSIBUjoPJHB0"], + } + self.assertEqual(expected_query, parse_qs(qs)) + def test_expired_token_forces_renewal(self, mock_middleware_random): mock_middleware_random.return_value = "examplestring" @@ -124,6 +182,32 @@ def test_expired_token_forces_renewal(self, mock_middleware_random): } self.assertEqual(expected_query, parse_qs(qs)) + @override_settings(OIDC_USE_PKCE=True) + def test_expired_token_forces_renewal_with_pkce(self, mock_middleware_random): + mock_middleware_random.return_value = "examplestring" + + request = self.factory.get("/foo") + request.user = self.user + request.session = {"oidc_id_token_expiration": time.time() - 10} + + response = SessionRefresh(MagicMock).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"], + "code_challenge_method": ["S256"], + "code_challenge": ["m8yog7rVNdOd7hYIoUg6yl5mk_IYauWdSIBUjoPJHB0"], + } + self.assertEqual(expected_query, parse_qs(qs)) + # This adds a "home page" we can test against. def fakeview(req): @@ -275,7 +359,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) - @patch("mozilla_django_oidc.middleware.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") def test_expired_token_redirects_to_sso(self, mock_middleware_random): mock_middleware_random.return_value = "examplestring" @@ -309,7 +393,44 @@ 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) - @patch("mozilla_django_oidc.middleware.get_random_string") + @override_settings(OIDC_USE_PKCE=True) + @patch("mozilla_django_oidc.utils.get_random_string") + def test_expired_token_redirects_to_sso_with_pkce(self, mock_middleware_random): + mock_middleware_random.return_value = "examplestring" + + client = ClientWithUser() + client.login(username=self.user.username, password="password") + + # Set expiration to some time in the past + session = client.session + session["oidc_id_token_expiration"] = time.time() - 100 + session[ + "_auth_user_backend" + ] = "mozilla_django_oidc.auth.OIDCAuthenticationBackend" + session.save() + + resp = client.get("/mdo_fake_view/") + self.assertEqual(resp.status_code, 302) + + url, qs = resp.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"], + "code_challenge_method": ["S256"], + "code_challenge": ["m8yog7rVNdOd7hYIoUg6yl5mk_IYauWdSIBUjoPJHB0"], + } + 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_ID_TOKEN_EXPIRY_SECONDS=120) + @patch("mozilla_django_oidc.utils.get_random_string") def test_refresh_fails_for_already_signed_in_user(self, mock_random_string): mock_random_string.return_value = "examplestring" diff --git a/tests/test_views.py b/tests/test_views.py index b77b9feb..721c668d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser -from django.core.exceptions import SuspiciousOperation +from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.test import Client, RequestFactory, TestCase, override_settings from django.urls import reverse from unittest.mock import patch @@ -478,7 +478,7 @@ def setUp(self): @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT="https://server.example.com/auth") @override_settings(OIDC_RP_CLIENT_ID="example_id") @override_settings(OIDC_USE_PKCE=True) - @patch("mozilla_django_oidc.views.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") def test_get(self, mock_views_random): """Test initiation of a successful OIDC attempt.""" mock_views_random.return_value = "examplestring" @@ -517,7 +517,7 @@ def test_get(self, mock_views_random): @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT="https://server.example.com/auth") @override_settings(OIDC_RP_CLIENT_ID="example_id") @override_settings(OIDC_USE_PKCE=False) - @patch("mozilla_django_oidc.views.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") def test_get_without_PKCE(self, mock_views_random): """Test initiation of a successful OIDC attempt with PKCE disabled.""" mock_views_random.return_value = "examplestring" @@ -548,7 +548,7 @@ def test_get_without_PKCE(self, mock_views_random): @override_settings(OIDC_RP_CLIENT_ID="example_id") @override_settings(OIDC_USE_PKCE=True) @override_settings(OIDC_PKCE_CODE_VERIFIER_SIZE=42) # must be between 43 and 128 - @patch("mozilla_django_oidc.views.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") def test_get_invalid_code_verifier_size_too_short(self, mock_views_random): """Test initiation of an OIDC attempt with an invalid code verifier size.""" mock_views_random.return_value = "examplestring" @@ -556,20 +556,14 @@ def test_get_invalid_code_verifier_size_too_short(self, mock_views_random): request = self.factory.get(url) request.session = dict() login_view = views.OIDCAuthenticationRequestView.as_view() - try: + with self.assertRaises(ImproperlyConfigured): login_view(request) - self.fail( - "OIDC_PKCE_CODE_VERIFIER_SIZE must be between 43 and 128," - " but OIDC_PKCE_CODE_VERIFIER_SIZE was 42 and no exception was raised." - ) - except ValueError: - pass @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT="https://server.example.com/auth") @override_settings(OIDC_RP_CLIENT_ID="example_id") @override_settings(OIDC_USE_PKCE=True) @override_settings(OIDC_PKCE_CODE_VERIFIER_SIZE=129) # must be between 43 and 128 - @patch("mozilla_django_oidc.views.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") def test_get_invalid_code_verifier_size_too_long(self, mock_views_random): """Test initiation of an OIDC attempt with an invalid code verifier size.""" mock_views_random.return_value = "examplestring" @@ -577,14 +571,8 @@ def test_get_invalid_code_verifier_size_too_long(self, mock_views_random): request = self.factory.get(url) request.session = dict() login_view = views.OIDCAuthenticationRequestView.as_view() - try: + with self.assertRaises(ImproperlyConfigured): login_view(request) - self.fail( - "OIDC_PKCE_CODE_VERIFIER_SIZE must be between 43 and 128," - " but OIDC_PKCE_CODE_VERIFIER_SIZE was 129 and no exception was raised." - ) - except ValueError: - pass @override_settings(ROOT_URLCONF="tests.namespaced_urls") @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT="https://server.example.com/auth") @@ -593,7 +581,7 @@ def test_get_invalid_code_verifier_size_too_long(self, mock_views_random): @override_settings( OIDC_AUTHENTICATION_CALLBACK_URL="namespace:oidc_authentication_callback" ) - @patch("mozilla_django_oidc.views.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") def test_get_namespaced(self, mock_views_random): """Test initiation of a successful OIDC attempt with namespaced redirect_uri.""" mock_views_random.return_value = "examplestring" @@ -635,7 +623,7 @@ def test_get_namespaced(self, mock_views_random): @override_settings( OIDC_AUTH_REQUEST_EXTRA_PARAMS={"audience": "some-api.example.com"} ) - @patch("mozilla_django_oidc.views.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") def test_get_with_audience(self, mock_views_random): """Test initiation of a successful OIDC attempt.""" mock_views_random.return_value = "examplestring" @@ -675,7 +663,7 @@ def test_get_with_audience(self, mock_views_random): @override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT="https://server.example.com/auth") @override_settings(OIDC_RP_CLIENT_ID="example_id") @override_settings(OIDC_USE_PKCE=True) - @patch("mozilla_django_oidc.views.get_random_string") + @patch("mozilla_django_oidc.utils.get_random_string") @patch("mozilla_django_oidc.views.OIDCAuthenticationRequestView.get_extra_params") def test_get_with_overridden_extra_params( self, mock_extra_params, mock_views_random