LinkedIn authentication with Django Social Auth

Installation

Install the repository:

$ pip install django-social-auth==0.7.25

Add it to your project apps:

INSTALLED_APPS = (
    ...
    'social_auth'
)

Include the LinkedIn authentication backends:

AUTHENTICATION_BACKENDS = (
    'social_auth.backends.contrib.linkedin.LinkedinBackend',
    'django.contrib.auth.backends.ModelBackend',
)

Setup the URLs:

urlpatterns = patterns('',
    ...
    url(r'', include('social_auth.urls')),
    ...
)

Set the LinkedIn credentials:

LINKEDIN_CONSUMER_KEY = 'xxxx' # The LinkedIn application "API Key"
LINKEDIN_CONSUMER_SECRET = 'xxxx' # The LinkedIn application "Secret Key"

To gain those LinkedIn credentials, you will need to create an app on the LinkedIn developer site: https://www.linkedin.com/secure/developer You will then be provide with the “API Key” and the “Secret Key” proprietary to the app you just created.

To complete the installation, we create the database tables for the social auth app:

$ python manage.py migrate social_auth

LinkedIn user data

You must choose the data you will required from LinkedIn. First, you select from a list of LinkedIn data scopes:

LINKEDIN_SCOPE = ['r_basicprofile', 'r_emailaddress',]

Each scope defines a list of fields that LinkedIn will make available to your app. A descriptive list of thoses scopes are available here: http://developer.linkedin.com/documents/profile-fields

Once the scope are loaded, we need to tell LinkedIn which fields we require from the selected scopes. Here’s some explanation on how the field selector works: http://developer.linkedin.com/documents/field-selectors

LINKEDIN_EXTRA_FIELD_SELECTORS = [
    'first-name',
    'last-name',
    'email-address',
    'headline', # The job title
    'positions', # Used to retrieve the company
]

We determine which fields will be saved against the UserSocialAuth table, as JSON.

LINKEDIN_EXTRA_DATA = [
    ('id', 'id'),
    ('first-name', 'first_name'),
    ('last-name', 'last_name'),]
     + [(field, field.replace('-', '_'), True) for field in LINKEDIN_EXTRA_FIELD_SELECTORS]

Views

To complete our authentication workflow, we need to add a ‘complete’ and ‘logout’ custom views.

from django.conf import settings
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from django.template import RequestContext 
from django.contrib.auth.decorators import login_required
from django.contrib.auth import logout as auth_logout
from django.contrib.messages.api import get_messages
# Where the user is redirected after successful authentication
@login_required
def complete(request): 
    return render_to_response('auth/complete.html', {}, context_instance=RequestContext(request))
# Since the logged in user is a normal Django user instance, we logout the user the natural Django way:
def logout(request):
    """Logs out user"""
    auth_logout(request)
    return HttpResponseRedirect('/')
def error(request):
    """Error view"""
    messages = get_messages(request)
    return render_to_response('auth/error.html', {'messages': messages}, RequestContext(request))

Setup the URLs:

urlpatterns = patterns("",
    ...
    url(r"^complete/", "myapp.views.complete", name="complete"),
    url(r"^logout/", "myapp.views.logout", name="logout"),
    ...
)

Custom URL in settings:

LOGIN_URL = '/login/'
LOGIN_ERROR_URL = '/error/'
LOGIN_REDIRECT_URL = '/complete/'

Going a step further, integrating with Django registration

Django registration is a simple integraion of a sign up process based on Django Auth model. In this example, we will extend it with “django-registration-email” to enable emails as usernames.

$ pip install django-registration==0.8 django-registration-email==0.5.4

Add those apps to INSTALLED_APPS:

INSTALLED_APPS = (
    ...
    'registration'
    'registration_email',
)

Add the authentication backend:

AUTHENTICATION_BACKENDS = (
    'registration_email.auth.EmailBackend',
    ...
)

Add those settings:

REGISTRATION_EMAIL_REGISTER_SUCCESS_URL = '/complete/'

Setup the URLs:

from myapp.forms import CustomEmailRegistrationForm
urlpatterns = patterns("",
    ...
    # Override the default form
    url(r"^profile/register/",
        "registration.views.register",
        {"backend": "registration.backends.simple.SimpleBackend",
        "template_name": "registration/registration_form.html",
        "form_class": CustomEmailRegistrationForm,
        "success_url": getattr(
            settings, "REGISTRATION_EMAIL_REGISTER_SUCCESS_URL", None),
        },
        name="registration_register", 
    ),
    url(r"^profile/", include("registration_email.backends.default.urls")),
    ...
)

Create a custom form (by default copied and edited from the registration-email package):

from django import forms
from django.contrib.auth.models import User
from registration_email.forms import generate_username
attrs_dict={'class':'form-control'}
class CustomEmailRegistrationForm(forms.Form):
    name = forms.CharField(widget=forms.TextInput(attrs=attrs_dict))
    email = forms.EmailField(widget=forms.TextInput(attrs=attrs_dict))
    password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), label="Password")
    password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), label="Password (repeat)")
    def clean_email(self):
        email = self.cleaned_data['email'].strip()
        try:
            User.objects.get(email__iexact=email)
        except User.DoesNotExist:
            return email.lower()
        raise forms.ValidationError('Someone has already been registered with that email.')
    def clean(self):
        data = self.cleaned_data
        if not 'email' in data:
            return data
        if ('password1' in data and 'password2' in data):
            if data['password1'] != data['password2']:
                raise forms.ValidationError( "The two password fields didn't match.")
        self.cleaned_data['username'] = generate_username(self.cleaned_data['email'])
        # Create a temporary UserProfile, to be linked to forthcoming new User instance
        profile, created = UserProfile.objects.get_or_create(email=self.cleaned_data['email'])
        profile.name = self.cleaned_data['name']
        profile.save()
        return self.cleaned_data

Create the registration form template (by default in registration/registration_form.html):

<form action="{% url 'registration_register' %}" method="post">
    {% if form.errors %}
       <div class="alert alert-danger">
           {% for field in form %}
               {% for error in field.errors %}<div>{{ error }}</div>{% endfor %}
           {% endfor %}
       </div>
    {% endif %}
    <fieldset>
        {% csrf_token %}
        {% for field in form %}
        <div class="form-group{% if field.errors %} has-error{% endif %}">
            <label class="control-label" for="{{field.auto_id}}">{{field.label}}</label>
            {{field}}
        </div>
        {% endfor %}
    </fieldset>
    <input type="submit" value="Register" class="btn btn-primary">
</form>

Setup your own Profile models (this is where we will save the LinkedIn user data):

from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
    user = models.OneToOneField(User, blank=True, null=True, unique=True)
    company = models.CharField(max_length=150, blank=True)
    job_title = models.CharField(max_length=150, blank=True)
    def __unicode__(self):
        return u'Profile: %s' % self.get_full_name()
def create_user_profile(sender, instance, created, **kwargs):
    """Get the temproray profile creating in the form class, then linked it to the user instance"""
    profile, created = UserProfile.objects.get_or_create(email=instance.email)
    profile.user = instance
    profile.save()
post_save.connect(create_user_profile, sender=User)

You will note that we use a signal ‘post_save’ to catch the moment when the User model has been saved, at which point we will connect the temporary UserProfile instance create in CustomEmailRegistrationForm.

And add it to your settings:

AUTH_PROFILE_MODULE = 'myapp.UserProfile'

Create the database tables for the registration packages:

$ python manage.py syncdb

Now, if you go to ‘/profile/register/’, you will be able to test the registration form.

Connect the social auth data to our user profile using pipeline

As you may already know, Django Social Auth uses a pipeline defined by a list of function to determine the authentication workflow. In our scenario, we need to add an extra function that will get the user data retrieve from the provider and save it againt the UserProfile model.

SOCIAL_AUTH_PIPELINE = (
    'social_auth.backends.pipeline.social.social_auth_user',
    'social_auth.backends.pipeline.user.get_username',
    'social_auth.backends.pipeline.user.create_user',
    'social_auth.backends.pipeline.social.associate_user',
    'social_auth.backends.pipeline.social.load_extra_data',
    'social_auth.backends.pipeline.user.update_user_details',
    'myqpp.models.social_auth_to_profile'
)

Add the social_auth_to_profile next to your UserProfile model class:

def social_auth_to_profile(backend, details, response, user=None, is_new=False, *args, **kwargs):
    if is_new:
        profile = UserProfile.objects.get_or_create(user=user)
    else:
        profile = UserProfile.objects.get(user=user)
    # Some of the default user details given in the pipeline
    profile.email = details['email']
    profile.name = details['fullname']
    # Now we also need the extra details, found in the `social_user` kwarg
    social_user = kwargs['social_user']
    profile.company = social_user.extra_data['headline']
    profile.job_title = social_user.extra_data['positions']['position'][0]['title']
    profile.save()

At that point, once a user has successfully connected using LinkedIn (it will also work for other platform, though the fields may change), it will create a UserProfile instance and save the LinkedIn data against it.

That may, whether the user has registered via email or LinkedIn, you will find on single table of profile to work from.

Happy coding.

< / >