Building an OAuth 2.0 provider in Django


I was working on a Django project recently that required OAuth 2.0 authentication so that others can authenticate their app and consume the API provided by the app. The OAuth reference guide - http://tools.ietf.org/html/rfc6749 was a little dense fore me. My understandings of OAuth mostly comes from two articles - Zapier API Guide and Mashape OAuth Bible. I hope you will find them useful too.

Django has excellent packages for providing OAuth capabilities. A quick search found me CaffieneHit’s django-oauth2-provider which has excellent OAuth provisioning capabilities.

I created a small django app to demonstrate how I built the OAuth 2.0 provider in Django and django-oauth2-provider. You can clone the source here.

Setting up the project

Use virtualenv to create a new virtual environment for the project. I use virtualenvwrapper for managing my virtual environments. Creating and activating a new virtual environment using virtualenvwrapper is as easy as

mkvirtualenv django-oauth-demo

Once its done, install Django using pip. Please note that I am installing Django 1.6 here as the current version of django-oauth2-provider is not compatiblie with Django 1.7. See https://github.com/caffeinehit/django-oauth2-provider/issues/96 for more information. This is possible due to Django 1.7 dropping ‘mimetype’ parameter in HttpResponse and using ‘content_type’ instead. There are pull requests for this available, but its still not merged into the master branch yet.

pip install Django==1.6

Then we need to create a new Django project. I’ve also created a new app named api which will contain the code for OAuth 2.0.

django-admin startproject django_oauth_demo
cd django_oauth_demo/
python manage.py startapp api

Install django-oauth2-provider to our environment using

pip install django-oauth2-provider

Once django-oauth2-provider is installed, add it to the INSTALLED_APPS in settings.py to enable it. Also add our newly created app api to the list of installed apps.

# django_oauth_demo/django_oauth_demo/settings.py
INSTALLED_APPS = (
    ...
    'django.contrib.staticfiles',
    'provider',
    'provider.oauth2',
    'api',
)

Now run migrations to syncdb all the necessary tables.

python manage.py syncdb

Include provider.oauth2.urls into your urls.py file.

urlpatterns = patterns('',
    ...
    url(r'^oauth2/', include('provider.oauth2.urls', namespace = 'oauth2')),
)

Finally use the Django shell to create a new OAuth client. You can also use Django admin to create the client. You need to enable Django admin though. From Django shell, first create a user object to use with the client. And then use that user object to create OAuth client itself.

>>> from django.contrib.auth.models import User
>>> from provider.oauth2.models import Client
# Create the user object
>>> user = User(username='apitester')
>>> user.set_password('apitester')
>>> user.save()
# Create new client with the user object created above
>>> client = Client(user=user, name="api tester", client_type=1, url="http://example.com")
>>> client.save()
>>> client.client_id
u'43e2ece67d8e263b054d'
>>> client.client_secret
u'c496768bf6546424e8a74d66b5098efe32dfe6a5'

Testing the Provider

Now we can launch the server and test the OAuth Provider. Use the client_id and client_secret from the above shell output.

python manage.py runserver

I use curl to test the API. You can use any http client like Postman to test too. Using curl to test the API with client_id and client_secret from above step is given below.

curl -d "client_id=ec6b98fe31fc946b494b&client_secret=45490d6427d6f72c3d0864a66cdc7a6243171a8b&grant_type=password&username=apitester&password=apitester&scope=write" http://localhost:8000/oauth2/access_token

And if everything is fine, you will get back your access token like this -

{"access_token": "d4f4a90739e334e3c08495017745c6a8e4f8965f", "token_type": "Bearer", "expires_in": 2591999, "scope": "read write read+write"}

With client ids and secrets passing around, SSL encryption is a must have for your APIs.

Writing the API

Now we’ve got Oauth 2.0 working, lets move ahead to build a small API. Of-course you can use django packages like Tastypie or Django-Rest-Framework to build the API and django-oauth2-provider will easily merge into these packages to provide authentication. But here we are going to build a simple view that returns the username of the API client by reading its access token. The verify_access_token function inside api/views.py validates the access token. The code for verify_access looks like

    from django.utils import timezone
    from provider.oauth2.models import AccessToken
    
    def verify_access_token(key):
        try:
            token = AccessToken.objects.get(token=key)
    
            if token.expires < timezone.now():
                raise OAuthError('AccessToken has expired.')
        except AccessToken.DoesNotExist, e:
            raise OAuthError("AccessToken not found at all.")
            
        return token

And finally is_authenticated function inside the views.py adds token and other information to request object once call to verify_access_token is successful.

The actual view is pretty straightforward. It just calls the is_authenticated function with its request object. If is_authenticated returns False, it sends a 403 response. Otherwise username of the client is returned. We use Django’s csrf_exempt decorator to exempt this view from CSRF protection. The code for the view will look like

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def verify_token(request):

    if is_authenticated(request) == False:
        return HttpResponse(
            json.dumps({
                'error': 'invalid_request'
            }),
            status = 403
        )

    return HttpResponse(
        json.dumps({
            'user': request.user.username
        })
    )

Hook this view into urls.py by adding this url to your project’s urls.py.

import api.views

urlpatterns = patterns('',
    ...
    url(r'^api/verify', api.views.verify_token, name='verify' ),
    ...
)

And test this view using client by issuing the below command.

curl -d "token=64b4a073573aed46dcafa8172573f1b4219d5945" http://localhost:8000/api/verify

Note that the token is obtained from our previous request to oauth2/access_token. If everything is fine, you will get your username back.

{"user": "apitester"}

And thats it! I hope this small example will help you get started with building OAuth2.0 providers in Django. The type of authentication we used here is called OAuth 2 (two-legged). You can find many more types in Mashape’s OAuth bible.