Skip to content

Commit 47ad2b9

Browse files
committed
Check against session oidc_state on OIDC callback.
1 parent b7c72ed commit 47ad2b9

File tree

2 files changed

+73
-1
lines changed

2 files changed

+73
-1
lines changed

mozilla_django_oidc/views.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
except ImportError:
44
from urllib.parse import urlencode
55

6+
from django.core.exceptions import SuspiciousOperation
67
from django.core.urlresolvers import reverse
78
from django.contrib import auth
89
from django.http import HttpResponseRedirect
@@ -40,6 +41,14 @@ def get(self, request):
4041
'code': request.GET['code'],
4142
'state': request.GET['state']
4243
}
44+
45+
if 'oidc_state' not in request.session:
46+
return self.login_failure()
47+
48+
if request.GET['state'] != request.session['oidc_state']:
49+
msg = 'Session `oidc_state` does not match the OIDC callback state'
50+
raise SuspiciousOperation(msg)
51+
4352
self.user = auth.authenticate(**kwargs)
4453

4554
if self.user and self.user.is_active:
@@ -60,14 +69,18 @@ def __init__(self, *args, **kwargs):
6069

6170
def get(self, request):
6271
"""OIDC client authentication initialization HTTP endpoint"""
72+
state = get_random_string(import_from_settings('OIDC_STATE_SIZE', 32))
73+
6374
params = {
6475
'response_type': 'code',
6576
'scope': 'openid',
6677
'client_id': self.OIDC_OP_CLIENT_ID,
6778
'redirect_uri': absolutify(reverse('oidc_authentication_callback')),
68-
'state': get_random_string(import_from_settings('OIDC_STATE_SIZE', 32))
79+
'state': state,
6980
}
7081

82+
request.session['oidc_state'] = state
83+
7184
query = urlencode(params)
7285
redirect_url = '{url}?{query}'.format(url=self.OIDC_OP_AUTH_ENDPOINT, query=query)
7386
return HttpResponseRedirect(redirect_url)

tests/test_views.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from mock import patch
77

8+
from django.core.exceptions import SuspiciousOperation
89
from django.contrib.auth import get_user_model
910
from django.core.urlresolvers import reverse
1011
from django.test import RequestFactory, TestCase, override_settings
@@ -32,6 +33,9 @@ def test_get_auth_success(self):
3233
}
3334
url = reverse('oidc_authentication_callback')
3435
request = self.factory.get(url, get_data)
36+
request.session = {
37+
'oidc_state': 'example_state'
38+
}
3539
callback_view = views.OIDCAuthenticationCallbackView.as_view()
3640

3741
with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
@@ -55,6 +59,9 @@ def test_get_auth_failure_nonexisting_user(self):
5559

5660
url = reverse('oidc_authentication_callback')
5761
request = self.factory.get(url, get_data)
62+
request.session = {
63+
'oidc_state': 'example_state'
64+
}
5865
callback_view = views.OIDCAuthenticationCallbackView.as_view()
5966

6067
with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
@@ -80,6 +87,9 @@ def test_get_auth_failure_inactive_user(self):
8087

8188
url = reverse('oidc_authentication_callback')
8289
request = self.factory.get(url, get_data)
90+
request.session = {
91+
'oidc_state': 'example_state'
92+
}
8393
callback_view = views.OIDCAuthenticationCallbackView.as_view()
8494

8595
with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
@@ -105,6 +115,54 @@ def test_get_auth_dirty_data(self):
105115
self.assertEqual(response.status_code, 302)
106116
self.assertEqual(response.url, '/failure')
107117

118+
@override_settings(LOGIN_REDIRECT_URL_FAILURE='/failure')
119+
def test_get_auth_failure_missing_session_state(self):
120+
"""Test authentication failure attempt for an inactive user."""
121+
user = User.objects.create_user('example_username')
122+
user.is_active = False
123+
user.save()
124+
125+
get_data = {
126+
'code': 'example_code',
127+
'state': 'example_state'
128+
}
129+
130+
url = reverse('oidc_authentication_callback')
131+
request = self.factory.get(url, get_data)
132+
request.session = {
133+
}
134+
callback_view = views.OIDCAuthenticationCallbackView.as_view()
135+
136+
response = callback_view(request)
137+
138+
self.assertEqual(response.status_code, 302)
139+
self.assertEqual(response.url, '/failure')
140+
141+
@override_settings(LOGIN_REDIRECT_URL_FAILURE='/failure')
142+
def test_get_auth_failure_tampered_session_state(self):
143+
"""Test authentication failure attempt for an inactive user."""
144+
user = User.objects.create_user('example_username')
145+
user.is_active = False
146+
user.save()
147+
148+
get_data = {
149+
'code': 'example_code',
150+
'state': 'example_state'
151+
}
152+
153+
url = reverse('oidc_authentication_callback')
154+
request = self.factory.get(url, get_data)
155+
request.session = {
156+
'oidc_state': 'tampered_state'
157+
}
158+
callback_view = views.OIDCAuthenticationCallbackView.as_view()
159+
160+
with self.assertRaises(SuspiciousOperation) as context:
161+
callback_view(request)
162+
163+
expected_error_message = 'Session `oidc_state` does not match the OIDC callback state'
164+
self.assertEqual(context.exception.args, (expected_error_message,))
165+
108166

109167
class OIDCAuthorizationRequestViewTestCase(TestCase):
110168
def setUp(self):
@@ -119,6 +177,7 @@ def test_get(self, mock_random_string):
119177
mock_random_string.return_value = 'examplestring'
120178
url = reverse('oidc_authentication_init')
121179
request = self.factory.get(url)
180+
request.session = dict()
122181
login_view = views.OIDCAuthenticationRequestView.as_view()
123182
response = login_view(request)
124183
self.assertEqual(response.status_code, 302)

0 commit comments

Comments
 (0)