Python Social Auth duplicating e-mails for different users
Asked Answered
H

2

5

In my website it is possible to login through:

  • username and password
  • e-mail and password
  • google auth 2
  • facebook

Where I am using django user built in system and python social auth.

The problem:

Suppose I create the following account:

username: losimonassi e-mail: [email protected]

Then when I try to login with my gmail ([email protected]) python social auth creates another user with the same e-mail. So, when I try to login using my e-mail the auth system finds two similar e-mails which raises an error.

I am trying to find a way that when the user try to login with gmail his e-mail is checked against the DB and if it already exists the process is stopped with a redirect and a alert message (I think it could be made through the middleware).

But of course the checking against the DB should only check against other backends users and normal users, to avoid blocking his own login.

I don't want to associate the accounts.

settings.py

SOCIAL_AUTH_PIPELINE = (
    'social.pipeline.social_auth.social_details',
    'social.pipeline.social_auth.social_uid',
    'social.pipeline.social_auth.auth_allowed',
    'social.pipeline.social_auth.social_user',
    'social.pipeline.user.get_username',
    'social.pipeline.user.create_user',
    #'social.pipeline.social_auth.associate_user',
    #'social.pipeline.social_auth.load_extra_data',
    'social.pipeline.user.user_details'
)

admin image

Hector answered 16/11, 2016 at 10:30 Comment(0)
C
6

I have encountered the situation you describe. Way I solved it: add custom step to social auth pipeline:

def check_email_exists(backend, details, uid, user=None, *args, **kwargs):
    email = details.get('email', '')
    provider = backend.name

    # check if social user exists to allow logging in (not sure if this is necessary)
    social = backend.strategy.storage.user.get_social_auth(provider, uid)
    # check if given email is in use
    exists = User.objects.filter(username=email).exists()

    # user is not logged in, social profile with given uid doesn't exist
    # and email is in use
    if not user and not social and exists:
        raise AuthException(backend)

Add your callable to the pipeline:

SOCIAL_AUTH_PIPELINE = (
    'social.pipeline.social_auth.social_details',
    'social.pipeline.social_auth.social_uid',
    'social.pipeline.social_auth.auth_allowed',
    'social.pipeline.social_auth.social_user',
    'social.pipeline.user.get_username',
    'path.to.module.check_email_exists',  # move if appropriate
    'social.pipeline.user.create_user',
    'social.pipeline.user.user_details'
)

Raising AuthException will redirect user to your settings.SOCIAL_AUTH_LOGIN_ERROR_URL. However, if that is not what you want, there also is another approach. Python-social-auth checks return value of any part of your pipeline. If return value is None, it just proceeds. If it is a dict, it will update kwargs using that dictionary so that the values are available later in the pipeline. However, if the return value is HttpResponse, say, HttpResponseRedirect, it will return that response to the user. So, instead of raising an AuthException, you can do

return HttpResponseRedirect(reverse('desired-endpoint'))

However, do take that with a grain of salt: the docs don't state this clearly and I have done this quite some time ago, so I might be mistaken.

Cartilage answered 16/11, 2016 at 11:48 Comment(2)
hey thanks for this! one question--how are you returning the error to the user? bascially i want to redirect to my sign in page, but with a message saying something like 'Sorry email already in use'.Croquet
There are two ways I can think up on he fly. First: with django.contrib.messages and SocialAuthExceptionMiddleware. Subclass SocialAuthException with appropriate message (override __str__) and raise it somewhere in the pipeline. SocialAuthExceptionMiddleware will automatically redirect to settings.LOGIN_ERROR_URL and add specified message. Second, as I've described in the answer, you can return HttpResponseRedirect anywhere in the pipeline, and the user will be directed to that page.Bratton
S
9

You can choose to associate users by their email addresses by adding a built-in, but by default not active step in the pipeline. Note that you should only do this if you are certain that the social provider verifies the email addresses. Otherwise I could sign up to a social provider using your email, login to your site and have access to your user on your site.

Python social auth docs and how to associate user by email: https://python-social-auth-docs.readthedocs.io/en/latest/use_cases.html?highlight=associate%20user#associate-users-by-email

From the above link:

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',
)

EDIT: As noted by Jack in the comments, the order of the pipeline steps are important.

Selectee answered 15/10, 2017 at 9:48 Comment(1)
Thanks, Important to note the order to have associate before create_user. That is what caught me out.Gonagle
C
6

I have encountered the situation you describe. Way I solved it: add custom step to social auth pipeline:

def check_email_exists(backend, details, uid, user=None, *args, **kwargs):
    email = details.get('email', '')
    provider = backend.name

    # check if social user exists to allow logging in (not sure if this is necessary)
    social = backend.strategy.storage.user.get_social_auth(provider, uid)
    # check if given email is in use
    exists = User.objects.filter(username=email).exists()

    # user is not logged in, social profile with given uid doesn't exist
    # and email is in use
    if not user and not social and exists:
        raise AuthException(backend)

Add your callable to the pipeline:

SOCIAL_AUTH_PIPELINE = (
    'social.pipeline.social_auth.social_details',
    'social.pipeline.social_auth.social_uid',
    'social.pipeline.social_auth.auth_allowed',
    'social.pipeline.social_auth.social_user',
    'social.pipeline.user.get_username',
    'path.to.module.check_email_exists',  # move if appropriate
    'social.pipeline.user.create_user',
    'social.pipeline.user.user_details'
)

Raising AuthException will redirect user to your settings.SOCIAL_AUTH_LOGIN_ERROR_URL. However, if that is not what you want, there also is another approach. Python-social-auth checks return value of any part of your pipeline. If return value is None, it just proceeds. If it is a dict, it will update kwargs using that dictionary so that the values are available later in the pipeline. However, if the return value is HttpResponse, say, HttpResponseRedirect, it will return that response to the user. So, instead of raising an AuthException, you can do

return HttpResponseRedirect(reverse('desired-endpoint'))

However, do take that with a grain of salt: the docs don't state this clearly and I have done this quite some time ago, so I might be mistaken.

Cartilage answered 16/11, 2016 at 11:48 Comment(2)
hey thanks for this! one question--how are you returning the error to the user? bascially i want to redirect to my sign in page, but with a message saying something like 'Sorry email already in use'.Croquet
There are two ways I can think up on he fly. First: with django.contrib.messages and SocialAuthExceptionMiddleware. Subclass SocialAuthException with appropriate message (override __str__) and raise it somewhere in the pipeline. SocialAuthExceptionMiddleware will automatically redirect to settings.LOGIN_ERROR_URL and add specified message. Second, as I've described in the answer, you can return HttpResponseRedirect anywhere in the pipeline, and the user will be directed to that page.Bratton

© 2022 - 2024 — McMap. All rights reserved.