Skip to content

Commit 7ee4bc5

Browse files
committed
refactor for consolidated auth code request
1 parent f75ff62 commit 7ee4bc5

File tree

5 files changed

+245
-151
lines changed

5 files changed

+245
-151
lines changed

mozilla_django_oidc/middleware.py

Lines changed: 13 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
11
import logging
22
import time
33
from re import Pattern as re_Pattern
4-
from urllib.parse import quote, urlencode
4+
from urllib.parse import quote
55

66
from django.contrib.auth import BACKEND_SESSION_KEY
77
from django.http import HttpResponseRedirect, JsonResponse
88
from django.urls import reverse
9-
from django.utils.crypto import get_random_string
109
from django.utils.deprecation import MiddlewareMixin
1110
from django.utils.functional import cached_property
1211
from django.utils.module_loading import import_string
1312

1413
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
15-
from mozilla_django_oidc.utils import (
16-
absolutify,
17-
add_state_and_verifier_and_nonce_to_session,
18-
import_from_settings,
19-
)
14+
from mozilla_django_oidc.utils import AuthorizationCodeRequestMixin
2015

2116
LOGGER = logging.getLogger(__name__)
2217

2318

24-
class SessionRefresh(MiddlewareMixin):
19+
class SessionRefresh(MiddlewareMixin, AuthorizationCodeRequestMixin):
2520
"""Refreshes the session with the OIDC RP after expiry seconds
2621
2722
For users authenticated with the OIDC RP, verify tokens are still valid and
@@ -30,24 +25,9 @@ class SessionRefresh(MiddlewareMixin):
3025
"""
3126

3227
def __init__(self, get_response):
33-
super(SessionRefresh, self).__init__(get_response)
28+
super().__init__(get_response)
29+
self.init_settings()
3430
self.OIDC_EXEMPT_URLS = self.get_settings("OIDC_EXEMPT_URLS", [])
35-
self.OIDC_OP_AUTHORIZATION_ENDPOINT = self.get_settings(
36-
"OIDC_OP_AUTHORIZATION_ENDPOINT"
37-
)
38-
self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID")
39-
self.OIDC_STATE_SIZE = self.get_settings("OIDC_STATE_SIZE", 32)
40-
self.OIDC_AUTHENTICATION_CALLBACK_URL = self.get_settings(
41-
"OIDC_AUTHENTICATION_CALLBACK_URL",
42-
"oidc_authentication_callback",
43-
)
44-
self.OIDC_RP_SCOPES = self.get_settings("OIDC_RP_SCOPES", "openid email")
45-
self.OIDC_USE_NONCE = self.get_settings("OIDC_USE_NONCE", True)
46-
self.OIDC_NONCE_SIZE = self.get_settings("OIDC_NONCE_SIZE", 32)
47-
48-
@staticmethod
49-
def get_settings(attr, *args):
50-
return import_from_settings(attr, *args)
5131

5232
@cached_property
5333
def exempt_urls(self):
@@ -115,6 +95,11 @@ def is_refreshable_url(self, request):
11595
and not any(pat.match(request.path) for pat in self.exempt_url_patterns)
11696
)
11797

98+
def get_extra_params(self, request):
99+
extra = super().get_extra_params(request)
100+
extra.update(prompt="none")
101+
return extra
102+
118103
def process_request(self, request):
119104
if not self.is_refreshable_url(request):
120105
LOGGER.debug("request is not refreshable")
@@ -129,35 +114,11 @@ def process_request(self, request):
129114

130115
LOGGER.debug("id token has expired")
131116
# The id_token has expired, so we have to re-authenticate silently.
132-
auth_url = self.OIDC_OP_AUTHORIZATION_ENDPOINT
133-
client_id = self.OIDC_RP_CLIENT_ID
134-
state = get_random_string(self.OIDC_STATE_SIZE)
135-
136-
# Build the parameters as if we were doing a real auth handoff, except
137-
# we also include prompt=none.
138-
params = {
139-
"response_type": "code",
140-
"client_id": client_id,
141-
"redirect_uri": absolutify(
142-
request, reverse(self.OIDC_AUTHENTICATION_CALLBACK_URL)
143-
),
144-
"state": state,
145-
"scope": self.OIDC_RP_SCOPES,
146-
"prompt": "none",
147-
}
148-
149-
params.update(self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", {}))
150-
151-
if self.OIDC_USE_NONCE:
152-
nonce = get_random_string(self.OIDC_NONCE_SIZE)
153-
params.update({"nonce": nonce})
154-
155-
add_state_and_verifier_and_nonce_to_session(request, state, params)
156-
117+
redirect_url = self.get_url_for_authorization_code_request(
118+
request, quote_via=quote
119+
)
157120
request.session["oidc_login_next"] = request.get_full_path()
158121

159-
query = urlencode(params, quote_via=quote)
160-
redirect_url = "{url}?{query}".format(url=auth_url, query=query)
161122
if request.headers.get("x-requested-with") == "XMLHttpRequest":
162123
# Almost all XHR request handling in client-side code struggles
163124
# with redirects since redirecting to a page where the user

mozilla_django_oidc/utils.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
import time
33
import warnings
44
from hashlib import sha256
5+
from urllib.parse import urlencode
56
from urllib.request import parse_http_list, parse_keqv_list
67

78
# Make it obvious that these aren't the usual base64 functions
89
import josepy.b64
910
from django.conf import settings
1011
from django.core.exceptions import ImproperlyConfigured
12+
from django.urls import reverse
13+
from django.utils.crypto import get_random_string
1114

1215
LOGGER = logging.getLogger(__name__)
1316

@@ -159,3 +162,88 @@ def add_state_and_verifier_and_nonce_to_session(
159162
"nonce": nonce,
160163
"added_on": time.time(),
161164
}
165+
166+
167+
class AuthorizationCodeRequestMixin:
168+
"""
169+
Class that encapsulates the functionality required to make an authorization code request.
170+
"""
171+
172+
@staticmethod
173+
def get_settings(attr, *args):
174+
return import_from_settings(attr, *args)
175+
176+
def init_settings(self):
177+
self.OIDC_OP_AUTH_ENDPOINT = self.get_settings("OIDC_OP_AUTHORIZATION_ENDPOINT")
178+
self.OIDC_OP_AUTHORIZATION_ENDPOINT = self.OIDC_OP_AUTH_ENDPOINT
179+
self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID")
180+
self.OIDC_STATE_SIZE = self.get_settings("OIDC_STATE_SIZE", 32)
181+
self.OIDC_AUTHENTICATION_CALLBACK_URL = self.get_settings(
182+
"OIDC_AUTHENTICATION_CALLBACK_URL",
183+
"oidc_authentication_callback",
184+
)
185+
self.OIDC_RP_SCOPES = self.get_settings("OIDC_RP_SCOPES", "openid email")
186+
self.OIDC_USE_NONCE = self.get_settings("OIDC_USE_NONCE", True)
187+
self.OIDC_NONCE_SIZE = self.get_settings("OIDC_NONCE_SIZE", 32)
188+
self.OIDC_USE_PKCE = self.get_settings("OIDC_USE_PKCE", False)
189+
self.OIDC_PKCE_CODE_VERIFIER_SIZE = self.get_settings(
190+
"OIDC_PKCE_CODE_VERIFIER_SIZE", 64
191+
)
192+
193+
if not (43 <= self.OIDC_PKCE_CODE_VERIFIER_SIZE <= 128):
194+
# Check that OIDC_PKCE_CODE_VERIFIER_SIZE is between the min and max length
195+
# defined in https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
196+
raise ImproperlyConfigured(
197+
"OIDC_PKCE_CODE_VERIFIER_SIZE must be between 43 and 128"
198+
)
199+
200+
self.OIDC_PKCE_CODE_CHALLENGE_METHOD = self.get_settings(
201+
"OIDC_PKCE_CODE_CHALLENGE_METHOD", "S256"
202+
)
203+
204+
if self.OIDC_PKCE_CODE_CHALLENGE_METHOD not in ("plain", "S256"):
205+
raise ImproperlyConfigured(
206+
"OIDC_PKCE_CODE_CHALLENGE_METHOD must be 'plain' or 'S256'"
207+
)
208+
209+
def get_extra_params(self, request):
210+
return self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", {})
211+
212+
def get_url_for_authorization_code_request(self, request, **urlencode_kwargs):
213+
"""
214+
Builds and returns the URL required for the authorization code request, and
215+
also adds the state, nonce, and code verifier (if using PKCE) to the session.
216+
"""
217+
state = get_random_string(self.OIDC_STATE_SIZE)
218+
219+
params = {
220+
"response_type": "code",
221+
"scope": self.OIDC_RP_SCOPES,
222+
"client_id": self.OIDC_RP_CLIENT_ID,
223+
"redirect_uri": absolutify(
224+
request, reverse(self.OIDC_AUTHENTICATION_CALLBACK_URL)
225+
),
226+
"state": state,
227+
}
228+
229+
params.update(self.get_extra_params(request))
230+
231+
if self.OIDC_USE_NONCE:
232+
params.update(nonce=get_random_string(self.OIDC_NONCE_SIZE))
233+
234+
if self.OIDC_USE_PKCE:
235+
code_verifier = get_random_string(self.OIDC_PKCE_CODE_VERIFIER_SIZE)
236+
params.update(
237+
code_challenge=generate_code_challenge(
238+
code_verifier, self.OIDC_PKCE_CODE_CHALLENGE_METHOD
239+
),
240+
code_challenge_method=self.OIDC_PKCE_CODE_CHALLENGE_METHOD,
241+
)
242+
else:
243+
code_verifier = None
244+
245+
add_state_and_verifier_and_nonce_to_session(
246+
request, state, params, code_verifier
247+
)
248+
249+
return f"{self.OIDC_OP_AUTHORIZATION_ENDPOINT}?{urlencode(params, **urlencode_kwargs)}"

mozilla_django_oidc/views.py

Lines changed: 10 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import time
2-
from urllib.parse import urlencode
32

43
from django.contrib import auth
54
from django.core.exceptions import SuspiciousOperation
65
from django.http import HttpResponseNotAllowed, HttpResponseRedirect
76
from django.shortcuts import resolve_url
8-
from django.urls import reverse
9-
from django.utils.crypto import get_random_string
107
from django.utils.http import url_has_allowed_host_and_scheme
118
from django.utils.module_loading import import_string
129
from django.views.generic import View
1310

1411
from mozilla_django_oidc.utils import (
15-
absolutify,
16-
add_state_and_verifier_and_nonce_to_session,
17-
generate_code_challenge,
12+
AuthorizationCodeRequestMixin,
1813
import_from_settings,
1914
)
2015

@@ -159,85 +154,26 @@ def get_next_url(request, redirect_field_name):
159154
return None
160155

161156

162-
class OIDCAuthenticationRequestView(View):
157+
class OIDCAuthenticationRequestView(View, AuthorizationCodeRequestMixin):
163158
"""OIDC client authentication HTTP endpoint"""
164159

165160
http_method_names = ["get"]
166161

167162
def __init__(self, *args, **kwargs):
168-
super(OIDCAuthenticationRequestView, self).__init__(*args, **kwargs)
169-
170-
self.OIDC_OP_AUTH_ENDPOINT = self.get_settings("OIDC_OP_AUTHORIZATION_ENDPOINT")
171-
self.OIDC_RP_CLIENT_ID = self.get_settings("OIDC_RP_CLIENT_ID")
172-
173-
@staticmethod
174-
def get_settings(attr, *args):
175-
return import_from_settings(attr, *args)
163+
super().__init__(*args, **kwargs)
164+
self.init_settings()
165+
self.OIDC_REDIRECT_FIELD_NAME = self.get_settings(
166+
"OIDC_REDIRECT_FIELD_NAME", "next"
167+
)
176168

177169
def get(self, request):
178170
"""OIDC client authentication initialization HTTP endpoint"""
179-
state = get_random_string(self.get_settings("OIDC_STATE_SIZE", 32))
180-
redirect_field_name = self.get_settings("OIDC_REDIRECT_FIELD_NAME", "next")
181-
reverse_url = self.get_settings(
182-
"OIDC_AUTHENTICATION_CALLBACK_URL", "oidc_authentication_callback"
183-
)
184-
185-
params = {
186-
"response_type": "code",
187-
"scope": self.get_settings("OIDC_RP_SCOPES", "openid email"),
188-
"client_id": self.OIDC_RP_CLIENT_ID,
189-
"redirect_uri": absolutify(request, reverse(reverse_url)),
190-
"state": state,
191-
}
192-
193-
params.update(self.get_extra_params(request))
194-
195-
if self.get_settings("OIDC_USE_NONCE", True):
196-
nonce = get_random_string(self.get_settings("OIDC_NONCE_SIZE", 32))
197-
params.update({"nonce": nonce})
198-
199-
if self.get_settings("OIDC_USE_PKCE", False):
200-
code_verifier_length = self.get_settings("OIDC_PKCE_CODE_VERIFIER_SIZE", 64)
201-
# Check that code_verifier_length is between the min and max length
202-
# defined in https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
203-
if not (43 <= code_verifier_length <= 128):
204-
raise ValueError("code_verifier_length must be between 43 and 128")
205-
206-
# Generate code_verifier and code_challenge pair
207-
code_verifier = get_random_string(code_verifier_length)
208-
code_challenge_method = self.get_settings(
209-
"OIDC_PKCE_CODE_CHALLENGE_METHOD", "S256"
210-
)
211-
code_challenge = generate_code_challenge(
212-
code_verifier, code_challenge_method
213-
)
214-
215-
# Append code_challenge to authentication request parameters
216-
params.update(
217-
{
218-
"code_challenge": code_challenge,
219-
"code_challenge_method": code_challenge_method,
220-
}
221-
)
222-
223-
else:
224-
code_verifier = None
225-
226-
add_state_and_verifier_and_nonce_to_session(
227-
request, state, params, code_verifier
228-
)
229-
230-
request.session["oidc_login_next"] = get_next_url(request, redirect_field_name)
231-
232-
query = urlencode(params)
233-
redirect_url = "{url}?{query}".format(
234-
url=self.OIDC_OP_AUTH_ENDPOINT, query=query
171+
redirect_url = self.get_url_for_authorization_code_request(request)
172+
request.session["oidc_login_next"] = get_next_url(
173+
request, self.OIDC_REDIRECT_FIELD_NAME
235174
)
236175
return HttpResponseRedirect(redirect_url)
237176

238-
def get_extra_params(self, request):
239-
return self.get_settings("OIDC_AUTH_REQUEST_EXTRA_PARAMS", {})
240-
241177

242178
class OIDCLogoutView(View):
243179
"""Logout helper view"""

0 commit comments

Comments
 (0)