Search code examples
djangodjango-allauth

Django-allauth: My post-sign-up redirect flow with Google Auth is broken and I can't figure out why


I integrated django-allauth with my Django app but something is not fully working:

Problem

  • If a user that does not have an account in my DB tries to sign-up using the the google allauth process, after the authentication process it is sent to the home page (behind login filter) successfully.

  • However, if a user that already has an account in my DB (manually created) tries to login using the google auth by visiting /accounts/google/login/, after the authentication process it is sent to /accounts/social/signup/ (a weird page that django-allauth has). Funny enough thought, the user is logged in and if they try to access the home page they can and everything works. So it's just the redirect that is broken.

I narrowed it down to the fact that the user's email already exist BUT there is no social account associated.

Setup

I have a custom user model where email is the main unique id of a user.

from django.contrib.auth.base_user import BaseUserManager
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import ugettext_lazy as _


class CustomUserManager(BaseUserManager):
    """
    Custom user model manager where email is the unique identifiers
    for authentication instead of usernames.
    """
    def create_user(self, email, password, **extra_fields):
        """
        Create and save a User with the given email and password.
        """
        if not email:
            raise ValueError(_('The Email must be set'))
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        """
        Create and save a SuperUser with the given email and password.
        """
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        extra_fields.setdefault('is_active', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError(_('Superuser must have is_staff=True.'))
        if extra_fields.get('is_superuser') is not True:
            raise ValueError(_('Superuser must have is_superuser=True.'))
        return self.create_user(email, password, **extra_fields)


class CustomUser(AbstractUser):
    username = None
    email = models.EmailField(_('email address'), unique=True)

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = CustomUserManager()

    def __str__(self):
        return self.email

I have an Adapter that I hoped was going to solve my problem but didn't (so I did something wrong on it)

class MySocialAccountAdapter(DefaultSocialAccountAdapter):
    def pre_social_login(self, request, sociallogin):
        user = sociallogin.user
        if user.id:
            return
        try:
            # if user exists, connect the account to the existing account and login
            customer = CustomUser.objects.get(email=user.email)
            sociallogin.state['process'] = 'connect'
            #sociallogin.connect(request, user)
            perform_login(request, customer, 'none')
        except CustomUser.DoesNotExist:
            pass

my settings

...

# Application definition

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

    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'whitenoise.runserver_nostatic',  # new
]

MIDDLEWARE = [
    #'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]


...


AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]



AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
    'guardian.backends.ObjectPermissionBackend',
]

SITE_ID = 3
LOGIN_REDIRECT_URL = '/'
ACCOUNT_ADAPTER = 'myusermodel.adapter.MyAccountAdapter'



# Additional configuration settings
SOCIALACCOUNT_QUERY_EMAIL = True
ACCOUNT_LOGOUT_ON_GET = True
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
AUTH_USER_MODEL = 'myusermodel.CustomUser'



SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'SCOPE': [
            'profile',
            'email',
        ],
        'AUTH_PARAMS': {
            'access_type': 'online',
        }
    }
}

...


Solution

  • FIXED!

    This is my new adapter, it was as simple as that!

    from allauth.account.adapter import DefaultAccountAdapter
    from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
    from django.shortcuts import redirect, reverse
    from myusermodel.models import CustomUser
    
    
    
    class MyAccountAdapter(DefaultAccountAdapter):
    
        def get_login_redirect_url(self, request):
            print('I am entering get_login_redirect_url')
            if 'team_membership_project_id' in request.session:
                parameters = {}
                parameters['invitation_id'] = request.session['invitation_id']
                path = reverse('action:accept_invitation', urlconf=None, args=None, kwargs=parameters)
                return path
    
            path = '/'
    
            return path
    
        def is_open_for_signup(self, request):
            """
            Checks whether or not the site is open for signups.Next to simply returning True/False you can also intervene the
            regular flow by raising an ImmediateHttpResponse. (Comment reproduced from the overridden method.)
            """
            return True
    
    
    class MySocialAccountAdapter(DefaultSocialAccountAdapter):
        def pre_social_login(self, request, sociallogin):
            user = CustomUser.objects.filter(email=sociallogin.user.email).first()
            if user and not sociallogin.is_existing:
                sociallogin.connect(request, user)