-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #231 from alprs/impl-drf
implement authentication class for DRF
- Loading branch information
Showing
8 changed files
with
227 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
======================================= | ||
DRF (Django REST Framework) integration | ||
======================================= | ||
|
||
If you want DRF to authenticate users based on an OAuth access token provided in | ||
the ``Authorization`` header, you can use the DRF-specific authentication class | ||
which ships with the package. | ||
|
||
Add this to your settings: | ||
|
||
.. code-block:: python | ||
REST_FRAMEWORK = { | ||
'DEFAULT_AUTHENTICATION_CLASSES': [ | ||
'mozilla_django_oidc.contrib.drf.OIDCAuthentication', | ||
# other authentication classes, if needed | ||
], | ||
} | ||
Note that this only takes care of authenticating against an access token, and | ||
provides no options to create or renew tokens. | ||
|
||
If you've created a custom Django ``OIDCAuthenticationBackend`` and added that | ||
to your ``AUTHENTICATION_BACKENDS``, the DRF class should be smart enough to | ||
figure that out. Alternatively, you can manually set the OIDC backend to use: | ||
|
||
.. code-block:: python | ||
OIDC_DRF_AUTH_BACKEND = 'mozilla_django_oidc.OIDCAuthenticationBackend' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ Contents: | |
installation | ||
settings | ||
xhr | ||
drf | ||
contributing | ||
authors | ||
history | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
""" | ||
Classes/functions for integrating with Django REST Framework. | ||
http://www.django-rest-framework.org/api-guide/authentication/#custom-authentication | ||
""" | ||
|
||
import logging | ||
|
||
from django.contrib.auth import get_backends | ||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation | ||
from django.utils.module_loading import import_string | ||
from rest_framework import authentication, exceptions | ||
from requests.exceptions import HTTPError | ||
|
||
from mozilla_django_oidc.auth import OIDCAuthenticationBackend | ||
from mozilla_django_oidc.utils import import_from_settings, parse_www_authenticate_header | ||
|
||
LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
def get_oidc_backend(): | ||
""" | ||
Get the Django auth backend that uses OIDC. | ||
""" | ||
|
||
# allow the user to force which back backend to use. this is mostly | ||
# convenient if you want to use OIDC with DRF but don't want to configure | ||
# OIDC for the "normal" Django auth. | ||
backend_setting = import_from_settings('OIDC_DRF_AUTH_BACKEND', None) | ||
if backend_setting: | ||
backend = import_string(backend_setting)() | ||
if not isinstance(backend, OIDCAuthenticationBackend): | ||
msg = 'Class configured in OIDC_DRF_AUTH_BACKEND ' \ | ||
'does not extend OIDCAuthenticationBackend!' | ||
raise ImproperlyConfigured(msg) | ||
return backend | ||
|
||
# if the backend setting is not set, look through the list of configured | ||
# backends for one that is an OIDCAuthenticationBackend. | ||
backends = [b for b in get_backends() if isinstance(b, OIDCAuthenticationBackend)] | ||
|
||
if not backends: | ||
msg = 'No backends extending OIDCAuthenticationBackend found - ' \ | ||
'add one to AUTHENTICATION_BACKENDS or set OIDC_DRF_AUTH_BACKEND!' | ||
raise ImproperlyConfigured(msg) | ||
if len(backends) > 1: | ||
raise ImproperlyConfigured('More than one OIDCAuthenticationBackend found!') | ||
return backends[0] | ||
|
||
|
||
class OIDCAuthentication(authentication.BaseAuthentication): | ||
""" | ||
Provide OpenID authentication for DRF. | ||
""" | ||
|
||
# used by the authenticate_header method. | ||
www_authenticate_realm = 'api' | ||
|
||
def __init__(self, backend=None): | ||
self.backend = backend or get_oidc_backend() | ||
|
||
def authenticate(self, request): | ||
""" | ||
Authenticate the request and return a tuple of (user, token) or None | ||
if there was no authentication attempt. | ||
""" | ||
access_token = self.get_access_token(request) | ||
|
||
if not access_token: | ||
return None | ||
|
||
try: | ||
user = self.backend.get_or_create_user(access_token, None, None) | ||
except HTTPError as exc: | ||
resp = exc.response | ||
|
||
# if the oidc provider returns 401, it means the token is invalid. | ||
# in that case, we want to return the upstream error message (which | ||
# we can get from the www-authentication header) in the response. | ||
if resp.status_code == 401 and 'www-authenticate' in resp.headers: | ||
data = parse_www_authenticate_header(resp.headers['www-authenticate']) | ||
raise exceptions.AuthenticationFailed(data['error_description']) | ||
|
||
# for all other http errors, just re-raise the exception. | ||
raise | ||
except SuspiciousOperation as exc: | ||
LOGGER.info('Login failed: %s', exc) | ||
raise exceptions.AuthenticationFailed('Login failed') | ||
|
||
if not user: | ||
msg = 'Login failed: No user found for the given access token.' | ||
raise exceptions.AuthenticationFailed(msg) | ||
|
||
return user, access_token | ||
|
||
def get_access_token(self, request): | ||
""" | ||
Get the access token based on a request. | ||
Returns None if no authentication details were provided. Raises | ||
AuthenticationFailed if the token is incorrect. | ||
""" | ||
header = authentication.get_authorization_header(request) | ||
if not header: | ||
return None | ||
header = header.decode(authentication.HTTP_HEADER_ENCODING) | ||
|
||
auth = header.split() | ||
|
||
if auth[0].lower() != 'bearer': | ||
return None | ||
|
||
if len(auth) == 1: | ||
msg = 'Invalid "bearer" header: No credentials provided.' | ||
raise exceptions.AuthenticationFailed(msg) | ||
elif len(auth) > 2: | ||
msg = 'Invalid "bearer" header: Credentials string should not contain spaces.' | ||
raise exceptions.AuthenticationFailed(msg) | ||
|
||
return auth[1] | ||
|
||
def authenticate_header(self, request): | ||
""" | ||
If this method returns None, a generic HTTP 403 forbidden response is | ||
returned by DRF when authentication fails. | ||
By making the method return a string, a 401 is returned instead. The | ||
return value will be used as the WWW-Authenticate header. | ||
""" | ||
return 'Bearer realm="%s"' % self.www_authenticate_realm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import mock | ||
|
||
from django.core.exceptions import SuspiciousOperation | ||
from django.test import RequestFactory, TestCase, override_settings | ||
from rest_framework import exceptions | ||
|
||
from mozilla_django_oidc.contrib.drf import OIDCAuthentication | ||
|
||
|
||
class TestDRF(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='client_secret') | ||
def setUp(self): | ||
self.auth = OIDCAuthentication(backend=mock.Mock()) | ||
self.request = RequestFactory().get('/', HTTP_AUTHORIZATION='Bearer faketoken') | ||
|
||
def test_authenticate_returns_none_if_no_access_token(self): | ||
with mock.patch.object(self.auth, 'get_access_token', return_value=None): | ||
ret = self.auth.authenticate(self.request) | ||
self.assertEqual(ret, None) | ||
|
||
def test_authenticate_raises_authenticationfailed_if_backend_returns_no_user(self): | ||
self.auth.backend.get_or_create_user.return_value = None | ||
with self.assertRaises(exceptions.AuthenticationFailed): | ||
self.auth.authenticate(self.request) | ||
|
||
def test_authenticate_raises_authenticationfailed_on_suspiciousoperation(self): | ||
self.auth.backend.get_or_create_user.side_effect = SuspiciousOperation | ||
with self.assertRaises(exceptions.AuthenticationFailed): | ||
self.auth.authenticate(self.request) | ||
|
||
def test_returns_user_and_token_if_backend_returns_user(self): | ||
user = mock.Mock() | ||
self.auth.backend.get_or_create_user.return_value = user | ||
ret = self.auth.authenticate(self.request) | ||
self.assertEqual(ret[0], user) | ||
self.assertEqual(ret[1], 'faketoken') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters