A recent app I have been working on, I wanted to allow login via Google, but wanted to ensure that only pre-approved users could login using social login - the application is for a closed user group, not designed for the random public. It's essentially access via invite only. I tried django-invitations, but it felt more geared towards non-social login (sorry guys if I am mis-selling what looks like a great library!). Therefore, it was back to customising django-allauth.

If you're using django-cookiecutter for your new projects (and if you're not, you should) you may know that it comes with out of the box user login management, backed up by django-allauth. This package provides you the framework to build your own user management experience.

This tutorial isn't going to cover getting djang-allauth working from scratch - either take a peek at the django-cookiecutter source, or follow the installation from the docs here, we're looking at novel ways to change the django-allauth workflow.


Changing Behaviour

django-allauth allows for the over-riding default behaviour to built custom functionality via the use of adaptors.  Using cookiecutter? a custom adaptor stub has already been created for you under <project name>/users/adapters.py.  If not, go ahead and create an adapters.py file under one of your Django apps in your project - if you have a 'core' or 'settings' app this might be the best place for it. The location doesn't really matter, as you have to specify the location in your Django settings.

...
ACCOUNT_ADAPTER = 'myapp.users.adapters.AccountAdapter'
SOCIALACCOUNT_ADAPTER = 'myapp.users.adapters.SocialAccountAdapter'
...

settings.py

from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from django.conf import settings
from django.http import HttpRequest


class AccountAdapter(DefaultAccountAdapter):

    def is_open_for_signup(self, request: HttpRequest):
        return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)


class SocialAccountAdapter(DefaultSocialAccountAdapter):

    def is_open_for_signup(self, request: HttpRequest, sociallogin: Any):
        return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)

adapters.py

All credit for this code snip goes to the django-cookiecutter devs, it's simple and get's the point across. At the moment both Account and Social based logins are open for business, this adaptor has no net effect on operation at the moment.

My requirement is to allow Social login and signup only, so first off, let's disable account based signup.

...
class AccountAdapter(DefaultAccountAdapter):

    def is_open_for_signup(self, request: HttpRequest):
        return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", False)
...

adapters.py

If you head over to http://app/accounts/signup/, you should be told that signup is closed. Great. Next to get Social Logon working. To take the example of Google, you need to do the following;

  • ensure you have run ./manage.py migrate to ensure that the required social login tables have been created,
  • ensure you have added 'allauth.socialaccount.providers.google', to your INSTALLED_APPS
  • head over and create your Google OAuth Credentials,
  • Login to your Django Admin and create a new Social Application object of type 'Google'.

If you head to http://app/accounts/login/, Google should be listed as a login provider. Test your Google Login, it should allow you to login with your Google account without any issues. A new user will be  created under Users and Social Account.

We're nearly there, last step is to restrict to only 'pre-approved' users only. Go ahead and delete via Django Admin the Social Account object created just now (leave the User in place). The way we are going to restrict to pre-approved users is to change the SocialAccountAdapter behaviour. We only want only social logins from an email address with a valid User object already created (either manually via Django Admin or another management screen).

class SocialAccountAdapter(DefaultSocialAccountAdapter):

    def pre_social_login(self, request, sociallogin):
        try:
            get_user_model().objects.get(email=sociallogin.user.email)
        except get_user_model().DoesNotExist:
            from django.contrib import messages
            messages.add_message(request, messages.ERROR, 'Social logon from this account not allowed.') 
            raise ImmediateHttpResponse(HttpResponse(status=500))
        else:
            user = get_user_model().objects.get(email=sociallogin.user.email)
            if not sociallogin.is_existing:
                sociallogin.connect(request, user) 

    def is_open_for_signup(self, request, sociallogin):        
        return True

This is our updated Social adapter. is_open_for_signup still returns true as even a pre-authorised User still hits the signup code path on their first login. pre_social_login is invoked when Social Login is completed, but before the conversion to a valid Django session is done. Therefore, this is the point to check for pre-authorisation.

The try block looks for an existing User against the email provided by the social login request. If a user exists, link the social logon and continue, if not, abort the attempt and leave an update in the 'message' queue.

That's it, this should give a basis for pre-approved social login. A few improvements to make this production ready;

  • ensure you force email verification, as some social networks (i.e Facebook) have been flagged as having questionable verification procedures for email addresses. By forcing email verification, it is an extra step of protection,
  • Start to override the default templates to provide logon buttons / logos for each provider you decide to use,
  • make the user experience slicker by suppressing the need for a username.

A suggested config to get you started;

ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_ADAPTER = 'myapp.users.adapters.AccountAdapter'
SOCIALACCOUNT_ADAPTER = 'myapp.users.adapters.SocialAccountAdapter'
SOCIALACCOUNT_QUERY_EMAIL = True
SOCIALACCOUNT_AUTO_SIGNUP = True
ACCOUNT_USERNAME_REQUIRED = False

That's all for now. As ever comments and questions below!