Skip to content

Commit 3de576c

Browse files
committed
Added middleware to refresh access tokens
1 parent 80adc32 commit 3de576c

File tree

4 files changed

+101
-54
lines changed

4 files changed

+101
-54
lines changed

django_auth_adfs/backend.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
2+
from datetime import datetime, timedelta
23

34
import jwt
4-
from django.contrib.auth import get_user_model
5+
from django.contrib.auth import get_user_model, logout
56
from django.contrib.auth.backends import ModelBackend
67
from django.contrib.auth.models import Group
78
from django.core.exceptions import (ImproperlyConfigured, ObjectDoesNotExist,
89
PermissionDenied)
10+
from requests import HTTPError
911

1012
from django_auth_adfs import signals
1113
from django_auth_adfs.config import provider_config, settings
@@ -398,10 +400,38 @@ def authenticate(self, request=None, authorization_code=None, **kwargs):
398400
provider_config.load_config()
399401

400402
adfs_response = self.exchange_auth_code(authorization_code, request)
401-
access_token = adfs_response["access_token"]
402-
user = self.process_access_token(access_token, adfs_response)
403+
user = self._process_adfs_response(request, adfs_response)
403404
return user
404405

406+
def _process_adfs_response(self, request, adfs_response):
407+
user = self.process_access_token(adfs_response['access_token'], adfs_response)
408+
request.session['adfs_access_token'] = adfs_response['access_token']
409+
expiry = datetime.now() + timedelta(seconds=adfs_response['expires_in'])
410+
request.session['adfs_token_expiry'] = expiry.isoformat()
411+
if 'refresh_token' in adfs_response:
412+
request.session['adfs_refresh_token'] = adfs_response['refresh_token']
413+
request.session.save()
414+
return user
415+
416+
def process_request(self, request):
417+
now = datetime.now() + settings.REFRESH_THRESHOLD
418+
expiry = datetime.fromisoformat(request.session['adfs_token_expiry'])
419+
if now > expiry:
420+
try:
421+
self._refresh_access_token(request, request.session['adfs_refresh_token'])
422+
except (PermissionDenied, HTTPError):
423+
logout(request)
424+
425+
def _refresh_access_token(self, request, refresh_token):
426+
provider_config.load_config()
427+
response = provider_config.session.post(
428+
provider_config.token_endpoint,
429+
data=f'grant_type=refresh_token&refresh_token={refresh_token}'
430+
)
431+
response.raise_for_status()
432+
adfs_response = response.json()
433+
self._process_adfs_response(request, adfs_response)
434+
405435

406436
class AdfsAccessTokenBackend(AdfsBaseBackend):
407437
"""

django_auth_adfs/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(self):
7272
self.USERNAME_CLAIM = "winaccountname"
7373
self.GUEST_USERNAME_CLAIM = None
7474
self.JWT_LEEWAY = 0
75+
self.REFRESH_THRESHOLD = timedelta(minutes=5)
7576
self.CUSTOM_FAILED_RESPONSE_VIEW = lambda request, error_message, status: render(
7677
request, 'django_auth_adfs/login_failed.html', {'error_message': error_message}, status=status
7778
)

django_auth_adfs/middleware.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from re import compile
55

66
from django.conf import settings as django_settings
7+
from django.contrib import auth
78
from django.contrib.auth.views import redirect_to_login
89
from django.urls import reverse
910

11+
from django_auth_adfs.backend import AdfsAuthCodeBackend
1012
from django_auth_adfs.exceptions import MFARequired
1113
from django_auth_adfs.config import settings
1214

@@ -49,3 +51,17 @@ def __call__(self, request):
4951
return redirect_to_login('django_auth_adfs:login-force-mfa')
5052

5153
return self.get_response(request)
54+
55+
56+
def adfs_refresh_middleware(get_response):
57+
def middleware(request):
58+
try:
59+
backend_str = request.session[auth.BACKEND_SESSION_KEY]
60+
except KeyError:
61+
pass
62+
else:
63+
backend = auth.load_backend(backend_str)
64+
if isinstance(backend, AdfsAuthCodeBackend):
65+
backend.process_request(request)
66+
return get_response()
67+
return middleware

tests/test_authentication.py

Lines changed: 51 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import base64
22

3+
from django.urls import reverse
4+
35
from django_auth_adfs.exceptions import MFARequired
46

57
try:
@@ -16,7 +18,6 @@
1618
from mock import Mock, patch
1719

1820
from django_auth_adfs import signals
19-
from django_auth_adfs.backend import AdfsAuthCodeBackend
2021
from django_auth_adfs.config import ProviderConfig, Settings
2122

2223
from .models import Profile
@@ -34,14 +35,13 @@ def setUp(self):
3435

3536
@mock_adfs("2012")
3637
def test_post_authenticate_signal_send(self):
37-
backend = AdfsAuthCodeBackend()
38-
backend.authenticate(self.request, authorization_code="dummycode")
38+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
3939
self.assertEqual(self.signal_handler.call_count, 1)
4040

4141
@mock_adfs("2012")
4242
def test_with_auth_code_2012(self):
43-
backend = AdfsAuthCodeBackend()
44-
user = backend.authenticate(self.request, authorization_code="dummycode")
43+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
44+
user = response.wsgi_request.user
4545
self.assertIsInstance(user, User)
4646
self.assertEqual(user.first_name, "John")
4747
self.assertEqual(user.last_name, "Doe")
@@ -52,8 +52,8 @@ def test_with_auth_code_2012(self):
5252

5353
@mock_adfs("2016")
5454
def test_with_auth_code_2016(self):
55-
backend = AdfsAuthCodeBackend()
56-
user = backend.authenticate(self.request, authorization_code="dummycode")
55+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
56+
user = response.wsgi_request.user
5757
self.assertIsInstance(user, User)
5858
self.assertEqual(user.first_name, "John")
5959
self.assertEqual(user.last_name, "Doe")
@@ -64,9 +64,15 @@ def test_with_auth_code_2016(self):
6464

6565
@mock_adfs("2016", mfa_error=True)
6666
def test_mfa_error_backends(self):
67-
with self.assertRaises(MFARequired):
68-
backend = AdfsAuthCodeBackend()
69-
backend.authenticate(self.request, authorization_code="dummycode")
67+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
68+
self.assertEqual(response.status_code, 302)
69+
self.assertEqual(
70+
response['Location'],
71+
"https://adfs.example.com/adfs/oauth2/authorize/?response_type=code&"
72+
"client_id=your-configured-client-id&resource=your-adfs-RPT-name&"
73+
"redirect_uri=http%3A%2F%2Ftestserver%2Foauth2%2Fcallback&state=Lw%3D%3D&scope=openid&"
74+
"amr_values=ngcmfa"
75+
)
7076

7177
@mock_adfs("azure")
7278
def test_with_auth_code_azure(self):
@@ -77,8 +83,8 @@ def test_with_auth_code_azure(self):
7783
with patch("django_auth_adfs.config.django_settings", settings):
7884
with patch("django_auth_adfs.config.settings", Settings()):
7985
with patch("django_auth_adfs.backend.provider_config", ProviderConfig()):
80-
backend = AdfsAuthCodeBackend()
81-
user = backend.authenticate(self.request, authorization_code="dummycode")
86+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
87+
user = response.wsgi_request.user
8288
self.assertIsInstance(user, User)
8389
self.assertEqual(user.first_name, "John")
8490
self.assertEqual(user.last_name, "Doe")
@@ -100,9 +106,8 @@ def test_with_auth_code_azure_guest_block(self):
100106
with patch('django_auth_adfs.backend.settings', Settings()):
101107
with patch("django_auth_adfs.config.settings", Settings()):
102108
with patch("django_auth_adfs.backend.provider_config", ProviderConfig()):
103-
with self.assertRaises(PermissionDenied, msg=''):
104-
backend = AdfsAuthCodeBackend()
105-
_ = backend.authenticate(self.request, authorization_code="dummycode")
109+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
110+
self.assertEqual(response.status_code, 401)
106111

107112
@mock_adfs("azure", guest=True)
108113
def test_with_auth_code_azure_guest_no_block(self):
@@ -117,8 +122,8 @@ def test_with_auth_code_azure_guest_no_block(self):
117122
with patch('django_auth_adfs.backend.settings', Settings()):
118123
with patch("django_auth_adfs.config.settings", Settings()):
119124
with patch("django_auth_adfs.backend.provider_config", ProviderConfig()):
120-
backend = AdfsAuthCodeBackend()
121-
user = backend.authenticate(self.request, authorization_code="dummycode")
125+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
126+
user = response.wsgi_request.user
122127
self.assertIsInstance(user, User)
123128
self.assertEqual(user.first_name, "John")
124129
self.assertEqual(user.last_name, "Doe")
@@ -139,8 +144,8 @@ def test_version_two_endpoint_calls_correct_url(self):
139144
with patch('django_auth_adfs.backend.settings', Settings()):
140145
with patch("django_auth_adfs.config.settings", Settings()):
141146
with patch("django_auth_adfs.backend.provider_config", ProviderConfig()):
142-
backend = AdfsAuthCodeBackend()
143-
user = backend.authenticate(self.request, authorization_code="dummycode")
147+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
148+
user = response.wsgi_request.user
144149
self.assertIsInstance(user, User)
145150
self.assertEqual(user.first_name, "John")
146151
self.assertEqual(user.last_name, "Doe")
@@ -151,14 +156,15 @@ def test_version_two_endpoint_calls_correct_url(self):
151156

152157
@mock_adfs("2016")
153158
def test_empty(self):
154-
backend = AdfsAuthCodeBackend()
155-
self.assertIsNone(backend.authenticate(self.request))
159+
response = self.client.get(reverse('django_auth_adfs:callback'))
160+
user = response.wsgi_request.user
161+
self.assertTrue(user.is_anonymous)
156162

157163
@mock_adfs("2016")
158164
def test_group_claim(self):
159-
backend = AdfsAuthCodeBackend()
160165
with patch("django_auth_adfs.backend.settings.GROUPS_CLAIM", "nonexisting"):
161-
user = backend.authenticate(self.request, authorization_code="dummycode")
166+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
167+
user = response.wsgi_request.user
162168
self.assertIsInstance(user, User)
163169
self.assertEqual(user.first_name, "John")
164170
self.assertEqual(user.last_name, "Doe")
@@ -167,9 +173,9 @@ def test_group_claim(self):
167173

168174
@mock_adfs("2016")
169175
def test_no_group_claim(self):
170-
backend = AdfsAuthCodeBackend()
171176
with patch("django_auth_adfs.backend.settings.GROUPS_CLAIM", None):
172-
user = backend.authenticate(self.request, authorization_code="dummycode")
177+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
178+
user = response.wsgi_request.user
173179
self.assertIsInstance(user, User)
174180
self.assertEqual(user.first_name, "John")
175181
self.assertEqual(user.last_name, "Doe")
@@ -181,9 +187,9 @@ def test_group_claim_with_mirror_groups(self):
181187
# Remove one group
182188
Group.objects.filter(name="group1").delete()
183189

184-
backend = AdfsAuthCodeBackend()
185190
with patch("django_auth_adfs.backend.settings.MIRROR_GROUPS", True):
186-
user = backend.authenticate(self.request, authorization_code="dummycode")
191+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
192+
user = response.wsgi_request.user
187193
self.assertIsInstance(user, User)
188194
self.assertEqual(user.first_name, "John")
189195
self.assertEqual(user.last_name, "Doe")
@@ -197,9 +203,9 @@ def test_group_claim_without_mirror_groups(self):
197203
# Remove one group
198204
Group.objects.filter(name="group1").delete()
199205

200-
backend = AdfsAuthCodeBackend()
201206
with patch("django_auth_adfs.backend.settings.MIRROR_GROUPS", False):
202-
user = backend.authenticate(self.request, authorization_code="dummycode")
207+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
208+
user = response.wsgi_request.user
203209
self.assertIsInstance(user, User)
204210
self.assertEqual(user.first_name, "John")
205211
self.assertEqual(user.last_name, "Doe")
@@ -210,9 +216,9 @@ def test_group_claim_without_mirror_groups(self):
210216

211217
@mock_adfs("2016", empty_keys=True)
212218
def test_empty_keys(self):
213-
backend = AdfsAuthCodeBackend()
214219
with patch("django_auth_adfs.config.provider_config.signing_keys", []):
215-
self.assertRaises(PermissionDenied, backend.authenticate, self.request, authorization_code='testcode')
220+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "testcode"})
221+
self.assertEqual(response.status_code, 401)
216222

217223
@mock_adfs("2016")
218224
def test_group_removal(self):
@@ -227,9 +233,8 @@ def test_group_removal(self):
227233
self.assertEqual(user.groups.all()[0].name, "group3")
228234
self.assertEqual(len(user.groups.all()), 1)
229235

230-
backend = AdfsAuthCodeBackend()
231-
232-
user = backend.authenticate(self.request, authorization_code="dummycode")
236+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
237+
user = response.wsgi_request.user
233238
self.assertIsInstance(user, User)
234239
self.assertEqual(user.first_name, "John")
235240
self.assertEqual(user.last_name, "Doe")
@@ -253,9 +258,8 @@ def test_group_removal_overlap(self):
253258
self.assertEqual(user.groups.all()[1].name, "group3")
254259
self.assertEqual(len(user.groups.all()), 2)
255260

256-
backend = AdfsAuthCodeBackend()
257-
258-
user = backend.authenticate(self.request, authorization_code="dummycode")
261+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
262+
user = response.wsgi_request.user
259263
self.assertIsInstance(user, User)
260264
self.assertEqual(user.first_name, "John")
261265
self.assertEqual(user.last_name, "Doe")
@@ -272,9 +276,8 @@ def test_group_to_flag_mapping(self):
272276
}
273277
with patch("django_auth_adfs.backend.settings.GROUP_TO_FLAG_MAPPING", group_to_flag_mapping):
274278
with patch("django_auth_adfs.backend.settings.BOOLEAN_CLAIM_MAPPING", {}):
275-
backend = AdfsAuthCodeBackend()
276-
277-
user = backend.authenticate(self.request, authorization_code="dummycode")
279+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
280+
user = response.wsgi_request.user
278281
self.assertIsInstance(user, User)
279282
self.assertEqual(user.first_name, "John")
280283
self.assertEqual(user.last_name, "Doe")
@@ -289,9 +292,8 @@ def test_boolean_claim_mapping(self):
289292
"is_superuser": "user_is_superuser",
290293
}
291294
with patch("django_auth_adfs.backend.settings.BOOLEAN_CLAIM_MAPPING", boolean_claim_mapping):
292-
backend = AdfsAuthCodeBackend()
293-
294-
user = backend.authenticate(self.request, authorization_code="dummycode")
295+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
296+
user = response.wsgi_request.user
295297
self.assertIsInstance(user, User)
296298
self.assertEqual(user.first_name, "John")
297299
self.assertEqual(user.last_name, "Doe")
@@ -312,9 +314,8 @@ def test_extended_model_claim_mapping_missing_instance(self):
312314
},
313315
}
314316
with patch("django_auth_adfs.backend.settings.CLAIM_MAPPING", claim_mapping):
315-
backend = AdfsAuthCodeBackend()
316-
317-
user = backend.authenticate(self.request, authorization_code="dummycode")
317+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
318+
user = response.wsgi_request.user
318319
self.assertIsInstance(user, User)
319320
self.assertEqual(user.first_name, "John")
320321
self.assertEqual(user.last_name, "Doe")
@@ -340,9 +341,8 @@ def create_profile(sender, instance, created, **kwargs):
340341
},
341342
}
342343
with patch("django_auth_adfs.backend.settings.CLAIM_MAPPING", claim_mapping):
343-
backend = AdfsAuthCodeBackend()
344-
345-
user = backend.authenticate(self.request, authorization_code="dummycode")
344+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "dummycode"})
345+
user = response.wsgi_request.user
346346
self.assertIsInstance(user, User)
347347
self.assertEqual(user.first_name, "John")
348348
self.assertEqual(user.last_name, "Doe")
@@ -493,5 +493,5 @@ def test_nonexisting_user(self):
493493
settings.AUTH_ADFS["CREATE_NEW_USERS"] = False
494494
with patch("django_auth_adfs.config.django_settings", settings),\
495495
patch("django_auth_adfs.backend.settings", Settings()):
496-
backend = AdfsAuthCodeBackend()
497-
self.assertRaises(PermissionDenied, backend.authenticate, self.request, authorization_code='testcode')
496+
response = self.client.get(reverse('django_auth_adfs:callback'), data={'code': "testcode"})
497+
self.assertEqual(response.status_code, 401)

0 commit comments

Comments
 (0)