Adding a new backend

Add new backends is quite easy, usually adding just a class with a couple settings and methods overrides to retrieve user data from services API. Follow the details below.

Common attributes

First, lets check the common attributes for all backend types.

name = ''
Any backend needs a name, usually the popular name of the service is used, like facebook, twitter, etc. It must be unique, otherwise another backend can take precedence if it’s listed before in AUTHENTICATION_BACKENDS setting.
ID_KEY = None
Defines the attribute in the service response that identifies the user as unique in the service, the value is later stored in the uid attribute in the UserSocialAuth instance.
REQUIRES_EMAIL_VALIDATION = False
Flags the backend to enforce email validation during the pipeline (if the corresponding pipeline social_core.pipeline.mail.mail_validation was enabled).
EXTRA_DATA = None
During the auth process some basic user data is returned by the provider or retrieved by user_data() method which usually is used to call some API on the provider to retrieve it. This data will be stored under UserSocialAuth.extra_data attribute, but to make it accessible under some common names on different providers, this attribute defines a list of tuples in the form (name, alias) where name is the key in the user data (which should be a dict instance) and alias is the name to store it on extra_data.

OAuth

OAuth1 and OAuth2 provide share some common definitions based on the shared behavior during the auth process, like a successful API response from AUTHORIZATION_URL usually returns some basic user data like a user Id.

Shared attributes

name
This defines the backend name and identifies it during the auth process. The name is used in the URLs /login/<backend name> and /complete/<backend name>.
ID_KEY = 'id'
Default key name where user identification field is defined, it’s used on auth process when some basic user data is returned. This Id is stored in UserSocialAuth.uid field, this together the UserSocialAuth.provider field is used to unique identify a user association.
SCOPE_PARAMETER_NAME = 'scope'
Scope argument is used to tell the provider the API endpoints you want to call later, it’s a permissions request granted over the access_token later retrieved. Default value is scope since that’s usually the name used in the URL parameter, but can be overridden if needed.
DEFAULT_SCOPE = None
Some providers give nothing about the user but some basic data in required like the user Id or an email address. Default scope attribute is used to specify a default value for scope argument to request those extra used bits.
SCOPE_SEPARATOR = ' '
The scope argument is usually a list of permissions to request, the list is joined used a separator, usually just a blank space, but differ from provider to provider, override the default value with this attribute if it differs.

OAuth2

OAuth2 backends are fairly simple to implement; just a few settings, a method override and it’s mostly ready to go.

The key points on this backends are:

AUTHORIZATION_URL
This is the entry point for the authorization mechanism, users must be redirected to this URL, used on auth_url method which builds the redirect address with AUTHORIZATION_URL plus some arguments (client_id, redirect_uri, response_type, and state).
ACCESS_TOKEN_URL
Must point to the API endpoint that provides an access_token needed to authenticate in users behalf on future API calls.
REFRESH_TOKEN_URL
Some providers give the option to renew the access_token since they are usually limited in time, once that time runs out, the token is invalidated and cannot be used any more. This attribute should point to that API endpoint.
RESPONSE_TYPE
The response type expected on the auth process, default value is code as dictated by OAuth2 definition. Override it if default value doesn’t fit the provider implementation.
STATE_PARAMETER
OAuth2 defines that an state parameter can be passed in order to validate the process, it’s kind of a CSRF check to avoid man in the middle attacks. Some don’t recognise it or don’t return it which will make the auth process invalid. Set this attribute to False in that case.
REDIRECT_STATE
For those providers that don’t recognise the state parameter, the app can add a redirect_state argument to the redirect_uri to mimic it. Set this value to False if the provider likes to verify the redirect_uri value and this parameter invalidates that check.

Example code:

from social_core.backends.oauth import BaseOAuth2

class GithubOAuth2(BaseOAuth2):
    """Github OAuth authentication backend"""
    name = 'github'
    AUTHORIZATION_URL = 'https://github.com/login/oauth/authorize'
    ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'
    SCOPE_SEPARATOR = ','
    EXTRA_DATA = [
        ('id', 'id'),
        ('expires', 'expires')
    ]

    def get_user_details(self, response):
        """Return user details from Github account"""
        return {'username': response.get('login'),
                'email': response.get('email') or '',
                'first_name': response.get('name')}

    def user_data(self, access_token, *args, **kwargs):
        """Loads user data from service"""
        url = 'https://api.github.com/user?' + urlencode({
            'access_token': access_token
        })
        return self.get_json(url)

OAuth1

OAuth1 process is a bit more trickier, Twitter Docs explains it quite well. Beside the AUTHORIZATION_URL and ACCESS_TOKEN_URL attributes, a third one is needed used when starting the process.

REQUEST_TOKEN_URL = ''
During the auth process an unauthorized token is needed to start the process, later this token is exchanged for an access_token. This setting points to the API endpoint where that unauthorized token can be retrieved.

Example code:

from xml.dom import minidom

from social_core.backends.oauth import ConsumerBasedOAuth


class TripItOAuth(ConsumerBasedOAuth):
    """TripIt OAuth authentication backend"""
    name = 'tripit'
    AUTHORIZATION_URL = 'https://www.tripit.com/oauth/authorize'
    REQUEST_TOKEN_URL = 'https://api.tripit.com/oauth/request_token'
    ACCESS_TOKEN_URL = 'https://api.tripit.com/oauth/access_token'
    EXTRA_DATA = [('screen_name', 'screen_name')]

    def get_user_details(self, response):
        """Return user details from TripIt account"""
        try:
            first_name, last_name = response['name'].split(' ', 1)
        except ValueError:
            first_name = response['name']
            last_name = ''
        return {'username': response['screen_name'],
                'email': response['email'],
                'fullname': response['name'],
                'first_name': first_name,
                'last_name': last_name}

    def user_data(self, access_token, *args, **kwargs):
        """Return user data provided"""
        url = 'https://api.tripit.com/v1/get/profile'
        request = self.oauth_request(access_token, url)
        content = self.fetch_response(request)
        try:
            dom = minidom.parseString(content)
        except ValueError:
            return None

        return {
            'id': dom.getElementsByTagName('Profile')[0].getAttribute('ref'),
            'name': dom.getElementsByTagName(
                'public_display_name')[0].childNodes[0].data,
            'screen_name': dom.getElementsByTagName(
                'screen_name')[0].childNodes[0].data,
            'email': dom.getElementsByTagName(
                'is_primary')[0].parentNode.getElementsByTagName(
                'address')[0].childNodes[0].data,
        }

OpenId

OpenId is fair simpler that OAuth since it’s used for authentication rather than authorization (regardless it’s used for authorization too).

A single attribute is usually needed, the authentication URL endpoint.

URL = ''
OpenId endpoint where to redirect the user.

Sometimes the URL is user dependant, like in myOpenId where the URL is https://<user handler>.myopenid.com. For those cases where the user must input it’s handle (or full URL). The backend must override the openid_url() method to retrieve it and return a full URL to where the user will be redirected.

Example code:

from social_core.backends.open_id import OpenIdAuth
from social_core.exceptions import AuthMissingParameter


class LiveJournalOpenId(OpenIdAuth):
    """LiveJournal OpenID authentication backend"""
    name = 'livejournal'

    def get_user_details(self, response):
        """Generate username from identity url"""
        values = super(LiveJournalOpenId, self).get_user_details(response)
        values['username'] = values.get('username') or \
                             urlparse.urlsplit(response.identity_url)\
                                        .netloc.split('.', 1)[0]
        return values

    def openid_url(self):
        """Returns LiveJournal authentication URL"""
        if not self.data.get('openid_lj_user'):
            raise AuthMissingParameter(self, 'openid_lj_user')
        return 'http://%s.livejournal.com' % self.data['openid_lj_user']

Auth APIs

For others authentication types, a BaseAuth class is defined to help. Those custom auth methods must override the auth_url() and auth_complete() methods.

Example code:

from google.appengine.api import users

from social_core.backends.base import BaseAuth
from social_core.exceptions import AuthException


class GoogleAppEngineAuth(BaseAuth):
    """GoogleAppengine authentication backend"""
    name = 'google-appengine'

    def get_user_id(self, details, response):
        """Return current user id."""
        user = users.get_current_user()
        if user:
            return user.user_id()

    def get_user_details(self, response):
        """Return user basic information (id and email only)."""
        user = users.get_current_user()
        return {'username': user.user_id(),
                'email': user.email(),
                'fullname': '',
                'first_name': '',
                'last_name': ''}

    def auth_url(self):
        """Build and return complete URL."""
        return users.create_login_url(self.redirect_uri)

    def auth_complete(self, *args, **kwargs):
        """Completes login process, must return user instance."""
        if not users.get_current_user():
            raise AuthException('Authentication error')
        kwargs.update({'response': '', 'backend': self})
        return self.strategy.authenticate(*args, **kwargs)