Django Social Auth Pipeline-- Checking for Duplicate Emails - python

So i'm piggybacking on this post here:
Python Social Auth duplicating e-mails for different users
Here's my issue:
I provide a user with ability to signup either via regular email sign up, facebook auth or twitter auth.
I'm also using same package Social Django Auth App for the user login pages.
I realized that a user might try sign up with a Facebook account (associated to one email) and then try again later to register with Twitter (which could have the same email address). Based on the aforementioned post, I added a function to check for duplicate emails like so:
def check_email_exists(request, 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
count = User.objects.filter(username=email).count()
success_message = messages.success(request, 'Sorry User With That Email Already 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 count:
return HttpResponseRedirect(reverse('accounts:sign_up', success_message))
and my pipeline with the function:
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',
'dealmazing.utils.check_email_exists',
'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',
)
UPON Testing--if i go to sign up with Twitter account of an already registered email address--it works. YAY!
BUT the main issue comes when i go to try to login via using either Facebook or Twitter. The function is checking on those logins as well and spitting me back the 'Email Allready Exists..' error.
So I somehow need to decipher between a login and a registration, but I'm having trouble finding how i actually do this with the social auth package.
Any ideas?

The difference between login and register is up to your project, it looks like in your scenario, a user landing in your pipeline function that matches an email in your DB should be considered like a login attempt and not a new singup. That's basically the functionality of associate_by_email method.
You might see the potential risk in this when a user uses a service that doesn't validate the email addresses, they can take control over somebody else account. You can mitigate this by doing validation on your end after any new social account is added, or by trusting services that are known to validate emails on their ends.

I would say, that you have to remove from the pipeline
'social_core.pipeline.social_auth.associate_by_email'
Reason: your application is not supporting unique user emails in User data model, and you are getting yourself into trouble. If you are not verifying user emails, then the trouble might be even bigger.

Related

Django/Django Rest How do I save user device to prevent tedious 2FA on every login?

Hello I have been working with Django Rest Framework with JWT as authentication framework and I successfully made Two factor authentication Login based on Email OTP but one thing I want to improve is I want to improve login and save user's device so that repeated 2FA(Two factor Authentcation) can be minimized?
here is certain instance of code I did for sending otp on user email.
serializers.py
class UserLoginSerializer(serializers.Serializer):
email = serializers.EmailField()
password = PasswordField()
views.py
class UserLoginView(generics.CreateWithMessageAPIView):
"""
Use this end-point to get login for user
"""
message = _('Please check your email for 6 digit OTP code.')
serializer_class = serializers.UserLoginSerializer
def perform_create(self, serializer):
usecases.UserLoginWithOTPUseCase(self.request, serializer=serializer).execute()
usecases.py
class UserLoginWithOTPUseCase(CreateUseCase, OTPMixin):
def __init__(self, request, serializer):
self._request = request
super().__init__(serializer)
def execute(self):
self._factory()
def _factory(self):
credentials = {
'username': self._data['email'],
'password': self._data['password']
}
self._user = authenticate(self._request, **credentials)
if self._user is not None:
"""
Sends email confirmation mail to the user's email
:return: None
"""
code = self._generate_totp(
user=self._user,
purpose='2FA',
interval=180
)
EmailVerificationEmail(
context={
'code': code,
'uuid': self._user.id
}
).send(to=[self._user.email])
else:
raise PermissionDenied(
{
'authentication_error': _('User name or password not matched')
}
)
I am confused how can I allow or save device to prevent repetitive 2FA.
TLDR;
At a very high level: tokenize the OTP (exchange the otp for a JWT).
Explanation
A JWT is nothing more than a JSON signed payload with some standardized fields exp (expiration), nbf (not before), etc.... The signature (symmetric or asymmetric) assures integrity, authenticity, and non-repudiation (eg the token has been issued by whom we think and has not been altered).
Within a JWT payload you can put anything (keeping in mind that the value is not encrypted) including data required for your application logic. You can also exchange different JWT during different authentication steps.
Some examples
Deferred 2FA
User gives you <username, password> if valid it receives a type_1_jwt if 2FA is enabled or else a type_2_jwt
User gives you <type_1_jwt, otp> if both are valid it receives a type_2_jwt
Your app accepts only type_2_jwt on all other endpoints
Here you have the flexibility to request the OTP after initial user authentication (or even if a suspicious device is detected). If the OTP is required user is not required to insert the password again but also the password is never stored anywhere nor re-submitted in any future request. The security level is kept the same as <username, password, otp> simultaneous authentication
Renew Token
User authenticates and receives a renew_jwt (expires after a long period or never)
User exchanges the renew_jwt for a auth_jwt (fast expiring)
Your app accepts only auth_jwt on all other endpoints
This solution allows for great flexibility if you want the authentication to be revoked or altered (altering roles). You can have auth_jwt that is used within your entire application but expires very fast, then you have a renew_jwt that has been already traded for proper authentication and can be used to automatically request an auth_jwt, this request will be met by the system only if certain criteria are met (for example authentication has not been revoked).
Mixing all together
User gives you <username, password, otp> it receives a auth_jwt (or a renew_jwt) and a otp_jwt.
User gives you <username, password, otp_jwt>
Your app accepts only auth_jwt on all other endpoints
After the first authentication the user device receives an otp_jwt (that has an arbitrarily long duration) and can be stored on the device (cookies, appStorage, secureStorage, ...). This otp_jwt can be used in any future moment (together with username and password) to authenticate again.
Conceptually the otp_jwt token is a secure proof the user has successfully authenticated with an OTP in the past
Assuming the device can be considered reasonably safe, such strategy does not significantly increase the risk of unauthorized access. But keep in mind that you should always give an option to the user to completely log out eg to clear this token too.
What you can do here is whenever you create a JWT token and set it on user's device for a domain, along with it create a unique key (just an identifier for this device) and set it on the user's device as a cookie for that domain. (save this unique key and user's id map in a db)
Keep the expiry for this cookie different from that of the JWT and do not clean this cookie even if the user logs out. (let it self expire)
So even if the user logs out of the device or jwt expires, in the next login request you will still receive the device cookie you set (lets say you set it for a month).
when a login request comes and has a device cookie with it, you can check if this user id and device key is registered with you. If yes you know that this user has logged in with this device in the last one month and you can skip 2FA, else if no such combination exists you can prompt for 2FA.
Additionally lets say the user is logging in for the first time, in that case the device cookie will be missing in the request and you can safely prompt for 2FA.
you can play around with this device cookie's checks like you can also save the timestamp for when this key was created and increase the level of checks.

How to implement single sign-on django auth in azure ad?

I have a django-based web application, a client requested that we integrate the login with Azure AD, following the documents I managed to integrate with the following flow.
In django the user types only the email, I identify the user and his company and redirect him to the microsoft ad login screen, after login, the redirect uri is activated and in my view I do some validations and authenticate the user on my system. The problem is that every time the customer is going to log in he needs to enter his credentials in azure, would it be possible with the microsoft user_id or the token acquired the first time the user logs in to login? Or another way to log in faster?
This my callback view, called in redirect_uri:
def callback(request):
user_id = request.session.pop('user_id', '')
user_internal = User.objects.filter(id=user_id).first()
company_azure = CompanyAzureAd.objects.filter(company=user_internal.employee.firm).first()
# Get the state saved in session
expected_state = request.session.pop('auth_state', '')
# Make the token request
url = request.build_absolute_uri(request.get_full_path())
token = get_token_from_code(url, expected_state, company_azure)
# Get the user's profile
user = get_user(token) #in this moment i have user microsoft profile, with token and id
# Save token and user
store_token(request, token)
store_user(request, user)
...
if it is possible to login I could store the token or user id in microsoft in my database, so it would only be necessary to login once
I think this is already answered here
Also try this
ADFS Authentication for Django
Even you can try the library in python
Django Microsoft Authentication Backend

Customize error messages on django-registration

I have a question about django-registration (https://bitbucket.org/ubernostrum/django-registration), but I can't find their issue tracker or a mailing list for it, so I'll try my luck here.
My application enables login via OpenID and login/password.
Some users "forget their password" on FS on try to reset it (here), but then they get the message:
The user account associated with this e-mail address cannot reset the password.
With no further explanations.
(You can try and reset my password - just type my email (tonylampada at gmail dot com) there to see the error message.
I want to customize that message. A better message would be:
The user account associated with this e-mail address cannot reset the password.
This happens because the user account was created with an OpenID or OAuth provider (tipically Google, Facebook, MyOpenID, etc).
To see the login provider(s) associated with this account, take a look at the user profile.
What is the easiest way to tell django-registration that?
Thanks!
PS: This issue on Github: https://github.com/freedomsponsors/www.freedomsponsors.org/issues/191 (just in case you're feeling like making a pull request today :-))
django-registration uses views from django.contrib.auth.
In this case: reset_password() github
Since this is no class based view, you can't override/inherit from it, but you can pass in a PasswordResetForm from django.contrib.auth.forms
from django.contrib.auth.forms import PasswordResetForm
class CustomResetForm(PasswordResetForm):
def validate(self, value):
#pseudocode
if user.cant_reset_pw:
raise ValidationError("The user account associated with this e-mail address cannot reset the password. and so forth..")
super(CustomResetForm, self).validate(value)
You'll have to wire things together by overriding the url r'^password/change/$' to point to a custom function that calls django.contrib.auth.passwort_reset() with your CustomResetForm.

How to update a user's `extra_data` after they have been associated with account?

I've successfully managed to use django-socialauth to associate an account (in this case, an instagram account) with an existing user account. I've also set up my pipeline to collect additional user details:
def update_social_auth(backend, details, response, social_user, uid, user,
*args, **kwargs):
if getattr(backend, 'name', None) in ('instagram', 'tumblr'):
social_user.extra_data['username'] = details.get('username')
social_user.save()
This works great when an account is associated for the first time. However, if the account has already been associated, the username field will not be present in extra_data.
How can I update a user's extra_data after the association has already been made? Is there a way using django-socialauth to do this without disconnecting and reconnecting, or using the account's (e.g Instagram's) API?
If it helps, this is my pipeline at the moment:
SOCIAL_AUTH_PIPELINE = (
'social_auth.backends.pipeline.social.social_auth_user',
'social_auth.backends.pipeline.social.associate_user',
'social_auth.backends.pipeline.social.load_extra_data',
'social_auth.backends.pipeline.user.update_user_details',
'apps.utils.social.utils.update_social_auth'
)
Here is a snippet of code I use to add 'admin' and 'staff' options to an existing Django user; I don't know about django-socialauth or the extra_data field, but I'm guessing something like this might be applicable:
:
userqueryset = User.objects.filter(username=user_name)
if not userqueryset:
print("User %s does not exist"%user_name, file=sys.stderr)
return am_errors.AM_USERNOTEXISTS
# Have all the details - now update the user in the Django user database
# see:
# https://docs.djangoproject.com/en/1.7/ref/contrib/auth/#django.contrib.auth.models.User
# https://docs.djangoproject.c om/en/1.7/ref/contrib/auth/#manager-methods
user = userqueryset[0]
user.is_staff = True
user.is_superuser = True
user.save()
:
FWIW, my app is using 3rd party authentication (specifically atm OpenId Connect via Google+), so I think there's some common goal here. In my case I want to be able to add Django admin privileges to a user that has already been created.
The full module containing the above code is at github.com/gklyne/annalist/blob/develop/src/annalist_root/annalist_manager/am_createuser.py#L231

GAE User registry without Google Accounts, want to restric to specific domain

I am planning on creating an application for the students of my school, and I want to restrict user registration to emails of the form person#myschool.edu. I would prefer to not manually create the user table and do the password hashing and such. Are there any libraries you can recommend for this?
Thanks for the help.
Sometimes, if you just send the user to a login screen you will end in a redirect loop if the user is already logged with a Google Account.
What i have found to be a good answer to this problem is to redirect the user to a log out page so he can later login with the domain you want.
I have used this for my code
user = users.get_current_user()
#Check if the user is in #mydomain.com
if user:
emailDomain = user.email().split("#")
if emailDomain[1] == "mydomain.com":
return True
else:
self.redirect(users.create_logout_url('/startPage'))
else:
self.redirect(users.create_login_url(self.request.uri))
This way, the application logs you out automatically and asks for your domain credentials
Since you said you don't know how the email are registered, that you don't want to manage a login/password database and you just need a regexp or somethings (I quote here!), I assume you could keep it very simple.
Something like.
user = users.get_current_user()
if user:
emailDomain = user.email().split("#")
if emailDomain == "yourschool.edu":
doSomething()
That way, all the trouble of registering to your app is given to the users (who will need to get a Google Account).

Categories