Skip to content

Commit

Permalink
Merge pull request #23 from akatsoulas/oidc-authenticate
Browse files Browse the repository at this point in the history
Basic authentication functionality.
  • Loading branch information
akatsoulas committed Oct 27, 2016
2 parents c9cc99f + e18b5f9 commit d6ab0f6
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 19 deletions.
74 changes: 74 additions & 0 deletions mozilla_django_oidc/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import jwt
import requests
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode

from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse

from mozilla_django_oidc.utils import import_from_settings


class OIDCAuthenticationBackend(object):
"""Override Django's authentication."""

def __init__(self, *args, **kwargs):
"""Initialize settings."""
self.OIDC_OP_TOKEN_ENDPOINT = import_from_settings('OIDC_OP_TOKEN_ENDPOINT')
self.OIDC_OP_USER_ENDPOINT = import_from_settings('OIDC_OP_USER_ENDPOINT')
self.OIDC_OP_CLIENT_ID = import_from_settings('OIDC_OP_CLIENT_ID')
self.OIDC_OP_CLIENT_SECRET = import_from_settings('OIDC_OP_CLIENT_SECRET')

self.UserModel = get_user_model()

def verify_token(self, token, **kwargs):
"""Validate the token signature."""

return jwt.decode(token,
self.OIDC_OP_CLIENT_SECRET,
verify=import_from_settings('OIDC_VERIFY_JWT', True))

def authenticate(self, code=None, state=None):
"""Authenticates a user based on the OIDC code flow."""

if not code or not state:
return None

token_payload = {
'client_id': self.OIDC_OP_CLIENT_ID,
'client_secret': self.OIDC_OP_CLIENT_SECRET,
'grand_type': 'authorization_code',
'code': code,
'redirect_url': reverse('oidc_authentication_callback')
}

# Get the token
response = requests.post(self.OIDC_OP_TOKEN_ENDPOINT,
data=token_payload,
verify=import_from_settings('VERIFY_SSL', True))
# Validate the token
payload = self.verify_token(response.get('id_token'))

if payload:
query = urlencode({
'access_token': response.get('access_token')
})
user_info = requests.get('{url}?{query}'.format(url=self.OIDC_OP_USER_ENDPOINT,
query=query))

try:
return self.UserModel.objects.get(email=user_info['verified_email'])
except self.UserModel.DoesNotExist:
return self.UserModel.objects.create_user(username=user_info['username'],
email=user_info['verified_email'])
return None

def get_user(self, user_id):
"""Return a user based on the id."""

try:
return self.UserModel.objects.get(pk=user_id)
except self.UserModel.DoesNotExist:
return None
8 changes: 4 additions & 4 deletions mozilla_django_oidc/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from mozilla_django_oidc import views

urlpatterns = [
url(r'^oidc/authorization_callback/$', views.OIDCAuthorizationCallbackView.as_view(),
name='oidc_authorization_callback'),
url(r'^oidc/authorization_init/$', views.OIDCAuthorizationRequestView.as_view(),
name='oidc_authorization_init'),
url(r'^oidc/authentication_callback/$', views.OIDCAuthenticationCallbackView.as_view(),
name='oidc_authentication_callback'),
url(r'^oidc/authentication_init/$', views.OIDCAuthenticationRequestView.as_view(),
name='oidc_authentication_init'),
]
8 changes: 4 additions & 4 deletions mozilla_django_oidc/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mozilla_django_oidc.utils import import_from_settings


class OIDCAuthorizationCallbackView(View):
class OIDCAuthenticationCallbackView(View):
"""OIDC client authentication callback HTTP endpoint"""

http_method_names = ['post']
Expand Down Expand Up @@ -46,13 +46,13 @@ def post(self, request):
return self.login_failure()


class OIDCAuthorizationRequestView(View):
class OIDCAuthenticationRequestView(View):
"""OIDC client authentication HTTP endpoint"""

http_method_names = ['get']

def __init__(self, *args, **kwargs):
super(OIDCAuthorizationRequestView, self).__init__(*args, **kwargs)
super(OIDCAuthenticationRequestView, self).__init__(*args, **kwargs)

self.OIDC_OP_AUTH_ENDPOINT = import_from_settings('OIDC_OP_AUTHORIZATION_ENDPOINT')
self.OIDC_OP_CLIENT_ID = import_from_settings('OIDC_OP_CLIENT_ID')
Expand All @@ -63,7 +63,7 @@ def get(self, request):
'response_type': 'code',
'scope': 'openid',
'client_id': self.OIDC_OP_CLIENT_ID,
'redirect_uri': reverse('oidc_authorization_callback')
'redirect_uri': reverse('oidc_authentication_callback')
}

query = urlencode(params)
Expand Down
154 changes: 154 additions & 0 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from mock import patch

from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase, override_settings

from mozilla_django_oidc.auth import OIDCAuthenticationBackend


User = get_user_model()


class OIDCAuthenticationBackendTestCase(TestCase):
"""Authentication tests."""

@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_OP_CLIENT_ID='example_id')
@override_settings(OIDC_OP_CLIENT_SECRET='example_secret')
def setUp(self):
self.backend = OIDCAuthenticationBackend()

@patch('mozilla_django_oidc.auth.OIDCAuthenticationBackend.verify_token')
@patch('mozilla_django_oidc.auth.requests')
def test_invalid_token(self, request_mock, token_mock):
"""Test authentication with an invalid token."""

token_mock.return_value = None
request_mock.get.return_value = {
'username': 'username',
'verified_email': 'email@example.com'
}
request_mock.post.return_value = {
'id_token': 'id_token',
'accesss_token': 'access_token'
}
self.assertEqual(self.backend.authenticate(code='foo', state='bar'), None)

def test_get_user(self):
"""Test get_user method with valid user."""

user = User.objects.create_user('example_username')
self.assertEqual(self.backend.get_user(user.pk), user)

def test_get_invalid_user(self):
"""Test get_user method with non existing user."""

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

@patch('mozilla_django_oidc.auth.requests')
@patch('mozilla_django_oidc.auth.OIDCAuthenticationBackend.verify_token')
def test_successful_authentication_existing_user(self, token_mock, request_mock):
"""Test successful authentication for existing user."""

user = User.objects.create_user(username='a_username',
email='email@example.com')
token_mock.return_value = True
request_mock.get.return_value = {
'username': 'a_username',
'verified_email': 'email@example.com'
}
request_mock.post.return_value = {
'id_token': 'id_token',
'access_token': 'access_granted'
}
post_data = {
'client_id': 'example_id',
'client_secret': 'example_secret',
'grand_type': 'authorization_code',
'code': 'foo',
'redirect_url': reverse('oidc_authentication_callback')
}
self.assertEqual(self.backend.authenticate(code='foo', state='bar'), user)
token_mock.assert_called_once_with('id_token')
request_mock.post.assert_called_once_with('https://server.example.com/token',
data=post_data,
verify=True)
request_mock.get.assert_called_once_with(
'https://server.example.com/user?access_token=access_granted'
)

@patch('mozilla_django_oidc.auth.requests')
@patch('mozilla_django_oidc.auth.OIDCAuthenticationBackend.verify_token')
def test_successful_authentication_new_user(self, token_mock, request_mock):
"""Test successful authentication and user creation."""

token_mock.return_value = True
request_mock.get.return_value = {
'username': 'a_username',
'verified_email': 'email@example.com'
}
request_mock.post.return_value = {
'id_token': 'id_token',
'access_token': 'access_granted'
}
post_data = {
'client_id': 'example_id',
'client_secret': 'example_secret',
'grand_type': 'authorization_code',
'code': 'foo',
'redirect_url': reverse('oidc_authentication_callback')
}
self.assertEqual(User.objects.all().count(), 0)
self.backend.authenticate(code='foo', state='bar')
self.assertEqual(User.objects.all().count(), 1)
user = User.objects.all()[0]
self.assertEquals(user.email, 'email@example.com')
self.assertEquals(user.username, 'a_username')

token_mock.assert_called_once_with('id_token')
request_mock.post.assert_called_once_with('https://server.example.com/token',
data=post_data,
verify=True)
request_mock.get.assert_called_once_with(
'https://server.example.com/user?access_token=access_granted'
)

def test_authenticate_no_code_no_state(self):
"""Test authenticate with wrong parameters."""

self.assertEqual(self.backend.authenticate(code='', state=''), None)

@patch('mozilla_django_oidc.auth.jwt')
@patch('mozilla_django_oidc.auth.requests')
def test_jwt_decode_params(self, request_mock, jwt_mock):
"""Test jwt verification signature."""

request_mock.get.return_value = {
'username': 'username',
'verified_email': 'email@example.com'
}
request_mock.post.return_value = {
'id_token': 'token',
'access_token': 'access_token'
}
self.backend.authenticate(code='foo', state='bar')
jwt_mock.decode.assert_called_once_with('token', 'example_secret', verify=True)

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

request_mock.get.return_value = {
'username': 'username',
'verified_email': 'email@example.com'
}
request_mock.post.return_value = {
'id_token': 'token',
'access_token': 'access_token'
}
self.backend.authenticate(code='foo', state='bar')
jwt_mock.decode.assert_called_once_with('token', 'example_secret', verify=False)
22 changes: 11 additions & 11 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def test_post_auth_success(self):
'code': 'example_code',
'state': 'example_state'
}
url = reverse('oidc_authorization_callback')
url = reverse('oidc_authentication_callback')
request = self.factory.post(url, post_data)
callback_view = views.OIDCAuthorizationCallbackView.as_view()
callback_view = views.OIDCAuthenticationCallbackView.as_view()

with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
with patch('mozilla_django_oidc.views.auth.login') as mock_login:
Expand All @@ -51,9 +51,9 @@ def test_post_auth_failure_nonexisting_user(self):
'state': 'example_state'
}

url = reverse('oidc_authorization_callback')
url = reverse('oidc_authentication_callback')
request = self.factory.post(url, post_data)
callback_view = views.OIDCAuthorizationCallbackView.as_view()
callback_view = views.OIDCAuthenticationCallbackView.as_view()

with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
mock_auth.return_value = None
Expand All @@ -75,9 +75,9 @@ def test_post_auth_failure_inactive_user(self):
'state': 'example_state'
}

url = reverse('oidc_authorization_callback')
url = reverse('oidc_authentication_callback')
request = self.factory.post(url, post_data)
callback_view = views.OIDCAuthorizationCallbackView.as_view()
callback_view = views.OIDCAuthenticationCallbackView.as_view()

with patch('mozilla_django_oidc.views.auth.authenticate') as mock_auth:
mock_auth.return_value = user
Expand All @@ -94,9 +94,9 @@ def test_post_auth_dirty_data(self):
'foo': 'bar',
}

url = reverse('oidc_authorization_callback')
url = reverse('oidc_authentication_callback')
request = self.factory.post(url, post_data)
callback_view = views.OIDCAuthorizationCallbackView.as_view()
callback_view = views.OIDCAuthenticationCallbackView.as_view()
response = callback_view(request)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/failure')
Expand All @@ -109,9 +109,9 @@ def setUp(self):
@override_settings(OIDC_OP_AUTHORIZATION_ENDPOINT='https://server.example.com/auth')
@override_settings(OIDC_OP_CLIENT_ID='example_id')
def test_get(self):
url = reverse('oidc_authorization_init')
url = reverse('oidc_authentication_init')
request = self.factory.get(url)
login_view = views.OIDCAuthorizationRequestView.as_view()
login_view = views.OIDCAuthenticationRequestView.as_view()
response = login_view(request)
self.assertEqual(response.status_code, 302)

Expand All @@ -120,7 +120,7 @@ def test_get(self):
'response_type': ['code'],
'scope': ['openid'],
'client_id': ['example_id'],
'redirect_uri': ['/oidc/authorization_callback/']
'redirect_uri': ['/oidc/authentication_callback/']
}
self.assertDictEqual(parse_qs(o.query), expected_query)
self.assertEqual(o.hostname, 'server.example.com')
Expand Down

0 comments on commit d6ab0f6

Please sign in to comment.