Skip to content

Commit 75341f4

Browse files
authored
Merge pull request #29 from johngian/multiple-fixes
Multiple fixes
2 parents fdc7ff5 + 727ace3 commit 75341f4

File tree

9 files changed

+144
-73
lines changed

9 files changed

+144
-73
lines changed

mozilla_django_oidc/auth.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import base64
12
import jwt
23
import requests
4+
35
try:
46
from urllib import urlencode
57
except ImportError:
@@ -8,7 +10,7 @@
810
from django.contrib.auth import get_user_model
911
from django.core.urlresolvers import reverse
1012

11-
from mozilla_django_oidc.utils import import_from_settings
13+
from mozilla_django_oidc.utils import absolutify, import_from_settings
1214

1315

1416
class OIDCAuthenticationBackend(object):
@@ -26,9 +28,16 @@ def __init__(self, *args, **kwargs):
2628
def verify_token(self, token, **kwargs):
2729
"""Validate the token signature."""
2830

29-
return jwt.decode(token,
30-
self.OIDC_OP_CLIENT_SECRET,
31-
verify=import_from_settings('OIDC_VERIFY_JWT', True))
31+
# Get JWT audience without signature verification
32+
audience = jwt.decode(token, verify=False)['aud']
33+
34+
secret = self.OIDC_OP_CLIENT_SECRET
35+
if import_from_settings('OIDC_RP_CLIENT_SECRET_ENCODED', False):
36+
secret = base64.urlsafe_b64decode(self.OIDC_OP_CLIENT_SECRET)
37+
38+
return jwt.decode(token, secret,
39+
verify=import_from_settings('OIDC_VERIFY_JWT', True),
40+
audience=audience)
3241

3342
def authenticate(self, code=None, state=None):
3443
"""Authenticates a user based on the OIDC code flow."""
@@ -39,30 +48,32 @@ def authenticate(self, code=None, state=None):
3948
token_payload = {
4049
'client_id': self.OIDC_OP_CLIENT_ID,
4150
'client_secret': self.OIDC_OP_CLIENT_SECRET,
42-
'grand_type': 'authorization_code',
51+
'grant_type': 'authorization_code',
4352
'code': code,
44-
'redirect_url': reverse('oidc_authentication_callback')
53+
'redirect_uri': absolutify(reverse('oidc_authentication_callback'))
4554
}
4655

4756
# Get the token
4857
response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT,
49-
data=token_payload,
58+
json=token_payload,
5059
verify=import_from_settings('VERIFY_SSL', True))
5160
# Validate the token
52-
payload = self.verify_token(response.get('id_token'))
61+
token_response = response.json()
62+
payload = self.verify_token(token_response.get('id_token'))
5363

5464
if payload:
5565
query = urlencode({
56-
'access_token': response.get('access_token')
66+
'access_token': token_response.get('access_token')
5767
})
58-
user_info = requests.get('{url}?{query}'.format(url=self.OIDC_OP_USER_ENDPOINT,
59-
query=query))
68+
user_response = requests.get('{url}?{query}'.format(url=self.OIDC_OP_USER_ENDPOINT,
69+
query=query))
70+
user_info = user_response.json()
6071

6172
try:
62-
return self.UserModel.objects.get(email=user_info['verified_email'])
73+
return self.UserModel.objects.get(email=user_info['email'])
6374
except self.UserModel.DoesNotExist:
64-
return self.UserModel.objects.create_user(username=user_info['username'],
65-
email=user_info['verified_email'])
75+
return self.UserModel.objects.create_user(username=user_info['nickname'],
76+
email=user_info['email'])
6677
return None
6778

6879
def get_user(self, user_id):

mozilla_django_oidc/utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
try:
2+
from urlparse import urljoin
3+
except ImportError:
4+
from urllib.parse import urljoin
5+
16
from django.conf import settings
27
from django.core.exceptions import ImproperlyConfigured
38

@@ -10,8 +15,15 @@ def import_from_settings(attr, default_val=None):
1015
ImproperlyConfigured
1116
"""
1217
try:
13-
if default_val:
18+
if default_val is not None:
1419
return getattr(settings, attr, default_val)
1520
return getattr(settings, attr)
1621
except AttributeError:
1722
raise ImproperlyConfigured('Setting {0} not found'.format(attr))
23+
24+
25+
def absolutify(path):
26+
"""Return the absolute URL of url_path."""
27+
28+
site_url = import_from_settings('SITE_URL')
29+
return urljoin(site_url, path)

mozilla_django_oidc/views.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66
from django.core.urlresolvers import reverse
77
from django.contrib import auth
88
from django.http import HttpResponseRedirect
9+
from django.utils.crypto import get_random_string
910
from django.views.generic import View
1011

11-
from mozilla_django_oidc.utils import import_from_settings
12+
from mozilla_django_oidc.utils import absolutify, import_from_settings
1213

1314

1415
class OIDCAuthenticationCallbackView(View):
1516
"""OIDC client authentication callback HTTP endpoint"""
1617

17-
http_method_names = ['post']
18+
http_method_names = ['get']
1819

1920
@property
2021
def failure_url(self):
@@ -31,13 +32,13 @@ def login_success(self):
3132
auth.login(self.request, self.user)
3233
return HttpResponseRedirect(self.success_url)
3334

34-
def post(self, request):
35+
def get(self, request):
3536
"""Callback handler for OIDC authorization code flow"""
3637

37-
if 'code' in request.POST and 'state' in request.POST:
38+
if 'code' in request.GET and 'state' in request.GET:
3839
kwargs = {
39-
'code': request.POST['code'],
40-
'state': request.POST['state']
40+
'code': request.GET['code'],
41+
'state': request.GET['state']
4142
}
4243
self.user = auth.authenticate(**kwargs)
4344

@@ -63,7 +64,8 @@ def get(self, request):
6364
'response_type': 'code',
6465
'scope': 'openid',
6566
'client_id': self.OIDC_OP_CLIENT_ID,
66-
'redirect_uri': reverse('oidc_authentication_callback')
67+
'redirect_uri': absolutify(reverse('oidc_authentication_callback')),
68+
'state': get_random_string(import_from_settings('OIDC_STATE_SIZE', 32))
6769
}
6870

6971
query = urlencode(params)

requirements/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
django>=1.9.6
22
PyJWT==1.4.2
3+
requests

runtests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
'mozilla_django_oidc',
2121
],
2222
SITE_ID=1,
23+
SITE_URL='http://example.com',
2324
MIDDLEWARE_CLASSES=(),
2425
)
2526

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
'mozilla_django_oidc',
4343
],
4444
include_package_data=True,
45-
install_requires=[],
45+
install_requires=['Django>1.7', 'PyJWT', 'requests'],
4646
license='MPL 2.0',
4747
zip_safe=False,
4848
keywords='mozilla-django-oidc',

tests/test_auth.py

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from mock import patch
1+
from mock import Mock, call, patch
22

33
from django.contrib.auth import get_user_model
4-
from django.core.urlresolvers import reverse
54
from django.test import TestCase, override_settings
65

76
from mozilla_django_oidc.auth import OIDCAuthenticationBackend
@@ -26,14 +25,18 @@ def test_invalid_token(self, request_mock, token_mock):
2625
"""Test authentication with an invalid token."""
2726

2827
token_mock.return_value = None
29-
request_mock.get.return_value = {
30-
'username': 'username',
31-
'verified_email': '[email protected]'
28+
get_json_mock = Mock()
29+
get_json_mock.json.return_value = {
30+
'nickname': 'username',
31+
'email': '[email protected]'
3232
}
33-
request_mock.post.return_value = {
33+
request_mock.get.return_value = get_json_mock
34+
post_json_mock = Mock()
35+
post_json_mock.json.return_value = {
3436
'id_token': 'id_token',
3537
'accesss_token': 'access_token'
3638
}
39+
request_mock.post.return_value = post_json_mock
3740
self.assertEqual(self.backend.authenticate(code='foo', state='bar'), None)
3841

3942
def test_get_user(self):
@@ -49,56 +52,67 @@ def test_get_invalid_user(self):
4952

5053
@patch('mozilla_django_oidc.auth.requests')
5154
@patch('mozilla_django_oidc.auth.OIDCAuthenticationBackend.verify_token')
55+
@override_settings(SITE_URL='http://site-url.com')
5256
def test_successful_authentication_existing_user(self, token_mock, request_mock):
5357
"""Test successful authentication for existing user."""
5458

5559
user = User.objects.create_user(username='a_username',
5660
5761
token_mock.return_value = True
58-
request_mock.get.return_value = {
59-
'username': 'a_username',
60-
'verified_email': '[email protected]'
62+
get_json_mock = Mock()
63+
get_json_mock.json.return_value = {
64+
'nickname': 'a_username',
65+
'email': '[email protected]'
6166
}
62-
request_mock.post.return_value = {
67+
request_mock.get.return_value = get_json_mock
68+
post_json_mock = Mock()
69+
post_json_mock.json.return_value = {
6370
'id_token': 'id_token',
6471
'access_token': 'access_granted'
6572
}
73+
request_mock.post.return_value = post_json_mock
74+
6675
post_data = {
6776
'client_id': 'example_id',
6877
'client_secret': 'example_secret',
69-
'grand_type': 'authorization_code',
78+
'grant_type': 'authorization_code',
7079
'code': 'foo',
71-
'redirect_url': reverse('oidc_authentication_callback')
80+
'redirect_uri': 'http://site-url.com/oidc/authentication_callback/'
7281
}
7382
self.assertEqual(self.backend.authenticate(code='foo', state='bar'), user)
7483
token_mock.assert_called_once_with('id_token')
7584
request_mock.post.assert_called_once_with('https://server.example.com/token',
76-
data=post_data,
85+
json=post_data,
7786
verify=True)
7887
request_mock.get.assert_called_once_with(
7988
'https://server.example.com/user?access_token=access_granted'
8089
)
8190

8291
@patch('mozilla_django_oidc.auth.requests')
8392
@patch('mozilla_django_oidc.auth.OIDCAuthenticationBackend.verify_token')
93+
@override_settings(SITE_URL='http://site-url.com')
8494
def test_successful_authentication_new_user(self, token_mock, request_mock):
8595
"""Test successful authentication and user creation."""
8696

8797
token_mock.return_value = True
88-
request_mock.get.return_value = {
89-
'username': 'a_username',
90-
'verified_email': '[email protected]'
98+
get_json_mock = Mock()
99+
get_json_mock.json.return_value = {
100+
'nickname': 'a_username',
101+
'email': '[email protected]'
91102
}
92-
request_mock.post.return_value = {
103+
request_mock.get.return_value = get_json_mock
104+
post_json_mock = Mock()
105+
post_json_mock.json.return_value = {
93106
'id_token': 'id_token',
94107
'access_token': 'access_granted'
95108
}
109+
request_mock.post.return_value = post_json_mock
96110
post_data = {
97111
'client_id': 'example_id',
98112
'client_secret': 'example_secret',
99-
'grand_type': 'authorization_code',
113+
'grant_type': 'authorization_code',
100114
'code': 'foo',
101-
'redirect_url': reverse('oidc_authentication_callback')
115+
'redirect_uri': 'http://site-url.com/oidc/authentication_callback/',
102116
}
103117
self.assertEqual(User.objects.all().count(), 0)
104118
self.backend.authenticate(code='foo', state='bar')
@@ -109,7 +123,7 @@ def test_successful_authentication_new_user(self, token_mock, request_mock):
109123

110124
token_mock.assert_called_once_with('id_token')
111125
request_mock.post.assert_called_once_with('https://server.example.com/token',
112-
data=post_data,
126+
json=post_data,
113127
verify=True)
114128
request_mock.get.assert_called_once_with(
115129
'https://server.example.com/user?access_token=access_granted'
@@ -125,30 +139,53 @@ def test_authenticate_no_code_no_state(self):
125139
def test_jwt_decode_params(self, request_mock, jwt_mock):
126140
"""Test jwt verification signature."""
127141

128-
request_mock.get.return_value = {
129-
'username': 'username',
130-
'verified_email': '[email protected]'
142+
jwt_mock.decode.return_value = {
143+
'aud': 'audience'
144+
}
145+
get_json_mock = Mock()
146+
get_json_mock.json.return_value = {
147+
'nickname': 'username',
148+
'email': '[email protected]'
131149
}
132-
request_mock.post.return_value = {
150+
request_mock.get.return_value = get_json_mock
151+
post_json_mock = Mock()
152+
post_json_mock.json.return_value = {
133153
'id_token': 'token',
134154
'access_token': 'access_token'
135155
}
156+
request_mock.post.return_value = post_json_mock
136157
self.backend.authenticate(code='foo', state='bar')
137-
jwt_mock.decode.assert_called_once_with('token', 'example_secret', verify=True)
158+
calls = [
159+
call('token', verify=False),
160+
call('token', 'example_secret', verify=True, audience='audience')
161+
]
162+
jwt_mock.decode.assert_has_calls(calls)
138163

139164
@override_settings(OIDC_VERIFY_JWT=False)
140165
@patch('mozilla_django_oidc.auth.jwt')
141166
@patch('mozilla_django_oidc.auth.requests')
142167
def test_jwt_decode_params_verify_false(self, request_mock, jwt_mock):
143168
"""Test jwt verification signature with verify False"""
144169

145-
request_mock.get.return_value = {
146-
'username': 'username',
147-
'verified_email': '[email protected]'
170+
jwt_mock.decode.return_value = {
171+
'aud': 'audience'
148172
}
149-
request_mock.post.return_value = {
173+
get_json_mock = Mock()
174+
get_json_mock.json.return_value = {
175+
'nickname': 'username',
176+
'email': '[email protected]'
177+
}
178+
request_mock.get.return_value = get_json_mock
179+
post_json_mock = Mock()
180+
post_json_mock.json.return_value = {
150181
'id_token': 'token',
151182
'access_token': 'access_token'
152183
}
184+
request_mock.post.return_value = post_json_mock
185+
calls = [
186+
call('token', verify=False),
187+
call('token', 'example_secret', verify=False, audience='audience')
188+
]
189+
153190
self.backend.authenticate(code='foo', state='bar')
154-
jwt_mock.decode.assert_called_once_with('token', 'example_secret', verify=False)
191+
jwt_mock.decode.assert_has_calls(calls)

tests/test_utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.core.exceptions import ImproperlyConfigured
22
from django.test import TestCase, override_settings
33

4-
from mozilla_django_oidc.utils import import_from_settings
4+
from mozilla_django_oidc.utils import absolutify, import_from_settings
55

66

77
class SettingImportTestCase(TestCase):
@@ -18,3 +18,10 @@ def test_attr_nonexisting_no_default_value(self):
1818
def test_attr_nonexisting_default_value(self):
1919
s = import_from_settings('EXAMPLE_VARIABLE', 'example_default')
2020
self.assertEqual(s, 'example_default')
21+
22+
23+
class AbsolutifyTestCase(TestCase):
24+
@override_settings(SITE_URL='http://site-url.com')
25+
def test_absolutify(self):
26+
url = absolutify('/foo/bar')
27+
self.assertEqual(url, 'http://site-url.com/foo/bar')

0 commit comments

Comments
 (0)