Skip to content

Auth backend proposal to address #50 #54

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
An application providing access to its own resources through an API protected with the OAuth2 protocol.

Application
TODO

Client
A client is an application authorized to access OAuth2-protected resources on behalf and with the authorization
Expand Down
1 change: 1 addition & 0 deletions docs/tutorial/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Tutorials

tutorial_01
tutorial_02
tutorial_03
72 changes: 72 additions & 0 deletions docs/tutorial/tutorial_03.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
Part 3 - OAuth2 token authentication
====================================

Scenario
--------
You want to use an :term:`Access Token` to authenticate users against Django's authentication
system.

Setup a provider
----------------
You need a fully-functional OAuth2 provider which is able to release access tokens: just follow
the steps in :doc:`the part 1 of the tutorial <tutorial_01>`. To enable OAuth2 token authentication
you need a middleware that checks for tokens inside requests and a custom authentication backend
which takes care of token verification. In your settings.py:

.. code-block:: python

AUTHENTICATION_BACKENDS = (
'oauth2_provider.backends.OAuth2Backend',
'...',
)

MIDDLEWARE_CLASSES = (
'...',
'oauth2_provider.middleware.OAuth2TokenMiddleware',
'...',
)

You can use `django.contrib.auth.backends.ModelBackend` along with the OAuth2 backend, but pay
attention to the order in which Django processes authentication backends.

If you put the OAuth2 backend *after* the AuthenticationMiddleware and `request.user` is valid,
the backend will do nothing; if `request.user` is the Anonymous user it will try to authenticate
the user using the OAuth2 access token.

If you put the OAuth2 backend *before* AuthenticationMiddleware, or AuthenticationMiddleware is
not used at all, it will try to authenticate user with the OAuth2 access token and set
`request.user` and `request._cached_user` fields so that AuthenticationMiddleware (when active)
will not try to get user from the session.

Protect your view
-----------------
The authentication backend will run smoothly with, for example, `login_required` decorators, so
that you can have a view like this in your `views.py` module:

.. code-block:: python

from django.contrib.auth.decorators import login_required
from django.http.response import HttpResponse

@login_required()
def secret_page(request, *args, **kwargs):
return HttpResponse('Secret contents!', status=200)

To check everything works properly, mount the view above to some url:

.. code-block:: python

urlpatterns = patterns(
'',
url(r'^secret$', 'my.views.secret_page', name='secret'),
'...',
)

You should have an :term:`Application` registered at this point, if you don't follow the steps in
the previous tutorials to create one. Obtain an :term:`Access Token`, either following the OAuth2
flow of your application or manually creating in the Django admin.
Now supposing your access token value is `123456` you can try to access your authenticated view:

::

curl -H "Authorization: Bearer 123456" -X GET http://localhost:8000/secret
123 changes: 18 additions & 105 deletions oauth2_provider/backends.py
Original file line number Diff line number Diff line change
@@ -1,113 +1,26 @@
from oauthlib import oauth2
from oauthlib.common import urlencode
from .compat import get_user_model
from .oauth2_backends import get_oauthlib_core

from .exceptions import OAuthToolkitError, FatalClientError
from .oauth2_validators import OAuth2Validator
UserModel = get_user_model()
OAuthLibCore = get_oauthlib_core()


class OAuthLibCore(object):
class OAuth2Backend(object):
"""
TODO: add docs
Authenticate against an OAuth2 access token
"""
def __init__(self, server=None):
"""
:params server: An instance of oauthlib.oauth2.Server class
"""
self.server = server or oauth2.Server(OAuth2Validator())

def _extract_params(self, request):
"""
Extract parameters from the Django request object. Such parameters will then be passed to OAuthLib to build its
own Request object
"""
uri = request.build_absolute_uri()
http_method = request.method
headers = request.META.copy()
if 'wsgi.input' in headers:
del headers['wsgi.input']
if 'wsgi.errors' in headers:
del headers['wsgi.errors']
if 'HTTP_AUTHORIZATION' in headers:
headers['Authorization'] = headers['HTTP_AUTHORIZATION']
body = urlencode(request.POST.items())
return uri, http_method, body, headers
def authenticate(self, **credentials):
request = credentials.get('request')
if request is not None:
oauthlib_core = get_oauthlib_core()
valid, r = oauthlib_core.verify_request(request, scopes=[])
if valid:
return r.user
return None

def validate_authorization_request(self, request):
"""
A wrapper method that calls validate_authorization_request on `server_class` instance.

:param request: The current django.http.HttpRequest object
"""
try:
uri, http_method, body, headers = self._extract_params(request)

scopes, credentials = self.server.validate_authorization_request(
uri, http_method=http_method, body=body, headers=headers)

return scopes, credentials
except oauth2.FatalClientError as error:
raise FatalClientError(error=error)
except oauth2.OAuth2Error as error:
raise OAuthToolkitError(error=error)

def create_authorization_response(self, request, scopes, credentials, allow):
"""
A wrapper method that calls create_authorization_response on `server_class`
instance.

:param request: The current django.http.HttpRequest object
:param scopes: A list of provided scopes
:param credentials: Authorization credentials dictionary containing
`client_id`, `state`, `redirect_uri`, `response_type`
:param allow: True if the user authorize the client, otherwise False
"""
def get_user(self, user_id):
try:
if not allow:
raise oauth2.AccessDeniedError()

# add current user to credentials. this will be used by OAuth2Validator
credentials['user'] = request.user

uri, headers, body, status = self.server.create_authorization_response(
uri=credentials['redirect_uri'], scopes=scopes, credentials=credentials)

return uri, headers, body, status

except oauth2.FatalClientError as error:
raise FatalClientError(error=error, redirect_uri=credentials['redirect_uri'])
except oauth2.OAuth2Error as error:
raise OAuthToolkitError(error=error, redirect_uri=credentials['redirect_uri'])

def create_token_response(self, request):
"""
A wrapper method that calls create_token_response on `server_class` instance.

:param request: The current django.http.HttpRequest object
"""
uri, http_method, body, headers = self._extract_params(request)

url, headers, body, status = self.server.create_token_response(uri, http_method, body, headers)
return url, headers, body, status

def verify_request(self, request, scopes):
"""
A wrapper method that calls verify_request on `server_class` instance.

:param request: The current django.http.HttpRequest object
:param scopes: A list of scopes required to verify so that request is verified
"""
uri, http_method, body, headers = self._extract_params(request)

valid, r = self.server.verify_request(uri, http_method, body, headers, scopes=scopes)
return valid, r


def get_oauthlib_core():
"""
Utility function that take a request and returns an instance of `oauth2_provider.backends.OAuthLibCore`
"""
from oauth2_provider.oauth2_validators import OAuth2Validator
from oauthlib.oauth2 import Server

server = Server(OAuth2Validator())
return OAuthLibCore(server)
return UserModel.objects.get(pk=user_id)
except UserModel.DoesNotExist:
return None
2 changes: 1 addition & 1 deletion oauth2_provider/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.core.exceptions import ImproperlyConfigured

from .oauth2_validators import OAuth2Validator
from .backends import OAuthLibCore
from .oauth2_backends import OAuthLibCore
from .settings import oauth2_settings


Expand Down
2 changes: 1 addition & 1 deletion oauth2_provider/ext/rest_framework/authentication.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework.authentication import BaseAuthentication

from ...backends import get_oauthlib_core
from ...oauth2_backends import get_oauthlib_core


class OAuth2Authentication(BaseAuthentication):
Expand Down
26 changes: 26 additions & 0 deletions oauth2_provider/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.contrib.auth import authenticate


class OAuth2TokenMiddleware(object):
"""
Middleware for OAuth2 user authentication

This middleware is able to work along with AuthenticationMiddleware and its behaviour depends
on the order it's processed with.

If it comes *after* AuthenticationMiddleware and request.user is valid, leave it as is and does
not proceed with token validation. If request.user is the Anonymous user proceeds and try to
authenticate the user using the OAuth2 access token.

If it comes *before* AuthenticationMiddleware, or AuthenticationMiddleware is not used at all,
tries to authenticate user with the OAuth2 access token and set request.user field. Setting
also request._cached_user field makes AuthenticationMiddleware use that instead of the one from
the session.
"""
def process_request(self, request):
# do something only if request contains a Bearer token
if request.META.get('HTTP_AUTHORIZATION', '').startswith('Bearer'):
if not hasattr(request, 'user') or request.user.is_anonymous():
user = authenticate(request=request)
if user:
request.user = request._cached_user = user
113 changes: 113 additions & 0 deletions oauth2_provider/oauth2_backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from oauthlib import oauth2
from oauthlib.common import urlencode

from .exceptions import OAuthToolkitError, FatalClientError
from .oauth2_validators import OAuth2Validator


class OAuthLibCore(object):
"""
TODO: add docs
"""
def __init__(self, server=None):
"""
:params server: An instance of oauthlib.oauth2.Server class
"""
self.server = server or oauth2.Server(OAuth2Validator())

def _extract_params(self, request):
"""
Extract parameters from the Django request object. Such parameters will then be passed to OAuthLib to build its
own Request object
"""
uri = request.build_absolute_uri()
http_method = request.method
headers = request.META.copy()
if 'wsgi.input' in headers:
del headers['wsgi.input']
if 'wsgi.errors' in headers:
del headers['wsgi.errors']
if 'HTTP_AUTHORIZATION' in headers:
headers['Authorization'] = headers['HTTP_AUTHORIZATION']
body = urlencode(request.POST.items())
return uri, http_method, body, headers

def validate_authorization_request(self, request):
"""
A wrapper method that calls validate_authorization_request on `server_class` instance.

:param request: The current django.http.HttpRequest object
"""
try:
uri, http_method, body, headers = self._extract_params(request)

scopes, credentials = self.server.validate_authorization_request(
uri, http_method=http_method, body=body, headers=headers)

return scopes, credentials
except oauth2.FatalClientError as error:
raise FatalClientError(error=error)
except oauth2.OAuth2Error as error:
raise OAuthToolkitError(error=error)

def create_authorization_response(self, request, scopes, credentials, allow):
"""
A wrapper method that calls create_authorization_response on `server_class`
instance.

:param request: The current django.http.HttpRequest object
:param scopes: A list of provided scopes
:param credentials: Authorization credentials dictionary containing
`client_id`, `state`, `redirect_uri`, `response_type`
:param allow: True if the user authorize the client, otherwise False
"""
try:
if not allow:
raise oauth2.AccessDeniedError()

# add current user to credentials. this will be used by OAuth2Validator
credentials['user'] = request.user

uri, headers, body, status = self.server.create_authorization_response(
uri=credentials['redirect_uri'], scopes=scopes, credentials=credentials)

return uri, headers, body, status

except oauth2.FatalClientError as error:
raise FatalClientError(error=error, redirect_uri=credentials['redirect_uri'])
except oauth2.OAuth2Error as error:
raise OAuthToolkitError(error=error, redirect_uri=credentials['redirect_uri'])

def create_token_response(self, request):
"""
A wrapper method that calls create_token_response on `server_class` instance.

:param request: The current django.http.HttpRequest object
"""
uri, http_method, body, headers = self._extract_params(request)

url, headers, body, status = self.server.create_token_response(uri, http_method, body, headers)
return url, headers, body, status

def verify_request(self, request, scopes):
"""
A wrapper method that calls verify_request on `server_class` instance.

:param request: The current django.http.HttpRequest object
:param scopes: A list of scopes required to verify so that request is verified
"""
uri, http_method, body, headers = self._extract_params(request)

valid, r = self.server.verify_request(uri, http_method, body, headers, scopes=scopes)
return valid, r


def get_oauthlib_core():
"""
Utility function that take a request and returns an instance of `oauth2_provider.backends.OAuthLibCore`
"""
from oauth2_provider.oauth2_validators import OAuth2Validator
from oauthlib.oauth2 import Server

server = Server(OAuth2Validator())
return OAuthLibCore(server)
2 changes: 2 additions & 0 deletions oauth2_provider/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@
from .test_rest_framework import *
from .test_application_views import *
from .test_decorators import *

from .test_auth_backends import *
Loading