Skip to content

Commit 3f4ba3f

Browse files
authored
Merge pull request #60 from johngian/middleware-logout
Delegate token middleware
2 parents ca398f3 + 84d0e61 commit 3f4ba3f

File tree

5 files changed

+136
-7
lines changed

5 files changed

+136
-7
lines changed

mozilla_django_oidc/auth.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def authenticate(self, **kwargs):
8989
code = kwargs.pop('code', None)
9090
state = kwargs.pop('state', None)
9191
nonce = kwargs.pop('nonce', None)
92+
session = kwargs.pop('session', None)
9293

9394
if not code or not state:
9495
return None
@@ -109,8 +110,13 @@ def authenticate(self, **kwargs):
109110

110111
# Validate the token
111112
token_response = response.json()
112-
if self.verify_token(token_response.get('id_token'), nonce=nonce):
113+
id_token = token_response.get('id_token')
114+
if self.verify_token(id_token, nonce=nonce):
113115
access_token = token_response.get('access_token')
116+
117+
if import_from_settings('OIDC_STORE_ACCESS_TOKEN', False):
118+
session['oidc_id_token'] = id_token
119+
114120
user_response = requests.get(self.OIDC_OP_USER_ENDPOINT,
115121
headers={
116122
'Authorization': 'Bearer {0}'.format(access_token)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.core.cache import cache
2+
3+
from six import string_types
4+
5+
from mozilla_django_oidc.contrib.auth0.utils import refresh_id_token
6+
from mozilla_django_oidc.utils import import_from_settings
7+
from mozilla_django_oidc.views import OIDCLogoutView
8+
9+
10+
class RefreshIDToken(object):
11+
"""
12+
Bluntly stolen from mozilla/airmozilla
13+
14+
For users authenticated with an id_token, we need to check that it's
15+
still valid after a specific amount of time.
16+
"""
17+
18+
def process_request(self, request):
19+
if request.user.is_authenticated() and not request.is_ajax():
20+
21+
if 'oidc_id_token' not in request.session:
22+
return None
23+
24+
cache_key = 'renew_id_token:{}'.format(request.user.id)
25+
if cache.get(cache_key):
26+
# still valid, we checked recently
27+
return
28+
29+
id_token = refresh_id_token(request.session['oidc_id_token'])
30+
31+
if id_token:
32+
assert isinstance(id_token, string_types)
33+
request.session['oidc_id_token'] = id_token
34+
timeout = import_from_settings('OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS', 60 * 15)
35+
cache.set(cache_key, True, timeout)
36+
else:
37+
# If that failed, your previous id_token is not valid
38+
# and you need to be signed out so you can get a new
39+
# one.
40+
return OIDCLogoutView.as_view()(request)

mozilla_django_oidc/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def get(self, request):
4646
kwargs = {
4747
'code': request.GET['code'],
4848
'state': request.GET['state'],
49-
'nonce': nonce
49+
'nonce': nonce,
50+
'session': request.session
5051
}
5152

5253
if 'oidc_state' not in request.session:
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from mock import patch
2+
3+
from django.contrib.auth import get_user_model
4+
from django.test import RequestFactory, TestCase, override_settings
5+
6+
from mozilla_django_oidc.contrib.auth0.middleware import RefreshIDToken
7+
8+
9+
User = get_user_model()
10+
11+
12+
class RefreshIDTokenTestCase(TestCase):
13+
def setUp(self):
14+
self.factory = RequestFactory()
15+
16+
def test_user_token_not_in_session(self):
17+
user = User.objects.create_user('example_username')
18+
request = self.factory.get('/foo')
19+
request.user = user
20+
request.session = dict()
21+
22+
middleware = RefreshIDToken()
23+
response = middleware.process_request(request)
24+
self.assertTrue(not response)
25+
26+
@patch('mozilla_django_oidc.contrib.auth0.middleware.cache')
27+
def test_user_token_in_session(self, mock_cache):
28+
user = User.objects.create_user('example_username')
29+
request = self.factory.get('/foo')
30+
request.user = user
31+
request.session = {
32+
'oidc_id_token': 'foobar'
33+
}
34+
mock_cache.get.return_value = True
35+
middleware = RefreshIDToken()
36+
response = middleware.process_request(request)
37+
self.assertTrue(not response)
38+
39+
@patch('mozilla_django_oidc.contrib.auth0.middleware.refresh_id_token')
40+
@patch('mozilla_django_oidc.contrib.auth0.middleware.cache')
41+
@override_settings(OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS=120)
42+
def test_stale_cache_valid_token(self, mock_cache, mock_refresh):
43+
user = User.objects.create_user('example_username')
44+
request = self.factory.get('/foo')
45+
request.user = user
46+
request.session = {
47+
'oidc_id_token': 'foobar'
48+
}
49+
mock_cache.get.return_value = False
50+
mock_refresh.return_value = 'renewed_token'
51+
middleware = RefreshIDToken()
52+
response = middleware.process_request(request)
53+
self.assertTrue(not response)
54+
cache_key = 'renew_id_token:{}'.format(user.id)
55+
mock_refresh.assert_called_once_with('foobar')
56+
mock_cache.set.assert_called_once_with(cache_key, True, 120)
57+
58+
@patch('mozilla_django_oidc.views.auth.logout')
59+
@patch('mozilla_django_oidc.contrib.auth0.middleware.refresh_id_token')
60+
@patch('mozilla_django_oidc.contrib.auth0.middleware.cache')
61+
@override_settings(LOGOUT_REDIRECT_URL='/logout_url')
62+
@override_settings(OIDC_RENEW_ID_TOKEN_EXPIRY_SECONDS=120)
63+
def test_stale_cache_invalid_token(self, mock_cache, mock_refresh, mock_logout):
64+
user = User.objects.create_user('example_username')
65+
request = self.factory.get('/foo')
66+
request.user = user
67+
request.session = {
68+
'oidc_id_token': 'foobar'
69+
}
70+
mock_cache.get.return_value = False
71+
mock_refresh.return_value = None
72+
middleware = RefreshIDToken()
73+
response = middleware.process_request(request)
74+
self.assertEqual(response.status_code, 302)
75+
self.assertEqual(response.url, '/logout_url')
76+
mock_refresh.assert_called_once_with('foobar')
77+
mock_logout.assert_called_once_with(request)

tests/test_views.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ def test_get_auth_success(self):
4343

4444
mock_auth.assert_called_once_with(code='example_code',
4545
state='example_state',
46-
nonce=None)
46+
nonce=None,
47+
session=request.session)
4748
mock_login.assert_called_once_with(request, user)
4849

4950
self.assertEqual(response.status_code, 302)
@@ -73,7 +74,8 @@ def test_get_auth_success_next_url(self):
7374

7475
mock_auth.assert_called_once_with(code='example_code',
7576
state='example_state',
76-
nonce=None)
77+
nonce=None,
78+
session=request.session)
7779
mock_login.assert_called_once_with(request, user)
7880

7981
self.assertEqual(response.status_code, 302)
@@ -100,7 +102,8 @@ def test_get_auth_failure_nonexisting_user(self):
100102

101103
mock_auth.assert_called_once_with(code='example_code',
102104
state='example_state',
103-
nonce=None)
105+
nonce=None,
106+
session=request.session)
104107

105108
self.assertEqual(response.status_code, 302)
106109
self.assertEqual(response.url, '/failure')
@@ -130,7 +133,8 @@ def test_get_auth_failure_inactive_user(self):
130133

131134
mock_auth.assert_called_once_with(code='example_code',
132135
state='example_state',
133-
nonce=None)
136+
nonce=None,
137+
session=request.session)
134138

135139
self.assertEqual(response.status_code, 302)
136140
self.assertEqual(response.url, '/failure')
@@ -223,7 +227,8 @@ def test_nonce_is_deleted(self):
223227

224228
mock_auth.assert_called_once_with(code='example_code',
225229
state='example_state',
226-
nonce='example_nonce')
230+
nonce='example_nonce',
231+
session=request.session)
227232
mock_login.assert_called_once_with(request, user)
228233

229234
self.assertEqual(response.status_code, 302)

0 commit comments

Comments
 (0)