california app design company

Social Auth with Django REST Framework

June 24, 2015

This post is part 3 of a series on using OAuth with Django REST Framework. Part 2 focused on setting up your own OAuth2 server for your application, part 4 offers a richer explanation of server vs. client oauth flows, and part 5 is about integrating parts 2 & 3. For a summary of my thoughts on the process, see the series overview.

This post is to help you set up Django REST Framework with Python Social Auth; the goal here is to allow users to sign up and sign in to your app using Facebook, Twitter, etc., using API endpoints that you manage with Django REST Framework. If you are using Tastypie instead of DRF, see our complemetary piece: Integrating Django, Tastypie & Python Social Auth.

1. Install and Configure

I'm assuming that you already have a Django project set up using DRF and some standard variant on a Django User model. With that out of the way, you just need to do a simple pip install python-social-auth and follow the Django configuration instructions. Don't forget to migrate your database.

2. Request / Response

Your front-end application will pass up the following data in a request:

{
    "provider": "facebook",  // or twitter, instagram, etc.
    "access_token": "ABC123",  // user's access_token assigned by provider
    "access_token_secret": "DEF456"  // required by some providers, e.g. Twitter
}

You'll need to add a route to your view:

urlpatterns = patterns('',
url(r'^social_sign_up/$', views.SocialSignUp.as_view(), name="social_sign_up"),
)

And the view above responds with your user instance (my serializer is just returning some basic user data).

3. SocialSignUp View

This is the bulk of the interesting work. Python social auth is a great out of the box solution using Django's views and templates, but takes a little picking apart to get to work with your DRF API. Python social auth knows how to use Django's views and urls to go through a server-side oauth flow. But if we want to initialize that process within our DRF view, we'll have to manually call some of the functions python social auth provides.

from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.response import Response
from social.apps.django_app import load_strategy
from social.apps.django_app.utils import load_backend
from social.backends.oauth import BaseOAuth1, BaseOAuth2
from social.exceptions import AuthAlreadyAssociated


User = get_user_model()


class SocialSignUp(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = SocialSignUpSerializer
    # This permission is nothing special, see part 2 of this series to see its entirety
    permission_classes = (IsAuthenticatedOrCreate,)

    def create(self, request, *args, **kwargs):
        """
        Override `create` instead of `perform_create` to access request
        request is necessary for `load_strategy`
        """
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        provider = request.data['provider']

        # If this request was made with an authenticated user, try to associate this social 
        # account with it
        authed_user = request.user if not request.user.is_anonymous() else None

        # `strategy` is a python-social-auth concept referencing the Python framework to
        # be used (Django, Flask, etc.). By passing `request` to `load_strategy`, PSA 
        # knows to use the Django strategy
        strategy = load_strategy(request)
        # Now we get the backend that corresponds to our user's social auth provider
        # e.g., Facebook, Twitter, etc.
        backend = load_backend(strategy=strategy, name=provider, redirect_uri=None)

        if isinstance(backend, BaseOAuth1):
            # Twitter, for example, uses OAuth1 and requires that you also pass
            # an `oauth_token_secret` with your authentication request
            token = {
                'oauth_token': request.data['access_token'],
                'oauth_token_secret': request.data['access_token_secret'],
            }
        elif isinstance(backend, BaseOAuth2):
            # We're using oauth's implicit grant type (usually used for web and mobile 
            # applications), so all we have to pass here is an access_token
            token = request.data['access_token']

        try:
            # if `authed_user` is None, python-social-auth will make a new user,
            # else this social account will be associated with the user you pass in
            user = backend.do_auth(token, user=authed_user)
        except AuthAlreadyAssociated:
            # You can't associate a social account with more than user
            return Response({"errors": "That social media account is already in use"},
                            status=status.HTTP_400_BAD_REQUEST)

        if user and user.is_active:
            # if the access token was set to an empty string, then save the access token 
            # from the request
            auth_created = user.social_auth.get(provider=provider)
            if not auth_created.extra_data['access_token']:
                # Facebook for example will return the access_token in its response to you. 
                # This access_token is then saved for your future use. However, others 
                # e.g., Instagram do not respond with the access_token that you just 
                # provided. We save it here so it can be used to make subsequent calls.
                auth_created.extra_data['access_token'] = token
                auth_created.save()

            # Set instance since we are not calling `serializer.save()`
            serializer.instance = user
            headers = self.get_success_headers(serializer.data)
            return Response(serializer.data, status=status.HTTP_201_CREATED, 
                            headers=headers)
        else:
            return Response({"errors": "Error with social authentication"},
                            status=status.HTTP_400_BAD_REQUEST)

What's going on here? We're handling a few different cases:

  • You have an existing user that is now associating a social account
  • You have a brand new user signing up with a social account
  • The authentication provider may be using OAuth1 or OAuth2

Admittedly this view is a bit long and not the most OO way to handle the nuances of all the different social providers. Ideally you could write something similar to PSA's provider classes that you could plug in here. Currently this has been tested with Facebook, Twitter, and Instagram.

That's it! You should now be able to use your app to sign up or login users from their social accounts (both will use the same view).

Note

If you're reading this and wondering how to get this access token that you are passing to your API, the answer is that your front-end application is doing that. For example, you have a JavaScript or mobile application that is using the Facebook SDK, or is manually going through the provider's OAuth flow. The mechanics of that are outside the scope of this post, but to be clear, all that should be happening in the front-end. When you look at the documentation for whatever provider you are using, you will want to follow the instructions for the implicit or client-side oauth flow. Yes, DRF and python-social-auth are working server-side, but getting the access token is happening client-side, so that's the flow you want to follow. Essentially, the entire auth flow happens client-side--python social auth is just helping us request information from the social providers and save associations to the user. More detail on those processes in the next post in this series.

is a Yeti Alum. At Yeti, she builds server-side APIs and web and mobile front-ends for applications. She is a former management consultant who decided to leave it all and attend a programming bootcamp. Outside of work, Baylee serves on the Board of Directors of Out for Undergrad, a non-profit that helps high-achieving LGBT undergrads reach their full potentials in their careers. Follow Baylee on Twitter.

blog comments powered by Disqus
Social Auth with Django REST Framework https://s3-us-west-1.amazonaws.com/yeti-site-static/img/blog-default.jpg
Yeti (415) 766-4198 https://s3-us-west-1.amazonaws.com/yeti-site-static/img/yeti-head-blue.png