Skip to content

Commit

Permalink
Merge pull request #54 from akatsoulas/validate-token
Browse files Browse the repository at this point in the history
Improve token validation.
  • Loading branch information
akatsoulas committed Nov 21, 2016
2 parents dc68193 + e8caa43 commit c6a7df9
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 46 deletions.
22 changes: 10 additions & 12 deletions mozilla_django_oidc/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import base64
import hashlib
import jwt
import json
import logging
import requests

Expand All @@ -12,6 +12,8 @@
from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse

from jose import jws

from mozilla_django_oidc.utils import absolutify, import_from_settings


Expand Down Expand Up @@ -68,21 +70,18 @@ def create_user(self, claims):
def verify_token(self, token, **kwargs):
"""Validate the token signature."""
nonce = kwargs.get('nonce')
# Get JWT audience without signature verification
audience = jwt.decode(token, verify=False)['aud']

secret = self.OIDC_RP_CLIENT_SECRET
if import_from_settings('OIDC_RP_CLIENT_SECRET_ENCODED', False):
secret = base64.urlsafe_b64decode(self.OIDC_RP_CLIENT_SECRET)
# Verify the token
verified_token = jws.verify(token, secret, algorithms=['HS256'])
token_nonce = json.loads(verified_token).get('nonce')

id_token = jwt.decode(token, secret,
verify=import_from_settings('OIDC_VERIFY_JWT', True),
audience=audience)

if import_from_settings('OIDC_USE_NONCE', True) and nonce != id_token['nonce']:
if import_from_settings('OIDC_USE_NONCE', True) and nonce != token_nonce:
msg = 'JWT Nonce verification failed.'
raise SuspiciousOperation(msg)
return id_token
return True

def authenticate(self, **kwargs):
"""Authenticates a user based on the OIDC code flow."""
Expand Down Expand Up @@ -110,15 +109,14 @@ def authenticate(self, **kwargs):

# Validate the token
token_response = response.json()
payload = self.verify_token(token_response.get('id_token'), nonce=nonce)

if payload:
if self.verify_token(token_response.get('id_token'), nonce=nonce):
access_token = token_response.get('access_token')
user_response = requests.get(self.OIDC_OP_USER_ENDPOINT,
headers={
'Authorization': 'Bearer {0}'.format(access_token)
})
user_response.raise_for_status()

user_info = user_response.json()
email = user_info.get('email')

Expand Down
2 changes: 1 addition & 1 deletion requirements/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
django>=1.9.6
PyJWT==1.4.2
requests
python-jose>=1.3.2
74 changes: 41 additions & 33 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from mock import Mock, call, patch

from django.conf import settings
Expand All @@ -17,7 +18,7 @@ class OIDCAuthenticationBackendTestCase(TestCase):
@override_settings(OIDC_OP_TOKEN_ENDPOINT='https://server.example.com/token')
@override_settings(OIDC_OP_USER_ENDPOINT='https://server.example.com/user')
@override_settings(OIDC_RP_CLIENT_ID='example_id')
@override_settings(OIDC_RP_CLIENT_SECRET='example_secret')
@override_settings(OIDC_RP_CLIENT_SECRET='client_secret')
def setUp(self):
self.backend = OIDCAuthenticationBackend()

Expand Down Expand Up @@ -52,9 +53,9 @@ def test_get_invalid_user(self):

self.assertEqual(self.backend.get_user(user_id=1), None)

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

Expand All @@ -76,7 +77,7 @@ def test_successful_authentication_existing_user(self, token_mock, request_mock)

post_data = {
'client_id': 'example_id',
'client_secret': 'example_secret',
'client_secret': 'client_secret',
'grant_type': 'authorization_code',
'code': 'foo',
'redirect_uri': 'http://site-url.com/callback/'
Expand Down Expand Up @@ -114,7 +115,7 @@ def test_successful_authentication_new_user(self, token_mock, request_mock, algo
request_mock.post.return_value = post_json_mock
post_data = {
'client_id': 'example_id',
'client_secret': 'example_secret',
'client_secret': 'client_secret',
'grant_type': 'authorization_code',
'code': 'foo',
'redirect_uri': 'http://site-url.com/callback/',
Expand All @@ -141,14 +142,14 @@ def test_authenticate_no_code_no_state(self):
self.assertEqual(self.backend.authenticate(code='', state=''), None)

@override_settings(OIDC_USE_NONCE=False)
@patch('mozilla_django_oidc.auth.jwt')
@patch('mozilla_django_oidc.auth.jws.verify')
@patch('mozilla_django_oidc.auth.requests')
def test_jwt_decode_params(self, request_mock, jwt_mock):
def test_jwt_decode_params(self, request_mock, jws_mock):
"""Test jwt verification signature."""

jwt_mock.decode.return_value = {
jws_mock.return_value = json.dumps({
'aud': 'audience'
}
})
get_json_mock = Mock()
get_json_mock.json.return_value = {
'nickname': 'username',
Expand All @@ -163,21 +164,20 @@ def test_jwt_decode_params(self, request_mock, jwt_mock):
request_mock.post.return_value = post_json_mock
self.backend.authenticate(code='foo', state='bar')
calls = [
call('token', verify=False),
call('token', 'example_secret', verify=True, audience='audience')
call('token', 'client_secret', algorithms=['HS256'])
]
jwt_mock.decode.assert_has_calls(calls)
jws_mock.assert_has_calls(calls)

@override_settings(OIDC_VERIFY_JWT=False)
@override_settings(OIDC_USE_NONCE=False)
@patch('mozilla_django_oidc.auth.jwt')
@patch('mozilla_django_oidc.auth.jws.verify')
@patch('mozilla_django_oidc.auth.requests')
def test_jwt_decode_params_verify_false(self, request_mock, jwt_mock):
def test_jwt_decode_params_verify_false(self, request_mock, jws_mock):
"""Test jwt verification signature with verify False"""

jwt_mock.decode.return_value = {
jws_mock.return_value = json.dumps({
'aud': 'audience'
}
})
get_json_mock = Mock()
get_json_mock.json.return_value = {
'nickname': 'username',
Expand All @@ -191,35 +191,37 @@ def test_jwt_decode_params_verify_false(self, request_mock, jwt_mock):
}
request_mock.post.return_value = post_json_mock
calls = [
call('token', verify=False),
call('token', 'example_secret', verify=False, audience='audience')
call('token', 'client_secret', algorithms=['HS256'])
]

self.backend.authenticate(code='foo', state='bar')
jwt_mock.decode.assert_has_calls(calls)
jws_mock.assert_has_calls(calls)

@override_settings(OIDC_USE_NONCE=True)
@patch('mozilla_django_oidc.auth.jwt')
@override_settings(OIDC_RP_CLIENT_SECRET_ENCODED=False)
@patch('mozilla_django_oidc.auth.jws')
def test_jwt_failed_nonce(self, jwt_mock):
"""Test Nonce verification."""

jwt_mock.decode.return_value = {
jwt_mock.verify.return_value = json.dumps({
'nonce': 'foobar',
'aud': 'aud'
}
})
id_token = 'my_token'
with self.assertRaises(SuspiciousOperation) as context:
self.backend.verify_token(id_token, **{'nonce': 'foo'})
self.assertEqual('JWT Nonce verification failed.', str(context.exception))

@override_settings(OIDC_CREATE_USER=False)
@override_settings(OIDC_USE_NONCE=False)
@patch('mozilla_django_oidc.auth.jwt')
@patch('mozilla_django_oidc.auth.jws.verify')
@patch('mozilla_django_oidc.auth.requests')
def test_create_user_disabled(self, request_mock, jwt_mock):
def test_create_user_disabled(self, request_mock, jws_mock):
"""Test with user creation disabled and no user found."""

jwt_mock.return_value = True
jws_mock.return_value = json.dumps({
'nonce': 'nonce'
})
get_json_mock = Mock()
get_json_mock.json.return_value = {
'nickname': 'a_username',
Expand All @@ -234,14 +236,16 @@ def test_create_user_disabled(self, request_mock, jwt_mock):
request_mock.post.return_value = post_json_mock
self.assertEqual(self.backend.authenticate(code='foo', state='bar'), None)

@patch('mozilla_django_oidc.auth.jwt')
@patch('mozilla_django_oidc.auth.jws.verify')
@patch('mozilla_django_oidc.auth.requests')
@override_settings(OIDC_USE_NONCE=False)
def test_create_user_enabled(self, request_mock, jwt_mock):
def test_create_user_enabled(self, request_mock, jws_mock):
"""Test with user creation enabled and no user found."""

self.assertEqual(User.objects.filter(email='email@example.com').exists(), False)
jwt_mock.return_value = True
jws_mock.return_value = json.dumps({
'nonce': 'nonce'
})
get_json_mock = Mock()
get_json_mock.json.return_value = {
'nickname': 'a_username',
Expand All @@ -259,14 +263,16 @@ def test_create_user_enabled(self, request_mock, jwt_mock):

@patch.object(settings, 'OIDC_USERNAME_ALGO')
@override_settings(OIDC_USE_NONCE=False)
@patch('mozilla_django_oidc.auth.jwt')
@patch('mozilla_django_oidc.auth.jws.verify')
@patch('mozilla_django_oidc.auth.requests')
def test_custom_username_algo(self, request_mock, jwt_mock, algo_mock):
def test_custom_username_algo(self, request_mock, jws_mock, algo_mock):
"""Test user creation with custom username algorithm."""

self.assertEqual(User.objects.filter(email='email@example.com').exists(), False)
algo_mock.return_value = 'username_algo'
jwt_mock.return_value = True
jws_mock.return_value = json.dumps({
'nonce': 'nonce'
})
get_json_mock = Mock()
get_json_mock.json.return_value = {
'nickname': 'a_username',
Expand All @@ -283,14 +289,16 @@ def test_custom_username_algo(self, request_mock, jwt_mock, algo_mock):
User.objects.get(username='username_algo'))

@override_settings(OIDC_USE_NONCE=False)
@patch('mozilla_django_oidc.auth.jwt')
@patch('mozilla_django_oidc.auth.jws.verify')
@patch('mozilla_django_oidc.auth.requests')
def test_duplicate_emails(self, request_mock, jwt_mock):
def test_duplicate_emails(self, request_mock, jws_mock):
"""Test auth with two users having the same email."""

User.objects.create(username='user1', email='email@example.com')
User.objects.create(username='user2', email='email@example.com')
jwt_mock.return_value = True
jws_mock.return_value = json.dumps({
'nonce': 'nonce'
})
get_json_mock = Mock()
get_json_mock.json.return_value = {
'nickname': 'a_username',
Expand Down

0 comments on commit c6a7df9

Please sign in to comment.