3
votes

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

2

2 Answers

7
votes

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.

5
votes

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.