djangopython-social-auth

Associate user by email


I want to override the pipeline to associate user's with their email for accounts that are only active. But I need the backend for a regular login. The AUTHENTICATION_BACKENDS(django.contrib.auth.backends.AllowAllUsersModelBackend) allows for all user's to be authenticated but I want only certain user's with the is_active to be authenticated using Google Login.

settings.py

AUTHENTICATION_BACKENDS = ['social_core.backends.google.GoogleOAuth2','django.contrib.auth.backends.AllowAllUsersModelBackend',]
# Extends default user with additional fields 
AUTH_USER_MODEL = 'pages.Profile' 
SOCIAL_AUTH_USER_MODEL = 'pages.Profile' 

# social auth configs for google
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = config('GOOGLE_OAUTH2_KEY')
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = config('GOOGLE_OAUTH2_SECRET')
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['https://www.googleapis.com/auth/calendar']
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS = {'access_type': 'offline','approval_prompt':'force'}
SESSION_COOKIE_SAMESITE = None
SOCIAL_AUTH_PIPELINE = (
    'social_core.pipeline.social_auth.social_details',
    'social_core.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.auth_allowed',
    'social_core.pipeline.social_auth.social_user',
    'social_core.pipeline.user.get_username',
    'social_core.pipeline.social_auth.associate_by_email',  # <--- enable this one
    'social_core.pipeline.user.create_user',
    'social_core.pipeline.social_auth.associate_user',
    'social_core.pipeline.social_auth.load_extra_data',
    'social_core.pipeline.user.user_details',
    'pages.pipeline.save_token'
) 

views.py

def login(request):
    if request.method == 'POST':
        form = AuthenticationForm(request.POST)
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user:
            if user.is_active:
                auth_login(request, user)
                return redirect('home')
            else:
                messages.error(request,'User blocked',extra_tags='login')
                return redirect('login')
        else:
            messages.error(request,'username or password not correct',extra_tags='login')
            return redirect('login')
    else:
        form = AuthenticationForm()
    return render(request, 'registration/login.html',{'form':form})

Solution

  • To override the original partial,

    In your pipeline.py (create one, if you don't have it in your app directory), define a method:

    from social_core.pipeline.partial import partial
    from social_core.exceptions import AuthAlreadyAssociated, AuthException, AuthForbidden
    
    
    @partial
    def associate_by_email(backend, details, user=None, *args, **kwargs):
        # No user 
        if user:
            return None
    
        email = details.get('email')
        if email:
            # Try to associate accounts registered with the same email address,
            # only if it's a single object. AuthException is raised if multiple
            # objects are returned.
            users = list(backend.strategy.storage.user.get_users_by_email(email))
    
            #That's the line you want to add
            active_users = [user for user in users if user.is_active]
    
            if len(active_users) == 0:
                return None
            elif len(active_users) > 1:
                    raise AuthException(
                        backend,
                        'The given email address is associated with another account'
                    )
            else:
                return {'user': active_users[0],
                        'is_new': False}
    

    And then in your settings.py replace the line

    SOCIAL_AUTH_PIPELINE = (
        ...
        'social_core.pipeline.social_auth.associate_by_email',  # <--- enable this
        ...
    )
    

    with:

    SOCIAL_AUTH_PIPELINE = (
        ...
        'your_app.pipeline.associate_by_email',  # <--- enable this one
        ...
    )
    

    Sure position the pipeline partial where it has in place of original one. The original method does not perform the check, so to add this behavior you have to write your own partial that overrides this behavior. Assuming you have a Django app (here for the sake of simplicity called your_app - just update the names accordingly. The original code for partial associate_by_email can be found here.

    If you want to add this check to simple authentication, you can provide your own backend in a file your_app/backends.py:

    from django.core.exceptions import ObjectDoesNotExist
    from settings import settings
    from django.contrib.auth.models import User
    from django.contrib.auth.backends import ModelBackend
    
    class LocalActiveBackend(object):
        def authenticate(self, username=None, password=None):
            try:
                # Add extra check for is_active
                user = User.objects.get(username=username, is_active=True)
            except User.DoesNotExist:
                return None
    
            pwd_valid = user.check_password(password)
        
            if pwd_valid:
                return user
    
            return None
    
        def get_user(self, user_id):
            try:
                return User.objects.get(pk=user_id)
            except User.DoesNotExist:
                return None
    

    And then append that new backend to the list of available backends:

    AUTHENTICATION_BACKENDS = (
        'django.contrib.auth.backends.ModelBackend', # default
    
        ...
    
        'your_app.backends.LocalActiveBackend',
    )
    

    This will allow you using it with simple authentication by password and username if needed.