Django Allauth Custom Provider

Django AllAuth is an amazingly useful and popular package for adding 3rd party social authentication to your web app. At some point you may come across a platform with a rest API, which employs OAuth2, that you need to integrate with. I think its fair to say that building this connection entirely from scratch is fairly daunting, and I know this because it represent a significant barrier for a lot of the 3rd party integrators I meet through my work who get in touch with the support team, (often to ask for a different authentication approach all together). 

I had been wanting to build an easily shareable example for ages, but had been putting it off for the aforementioned reasons. I'd cobbled together what I consider to be a reasonable workaround, but in the back of my mind I knew it was not....'proper', (for want of a better phrase). I had used Django AllAuth, and I knew that it did what I wanted it to do, in relation to other well known social platforms, but I wasnt sure how to go about bending it to work with the products we offer.

That was until I peeked at the advanced usage section in their docs:

When an existing provider doesn’t quite meet your needs, you might find yourself needing to customize a provider.

This can be achieved by subclassing an existing provider and making your changes there. Providers are defined as django applications, so typically customizing one will mean creating a django application in your project. This application will contain your customized urls.py, views.py and provider.py files. The behaviour that can be customized is beyond the scope of this documentation.

Ok...that doesn't sound too bad! I read that as 'look at what has already been implemented and just adapt it for your OAuth2 server'. I began poking around in the source code for the various different providers Allauth currently ships with and started to notice snippets of code which resembled what I was already familiar with. After a bit more searching, I found 3 things which were an enourmous help: 

Between these three examples, I was finally able to implement what had been evading me for months. Here is a walkthrough of the steps I took.Obviously my exampe is specific to my companies product, but you should be able to adapt it to your service. 



Assuming your Django project is created and ready to go, make sure that allauth is installed: 
 

pip install django-allauth


As you may have already gathered from the docs quoted above, your provider needs to be its own Django app, so create that:
 

python manage.py startapp provider


Next up are the changes to settings. The app needs to be installed, alongside the relevant ones from the allauth package. SITE_ID and AUTHENTICATION_BACKENDS should already be there from the standard setup of allauth. OAUTH SERVER BASE URL is self explanatory! In my case, its a test instance of my company's product which is going to be providing our authentication. For this example, I will only create a homepage, hence the login and out redirects both being 'home'. 
 

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'core.apps.CoreConfig',
    'django.contrib.sites',

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'provider',

]

LOGIN_REDIRECT_URL = 'home'

ACCOUNT_LOGOUT_REDIRECT = 'home'

SITE_ID = 1

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
)

OAUTH_SERVER_BASEURL = 'https://example.provider.com/example'


My project is called 'properauth' and it also has a 'core' app. properauth/urls.py looks like: 

from django.contrib import admin
from django.urls import path, include
from core.views import Home


urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('allauth.urls')),
    path('', Home.as_view(), name='home')

]


The home view is: 

from django.views.generic import TemplateView

class Home(TemplateView):
    template_name = 'home.html'

and that template: 

{% load socialaccount %}

<h1>Web App with OAuth2 Log In </h1>

{% if user.is_authenticated %}

<p>Welcome {{ user.email }}, you just logged in via your instance!</p>

    <form method="post" action="{% url 'account_logout' %}">
        {% csrf_token %}
        <button type="submit">Log Out</button>
    </form>

{% else %}

    <p>Click Here to log in:</p>

    <a href="{% provider_login_url 'provider' %}">Sign In</a>

{% endif %}

Now that the basics are all in place, and the database has been migrated:

python manage.py makemigrations
python manage.py migrate

.....lets get down to actually creating the custom provider! 


The views file inside my provider app looks like this: 

import requests
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter, OAuth2LoginView, OAuth2CallbackView)
from .provider import CustomProvider
from django.conf import settings


class CustomAdapter(OAuth2Adapter):
    provider_id = CustomProvider.id

    access_token_url = f'{settings.OAUTH_SERVER_BASEURL}/api/oauthtoken'
    profile_url = f'{settings.OAUTH_SERVER_BASEURL}/api/useraccount/'

    authorize_url = f'{settings.OAUTH_SERVER_BASEURL}/authorization'

    def complete_login(self, request, app, token, **kwargs):
        headers = {'Authorization': f'Bearer {token.token}', 'Accept':'application/json'}
        useremail = kwargs['response']['useremail']
        resp = requests.get(self.profile_url + f'{useremail}?type=email', headers=headers)
        extra_data = resp.json()
        return self.get_provider().sociallogin_from_response(request, extra_data)


oauth2_login = OAuth2LoginView.adapter_view(CustomAdapter)
oauth2_callback = OAuth2CallbackView.adapter_view(CustomAdapter)


The key elements here are: 

'access_token_url' : This is the url that allauth will be querying to get the access tokens.

'profile_url' : This is the url that we will use to get our user detail. If you compare my views file to those in the resoures I referneced above, you'll notice that my provider makes it slightly less easy than other providers to get this. The endpoint requires a useremail parameter to get the correct details. We can get this from the token call (which is pass as kwargs to the complete_login() function.The service returns XML by default, so I am also adding an 'Accept' header because allauth expects json here (as you will see in the next file). 

'authorize_url' : This is the url where we get the 'authorization code' from in THIS part of the OAuth2 flow. 


Next up we move onto the 'provider.py' file which is also in the provider app:
 

from allauth.socialaccount import providers
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider


class CustomAccount(ProviderAccount):
    pass


class CustomProvider(OAuth2Provider):

    id = 'provider'
    name = 'ProviderOAuth2 Provider'
    account_class = CustomAccount

    def extract_uid(self, data):
        return str(data['userid'])

    def extract_common_fields(self, data):
        return dict(
                    email=data['email'],
                    first_name=data['firstname'],
                    last_name=data['lastname'],)

    # def get_default_scope(self):
    #     pass


providers.registry.register(CustomProvider)

 

I am commenting out 'get_default_scope()' because we do not use 'scope' in our product at work. The extract methods are pulling data out the structure that we get from querying the 'profile_url' in the views file. 

Lastly, we take care of the provider/urls.py: 
 

from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import CustomProvider

urlpatterns = default_urlpatterns(CustomProvider)

 

Now the custom provider has been taken care of, we can register the social application in our Django app, and our API consumer at the external service's end. 

Create a superuser, so that you can log in to the Django admin: 
 

python manage.py createsuperuser


Login to the admin app and then click on 'Add' next to social application:


 

The provider is added like so: 



You'll notice we have a client id and secret key here. We retrieve these when we register the API consumer at the Provider end: 



The full redirect URL value here is: 'http://127.0.0.1:8000/accounts/provider/login/callback/'. 

That should be it now. If we run the server, these are the results:

  1. Unauthenticated user hits the provider login url
  2. They are redirected to the provider instance. if they do not have an authenticated session there, they will be prompted to login and then they hit the 'Allow/Deny' screen. 
  3. Upon allowing the api consumer to access their account they are then redirected back to the Django application. Under the hood, the provider passes a code back to django allauth which allows it to make an access token call, as well as the subsequent call to grab the user details. 
  4.  The user is now authenticated in the Django app, courtsey of my company's product. Check the Admin app again: 


Now the log in and out flow is applied, it should be clear how we can proceed integrating further rest calls from our Django application to the provider's service. Utilising all-auth's ability to easily have custom providers was actually a fairly painless process. Now all-auth is taking care of getting an external system to provide my authentication, I can concentrate on building a second, more complete integration with the Legal Tech SaaS Product my company offers! 
 

You may also like: