Wednesday, September 25, 2013

Securing Your Django Rest Service with OAuth2

Once you have a rest service that you want others to access, you probably want to secure it. There are many ways this can be done but I thought I'd look in to the OAuth authentication standard. OAuth is a specification specifically for Authorization and provides patterns for distributing access tokens that can be checked to protect server side resources.

For example, in a simple case, you may have one server trying to access a service or resource from another service. First, the calling server would create an account on the server owning the service and obtain its own client id and client secret which would need to be stored securely. Once it has its own secret, it can send its id and secret to the owning service for an authorization check and receive an access token in exchange. That access token can then be used to retrieve data from the server.

In a more complex example, a mobile app may want to pull data from a rest service. A mobile app cannot truly store a client secret and consider it private so extra steps need to be taken. To accomplish this, an account would need to exist on the server owning the service for any user that wants access. The mobile app would first direct the user to a log in page on the remote server owning the service where they either log in or create an account there if one doesn't exist. Once they have an account and enter their credentials, the server will redirect back to a url specified by the app and pass it an authorization code. The app would listen for the redirect and take the access code from the redirect. Now the app can call the server again passing its client id, client secret and access code and get an oauth token back. Now the mobile app can then use this token to make requests to the service. The use of a log in page and redirect url are important as we don't want the mobile app to need to store the username or password for security reasons and we don't want to pass back the authorization code to just anyone. It is usally recommended to somehow register the redirect url with the remote server to prevent third parties from passing random urls that they control.

For more details on these scenarios, refer to the spec here.

For this tutorial we will focus on the simple case of a website or service requesting data from another service. Our consumer service is assumed to be securely storing its client id and secret. As OAuth2 is just a specification, there are multiple implementation libraries out there. For this demonstration we will use the django-oauth2-provider (https://github.com/caffeinehit/django-oauth2-provider). Assuming you have pip, you can install it like this:

sudo pip install django-oauth2-provider

Once we have the provider installed, we want to allow django to use it. Modify settings.py by adding to INSTALLED_APPS:

INSTALLED_APPS = (
   ...
   'provider',
   'provider.oauth2',
   ...
)

Now we want to add the following entry to urls.py:

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

This code sets up a url to route token requests for oauth2. We will use that shortly. First we need to set up a client id and secret that we can use to request a token. For this we will use the admin console and a backend datastore to store our information. In my case, django is configured to use my local MySql database. Make sure your database is running and enter the console like this:

python manage.py shell

Now in the shell you can enter your script to create a new user, associate it with a client and get the clients id and secret:

from provider.oauth2.models import Client
from django.contrib.auth.models import User
user = User.objects.create_user('john', 'john@djangotest.com', 'abcd')
c = Client(user=user, name="mysite client", client_type=1, url="http://djangotest.com")
c.save()

Then you can use c.client_id and c.client_secret to get the information for the new client. In this case, the id is:

'c513118ee3b176805722'
and the secret is:
'd4e5bca2996c8c543349cf0ce140bcd73c86450c'

With this information we can now make calls to the url we set up above to get a token. For example, if your url is http://johnssite.com and django is running on port 80 then you would send a post or get request to:

http://johnssite.com/oauth2/accesstoken

The data to post would look like this:

client_id=c513118ee3b176805722&client_secret=d4e5bca2996c8c543349cf0ce140bcd73c86450c&grant_type=password&username=john&password=abcd&scope=write

This should return a token you can use for your requests.

Now we can fix our service to make use of incoming tokens. The first step is to make sure https is on for your service. This is important to protect the information you are sending to the authorization service. Next we will modify our service to expect authentication. We can add the following code to what we previously had in views.py for our rest service:

import datetime
from provider.oauth2.models import AccessToken
...
if request.method == 'GET':
    restaurants = dbconn['restaurants']
    rests = []

    key = request.META.get('HTTP_AUTHORIZATION')
    if not key:
        return Response({ 'Error': 'No token provided.'})
    else:
        try:
            token = AccessToken.objects.get(token=key)
            if not token:
                return Response({'Error': 'Access denied.'})

            if token.expires < datetime.datetime.now():
                return Response({'Error': 'Token has expired.'})
        except AccessToken.DoesNotExist, e:
            return Response({'Error': 'Token does not exist.'})

     ...

In this code we first take the token that should have been passed in to the authorization header. If you are expecting it as an argument, you can modify the code accordingly. If we have a token, we next check that it exists in the token store that issued it and then check that it didn't expire. If all tests pass, the code flow continues to get the data and return it.

So that's it. Using the django-oauth2-provider library makes it pretty straight forward to create a token authorization service. With only a little extra effort we added basic security to our site.