1
+ import json
1
2
import logging
2
3
import time
3
4
4
- from django .contrib . auth import BACKEND_SESSION_KEY
5
+ from django .contrib import auth
5
6
from django .http import HttpResponseRedirect , JsonResponse
6
7
from django .urls import reverse
7
8
from django .utils .crypto import get_random_string
8
9
from django .utils .deprecation import MiddlewareMixin
9
10
from django .utils .functional import cached_property
10
11
from django .utils .module_loading import import_string
12
+ import requests
13
+ from requests .auth import HTTPBasicAuth
11
14
12
- from mozilla_django_oidc .auth import OIDCAuthenticationBackend
15
+ from mozilla_django_oidc .auth import OIDCAuthenticationBackend , store_tokens
13
16
from mozilla_django_oidc .utils import (absolutify ,
14
17
add_state_and_nonce_to_session ,
15
18
import_from_settings )
@@ -95,6 +98,11 @@ def exempt_url_patterns(self):
95
98
exempt_patterns .add (url_pattern )
96
99
return exempt_patterns
97
100
101
+ @property
102
+ def logout_redirect_url (self ):
103
+ """Return the logout url defined in settings."""
104
+ return self .get_settings ('LOGOUT_REDIRECT_URL' , '/' )
105
+
98
106
def is_refreshable_url (self , request ):
99
107
"""Takes a request and returns whether it triggers a refresh examination
100
108
@@ -104,7 +112,7 @@ def is_refreshable_url(self, request):
104
112
105
113
"""
106
114
# Do not attempt to refresh the session if the OIDC backend is not used
107
- backend_session = request .session .get (BACKEND_SESSION_KEY )
115
+ backend_session = request .session .get (auth . BACKEND_SESSION_KEY )
108
116
is_oidc_enabled = True
109
117
if backend_session :
110
118
auth_backend = import_string (backend_session )
@@ -118,27 +126,71 @@ def is_refreshable_url(self, request):
118
126
not any (pat .match (request .path ) for pat in self .exempt_url_patterns )
119
127
)
120
128
121
- def process_request (self , request ):
129
+ def is_expired (self , request ):
122
130
if not self .is_refreshable_url (request ):
123
131
LOGGER .debug ('request is not refreshable' )
124
- return
132
+ return False
125
133
126
- expiration = request .session .get ('oidc_id_token_expiration ' , 0 )
134
+ expiration = request .session .get ('oidc_token_expiration ' , 0 )
127
135
now = time .time ()
128
136
if expiration > now :
129
137
# The id_token is still valid, so we don't have to do anything.
130
138
LOGGER .debug ('id token is still valid (%s > %s)' , expiration , now )
139
+ return False
140
+
141
+ return True
142
+
143
+ def process_request (self , request ):
144
+ if not self .is_expired (request ):
131
145
return
132
146
133
147
LOGGER .debug ('id token has expired' )
134
- # The id_token has expired, so we have to re-authenticate silently.
148
+ return self .finish (request , prompt_reauth = True )
149
+
150
+ def finish (self , request , prompt_reauth = True ):
151
+ """Finish request handling and handle sending downstream responses for XHR.
152
+
153
+ This function should only be run if the session is determind to
154
+ be expired.
155
+
156
+ Almost all XHR request handling in client-side code struggles
157
+ with redirects since redirecting to a page where the user
158
+ is supposed to do something is extremely unlikely to work
159
+ in an XHR request. Make a special response for these kinds
160
+ of requests.
161
+
162
+ The use of 403 Forbidden is to match the fact that this
163
+ middleware doesn't really want the user in if they don't
164
+ refresh their session.
165
+ """
166
+ default_response = None
167
+ xhr_response_json = {'error' : 'the authentication session has expired' }
168
+ if prompt_reauth :
169
+ # The id_token has expired, so we have to re-authenticate silently.
170
+ refresh_url = self ._prepare_reauthorization (request )
171
+ default_response = HttpResponseRedirect (refresh_url )
172
+ xhr_response_json ['refresh_url' ] = refresh_url
173
+
174
+ if request .headers .get ('x-requested-with' ) == 'XMLHttpRequest' :
175
+ xhr_response = JsonResponse (xhr_response_json , status = 403 )
176
+ if 'refresh_url' in xhr_response_json :
177
+ xhr_response ['refresh_url' ] = xhr_response_json ['refresh_url' ]
178
+ return xhr_response
179
+ else :
180
+ return default_response
181
+
182
+ def _prepare_reauthorization (self , request ):
183
+ # Constructs a new authorization grant request to refresh the session.
184
+ # Besides constructing the request, the state and nonce included in the
185
+ # request are registered in the current session in preparation for the
186
+ # client following through with the authorization flow.
135
187
auth_url = self .OIDC_OP_AUTHORIZATION_ENDPOINT
136
188
client_id = self .OIDC_RP_CLIENT_ID
137
189
state = get_random_string (self .OIDC_STATE_SIZE )
138
190
139
191
# Build the parameters as if we were doing a real auth handoff, except
140
192
# we also include prompt=none.
141
- params = {
193
+ auth_params = {
142
194
'response_type' : 'code' ,
143
195
'client_id' : client_id ,
144
196
'redirect_uri' : absolutify (
@@ -152,26 +204,83 @@ def process_request(self, request):
152
204
153
205
if self .OIDC_USE_NONCE :
154
206
nonce = get_random_string (self .OIDC_NONCE_SIZE )
155
- params .update ({
207
+ auth_params .update ({
156
208
'nonce' : nonce
157
209
})
158
210
159
- add_state_and_nonce_to_session ( request , state , params )
160
-
211
+ # Register the one-time parameters in the session
212
+ add_state_and_nonce_to_session ( request , state , auth_params )
161
213
request .session ['oidc_login_next' ] = request .get_full_path ()
162
214
163
- query = urlencode (params , quote_via = quote )
164
- redirect_url = '{url}?{query}' .format (url = auth_url , query = query )
165
- if request .headers .get ('x-requested-with' ) == 'XMLHttpRequest' :
166
- # Almost all XHR request handling in client-side code struggles
167
- # with redirects since redirecting to a page where the user
168
- # is supposed to do something is extremely unlikely to work
169
- # in an XHR request. Make a special response for these kinds
170
- # of requests.
171
- # The use of 403 Forbidden is to match the fact that this
172
- # middleware doesn't really want the user in if they don't
173
- # refresh their session.
174
- response = JsonResponse ({'refresh_url' : redirect_url }, status = 403 )
175
- response ['refresh_url' ] = redirect_url
176
- return response
177
- return HttpResponseRedirect (redirect_url )
215
+ query = urlencode (auth_params , quote_via = quote )
216
+ return '{auth_url}?{query}' .format (auth_url = auth_url , query = query )
217
+
218
+
219
+ class RefreshOIDCAccessToken (SessionRefresh ):
220
+ """
221
+ A middleware that will refresh the access token following proper OIDC protocol:
222
+ https://auth0.com/docs/tokens/refresh-token/current
223
+ """
224
+ def process_request (self , request ):
225
+ if not self .is_expired (request ):
226
+ return
227
+
228
+ token_url = import_from_settings ('OIDC_OP_TOKEN_ENDPOINT' )
229
+ client_id = import_from_settings ('OIDC_RP_CLIENT_ID' )
230
+ client_secret = import_from_settings ('OIDC_RP_CLIENT_SECRET' )
231
+ refresh_token = request .session .get ('oidc_refresh_token' )
232
+
233
+ if not refresh_token :
234
+ LOGGER .debug ('no refresh token stored' )
235
+ return self .finish (request , prompt_reauth = True )
236
+
237
+ token_payload = {
238
+ 'grant_type' : 'refresh_token' ,
239
+ 'client_id' : client_id ,
240
+ 'client_secret' : client_secret ,
241
+ 'refresh_token' : refresh_token ,
242
+ }
243
+
244
+ req_auth = None
245
+ if self .get_settings ('OIDC_TOKEN_USE_BASIC_AUTH' , False ):
246
+ # When Basic auth is defined, create the Auth Header and remove secret from payload.
247
+ user = token_payload .get ('client_id' )
248
+ pw = token_payload .get ('client_secret' )
249
+
250
+ req_auth = HTTPBasicAuth (user , pw )
251
+ del token_payload ['client_secret' ]
252
+
253
+ try :
254
+ response = requests .post (
255
+ token_url ,
256
+ auth = req_auth ,
257
+ data = token_payload ,
258
+ verify = import_from_settings ('OIDC_VERIFY_SSL' , True )
259
+ )
260
+ response .raise_for_status ()
261
+ token_info = response .json ()
262
+ except requests .exceptions .Timeout :
263
+ LOGGER .debug ('timed out refreshing access token' )
264
+ # Don't prompt for reauth as this could be a temporary problem
265
+ return self .finish (request , prompt_reauth = False )
266
+ except requests .exceptions .HTTPError as exc :
267
+ status_code = exc .response .status_code
268
+ LOGGER .debug ('http error %s when refreshing access token' , status_code )
269
+ return self .finish (request , prompt_reauth = (status_code == 401 ))
270
+ except json .JSONDecodeError :
271
+ LOGGER .debug ('malformed response when refreshing access token' )
272
+ # Don't prompt for reauth as this could be a temporary problem
273
+ return self .finish (request , prompt_reauth = False )
274
+ except Exception as exc :
275
+ LOGGER .debug (
276
+ 'unknown error occurred when refreshing access token: %s' , exc )
277
+ # Don't prompt for reauth as this could be a temporary problem
278
+ return self .finish (request , prompt_reauth = False )
279
+
280
+ # Until we can properly validate an ID token on the refresh response
281
+ # per the spec[1], we intentionally drop the id_token.
282
+ # [1]: https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse
283
+ id_token = None
284
+ access_token = token_info .get ('access_token' )
285
+ refresh_token = token_info .get ('refresh_token' )
286
+ store_tokens (request .session , access_token , id_token , refresh_token )
0 commit comments