Save avatar from facebook in allauth - django

There are questions here which answer this, but my case is different.
Instead of letting allauth create a new user I'm catching wheter or not the email exists and performing a login using
user = User.objects.get(email=email)
sociallogin.connect(request, user)
social_account_added.send(
sender=SocialLogin,
request=request,
sociallogin=sociallogin,
)
But, I can't set the avatar here due to the way my conditions are setup and this may never be hit.
The alternative is in get_redirect_url from what I see, but this isn't called if I use sociallogin.get_redirect_url so it seems theres opportunity for both to be skipped.
There's a signal user_logged_in = Signal(providing_args=["request", "user"]) under account app in allauth but what if they have multiple socials connected? How would I determine the proper avatar to get...

Is your question:
how to capture the avatar at signup?
how to choose which avatar if there are multiple social accounts?
how to hook the signup and instead connect the potential new user to an existing accout?
I'll address all three.
Capturing avatar at signup
My approach to this is receive the user_signed_up signal. I also extract the user name (if available) at this time.
This is the function I use:
#receiver(user_signed_up)
def set_initial_user_names(request, user, sociallogin=None, **kwargs):
"""
When a social account is created successfully and this signal is received,
django-allauth passes in the sociallogin param, giving access to metadata on the remote account, e.g.:
sociallogin.account.provider # e.g. 'twitter'
sociallogin.account.get_avatar_url()
sociallogin.account.get_profile_url()
sociallogin.account.extra_data['screen_name']
See the socialaccount_socialaccount table for more in the 'extra_data' field.
From http://birdhouse.org/blog/2013/12/03/django-allauth-retrieve-firstlast-names-from-fb-twitter-google/comment-page-1/
"""
preferred_avatar_size_pixels = 256
picture_url = "http://www.gravatar.com/avatar/{0}?s={1}".format(
hashlib.md5(user.email.encode('UTF-8')).hexdigest(),
preferred_avatar_size_pixels
)
if sociallogin:
# Extract first / last names from social nets and store on User record
if sociallogin.account.provider == 'twitter':
name = sociallogin.account.extra_data['name']
user.first_name = name.split()[0]
user.last_name = name.split()[1]
if sociallogin.account.provider == 'facebook':
user.first_name = sociallogin.account.extra_data['first_name']
user.last_name = sociallogin.account.extra_data['last_name']
# verified = sociallogin.account.extra_data['verified']
picture_url = "http://graph.facebook.com/{0}/picture?width={1}&height={1}".format(
sociallogin.account.uid, preferred_avatar_size_pixels)
if sociallogin.account.provider == 'google':
user.first_name = sociallogin.account.extra_data['given_name']
user.last_name = sociallogin.account.extra_data['family_name']
# verified = sociallogin.account.extra_data['verified_email']
picture_url = sociallogin.account.extra_data['picture']
profile = UserProfile(user=user, avatar_url=picture_url)
profile.save()
user.guess_display_name()
user.save()
That's from a Django-allauth starter example I wrote
When the user signs in that function is called. It grabs the user's name and avatar data if it can.
Choosing which avatar if there are multiple accounts
My view is that this is a user choice thing. A user can't sign up with multiple social providers in the same instant.
There is always a sequence, e.g. Google first then they connect Facebook.
As such, my view is the system uses the avatar from the first social provider. The allauth UI lets the user add
other social providers once they've set up the initial one (try the deo-allauth-bootrap above and you'll see).
If the user adds another social network, add some UI to choose that avatar when they want to.
Hook signup to connect existing account
This would take some experimentation but overall I feel like this is not solving a real problem.
The built-in allauth UI allows the user (once registered) to add existing social providers. That's the correct
way to do it and it works out-of-the-box.
If the user signs up with another social provider then it's arguably either a mistake or they want two
separate accounts.
Granted, this needs some experimentation with users to see what is the most intuitive experience.
It could be, for example, that the site notes that the user has signed in with Google before and shows the Google
button slightly differently or "sign in again with Google", so the user doesn't accidentally sign up with a
different social account.

Related

Email conflict while login by different socials

I have python-django backend, that allows u to sign in through fb, apple, email, google. My email field is unique, so I can't have more than one user with single email.
When user sign in with socials I take his email and create new user.
Problem is, if u have two socials with single email, u can't use both of them to sign in. It works like:
We have Facebook and appleId with same email
Sign in with apple -> I create user with appleId, name, email -> user press logout -> user press sign in with Facebook -> I can't create new user because I have that email in db already.
So the question is, what should I do and where I can find examples of it.
Details: I have custom Django User and I have to take email in any case. I can't use Django-social.
I think on the last step I should give user profile, that was made in second step, but I don't know how to google this problem and how its done common practice
when someone logs in through FB or Google and its email not present in the local account, It creates a new social account. If the email present in local accounts matches with google or Facebook accounts while logging in through this, it only authenticates it (no need to put local account password). I also saved few things into the User during #receiver(user_signed_up).
This code solved the conflict between same google and Facebook using the same email id
I did not use verification, you can use it if you want
class MyAppSocialAccountAdapter(DefaultSocialAccountAdapter):
# login(request, user) Before we did this
#transaction.atomic
def pre_social_login(self, request, sociallogin):
# social account already exists, so no need to do anything Auto login will happen
if sociallogin.is_existing:
return
# some social logins don't have an email address, e.g. facebook accounts
# with mobile numbers only, but allauth takes care of this case so just
# ignore it
if 'email' not in sociallogin.account.extra_data:
return
# find the first verified email that we get from this sociallogin
# verified_email = None
# for email in sociallogin.email_addresses:
# if email.verified:
# verified_email = email
# break
try:
user = User.objects.get(email=sociallogin.email_addresses[0])
# This user now can be authenticated without password through google or facebook
sociallogin.connect(request, user)
raise ImmediateHttpResponse(redirect('logout_process')) # send it back to login
except Exception as e:
print(e)
# if social account does not exist, it creates one by default

register user by email or mobile number in django

i have custom user model with email as unique identifier for auth instead of username. i want register a user by email or mobile number. if user enter email address, then register user by activation link, and if user enter phone number then register by SMS OTP.
something like instagram registration:
https://www.instagram.com/accounts/emailsignup/
i found this topic but the answer is not explained well.
A username is required for a User object. But, you can make that username their email too, so it's not a problem (at least in django 2.x, not sure about 1.x). You didn't describe what the app you're making was for, it's purpose, etc., so before you skip to the code, read the following warnings and thought process of why I am giving you a better option.
It's a very bad idea to force the person to have the username equal to their email, because in the future you might want to add some other functionality.
For example: Maybe you'd want to make a message board so people can talk to each other. But because of bad planning from the beginning, everyone would see each other by their email. Technically, you could asign them all a bunch of random usernames they didn't make up, but that's not a good idea cause they're less likely to remember it, and they might not like it. The only good use of a name like sPaRkLe_DaNcEr12 or Poothtaste, is for making people rage quit in video games.
So if you wanted a future ability for users to talk to each other, it would be better to only show their username to other users, but ALLOW people to log in with their email or phone number if they wanted to. This way, now they can login with their (username) or (email) or (phone_number), and they only have to remember one of those. I will show you how soon.
Some downsides of this: It makes more queries to your database, which can make it slower if you have tons of users, but that's your call. Personally, I say it's worth it because it's negligible, and easier for users, and it's them you should cater to. So pay for a faster server, or no??? Ultimately, you always ought to design around having as few queries as possible, while caching certain pages that are heavy on the database so it doesn't have to do the same thing X number of times.
Let's begin:
Remember that the following is an example of what I would do for django 2.x, with a better functionality than you're asking for. If you're using 1.x, just use url() instead of path(), and any other requirements.
Assume we have an app called accounts_app.
Also assume that we put path('accounts/', include('accounts_app.urls')), inside our project level urls.py.
I'm also going to assume you know how to use templates... Now, create a urls.py inside that app:
accounts_app/urls.py:
app_name = 'accounts_app'
urlpatterns = [
...
path('signup/', views.signup, name='signup'),
]
accounts_app/models.py:
The user attribute below enables you to extend the User model, so you can have their phone_number too. In this example, I allowed that be blank in case they don't want to give it. But if they did, you would have make a separate view for it. To make this whole thing much more simple, I'm not going to include that, nor tell you how to overwrite the whole User model. I'm only going to show you how to log in with an email with the regular User model. After that, doing so with a phone number shouldn't be hard at all. A reminder on what you will need is at the very end of this post.
class ExtendedUserExample(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
phone_number = models.IntegerField(blank=True)
settings.py.... NOTE THAT THIS IS IN THE PROJECT LEVEL FOLDER
The order of these backends matters. Always do ModelBackend first, or it will break.
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'accounts_app.authentication.EmailAuthBackend', # to be able to login with email, described next
]
accounts_app/authentication.py:
This is an example to be able to login by their email. If you wanted someone to login by their phone number too, the principle is the same, but in this file, you would also need to import the ExtendedUserExample model above, add that to the above settings AUTHENTICATION_BACKENDS at the bottom of it, and make a new class for PhoneAuthBackend, that searches ExtendedUserExample for the phone_number. Again, to keep this more simple, I am not completely overwriting the User model, rather I only extend it, so if a user made an account and wanted to login by phone number later on, they would have to sign up with a username and email first, and once in, they could add a phone number (with this example, you will need another view for that).
So try this email example first until you get the hang of it. You also don't need to import this file anywhere else because the settings.py file takes care of it.
Here's what's happening: On your template for logging in, it will first search for the username field inside your User model, because your settings.py file has the AUTHENTICATION_BACKENDS variable to check the ModelBackend first.
But let's say a user entered their username as aaa#aaa.com. Now since you didn't allow anyone to sign up with an email as their username, when aaa#aaa.com is not found as a username in the User model, your settings file now says to go check the same User object a second time, but search their input by the email field / column instead. If their input exists in that column, authenticate() logs them in by their email if the password is right.
from django.contrib.auth.models import User
class EmailAuthBackend(object):
""" Authenticate using an email address """
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.get(email=username) # gets the email by the 'username' they entered
if user.check_password(password):
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
acounts_app/views.py:
I'll assume you know how to make django forms. If not, check here for model forms.
from .forms import ExtendedUserForm
def signup(request):
form = ExtendedUserForm(request.POST or None)
if request.method == 'POST':
if request.POST['password1'] == request.POST['password2']:
potential_user = request.POST.get('username', False).lower()
try:
user = User.objects.get(username=potential_user)
return render(request, 'accounts_app/signup.html', {'error': 'Username has already been taken. Please try another'})
except User.DoesNotExist:
if form.is_valid():
new_user = User.objects.create_user(username=potential_user, email=request.POST['email'], password=request.POST['password1'])
# backend argument required cause we are making the ability to LOGIN by email.
# Remember, I only extended the User model.
auth.login(request, new_user, backend='django.contrib.auth.backends.ModelBackend')
return redirect('some_app:some_view')
else:
return render(request, 'accounts_app/signup.html', {'error': "Password's must match."})
return render(request, 'accounts_app/signup.html', {'form': form})
Now that your login by email should be working, it's not that hard to follow these principles to create the same ability for logging in by phone. If you continue with this example, you will need to make a new view to save a phone number to ExtendedUserExample.phone_number. After that, add another line at the bottom of AUTHENTICATION_BACKENDS, write a new class in authentication.py, and you'd be set... So long as you have <input type="text" name="username" required> when they use your login view.

django-allauth Not Saving Social Info

So I'll give full disclosure from the get-go that I am quite new to both Django and django-allauth.
Now that that is out of the way, the problem that I am having is that when a user logs in via a social site, (I have been trying Google and Facebook), none of the data retrieved from the site is pulled into the user's data fields. After authenticating, the user is still prompted to enter an email, and all name fields are left blank. I tried to fix this manually by creating a custom adapter, but that did not work either. From using print statements, I can see that the data is being fetched from the site just fine -- it just isn't being saved to the user's attributes.
Correct me if I'm wrong, but by reading the documentation and the some of the source of django-allauth, I am under the impression that social authorization automatically saves the user's email and first and last names via the populate_user(self, request, sociallogin, data): hook in the DefaultSocialAccountAdapter class, so I really shouldn't even have to deal with workarounds.
Thus, I'm guessing that I am just doing something foolish that is messing this up for me... Although if there is a clever workaround that will fix this problem, I'd be fine with that, for lack for a better solution.
Note: Using Django 1.7 and Python 3.4.1
EDIT: Django-allauth is succeeding in creating a User and linking the user to a social account, which contains all of the data fetched from the social site, but none of that data is populating the fields within the User object, like email, first_name, and last_name.
Here are my django-allauth configuration settings in settings.py:
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "required"
ACCOUNT_USERNAME_REQUIRED = False
SOCIALACCOUNT_AUTO_SIGNUP = True
# The following line was uncommented when I was trying to use my own adapter
# SOCIALACCOUNT_ADAPTER = 'profiles.profile_adapter.ProfileAdapter'
SOCIALACCOUNT_PROVIDERS = {
'facebook':
{ 'SCOPE': ['email'],
'AUTH_PARAMS': {'auth_type': 'reauthenticate'},
'METHOD': 'oauth2',
'LOCALE_FUNC': lambda request: 'en_US'},
'google':
{ 'SCOPE': ['https://www.googleapis.com/auth/userinfo.profile'],
'AUTH_PARAMS': { 'access_type': 'online' } },
}
And here is the code I had in my custom adapter (Which, by using print statements, I could tell was getting used and processing the correct data) where I tried to manually save the fields into the user object
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class ProfileAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
'''
Check for extra user data and save the desired fields.
'''
data = sociallogin.account.extra_data
user = sociallogin.account.user
print("LOGS: Caught the signal -> Printing extra data of the account: \n" + str(data))
if 'first_name' in data:
user.first_name = data['first_name']
elif 'given_name' in data:
user.first_name = data['given_name']
if 'last_name' in data:
user.last_name = data['last_name']
elif 'family_name' in data:
user.last_name = data['family_name']
user.save()
Note The above code creates a user in the database that is not linked to any social account, but contains the correct first and last names. Then the user is redirected to a form saying they are logging in with a social account and is prompted for an email address. Once this form is submitted, the original user created is overwritten by a new user that is linked to a social account, contains the email entered into the form, but does not have first or last name fields populated.
The problem was that when an email was not included with the data fetched from the social media site, django-allauth would ask for an email in a subsequent form to create the account with. When the account is then created from this form, django-allauth would not use the data fetched from the social media to populate fields. I think that this is a problem with django-allauth.

Django allauth social login: automatically linking social site profiles using the registered email

I aim to create the easiest login experience possible for the users of my Django site. I imagine something like:
Login screen is presented to user
User selects to login with Facebook or Google
User enter password in external site
User can interact with my site as an authenticated user
Ok, this part is easy, just have to install django-allauth and configure it.
But I also want to give the option to use the site with a local user. It would have another step:
Login screen is presented to user
User selects to register
User enter credentials
Site sends a verification email
User clicks in email link and can interact with my site as an authenticated user
Ok, both the default authentication and allauth can do it. But now is the million dollars question.
If they change how they do the login, how do I automatically associate their Google, FB and local accounts?
See that any way they login, I have their email address. Is it possible to do it using django-allauth? I know I can do it with user intervention. Today the default behavior is to refuse the login saying that the email is already registered.
If it isn't possible to do just with configuration, I'll accept the answer that gives me some orientation about which modifications should I make in allauth code to support this workflow.
There are a lot of reasons to do this. The users will forget which method they used to authenticate, and will sometimes use Google, sometimes FB and sometimes the local user account. We already have a lot of local user accounts and social accounts will be a new feature. I want the users to maintain their identity. I envision the possibility to ask for the user friends list, so if they logged using Google, I'd like to also have their FB account.
It is a hobby site, there isn't great security requirements, so please don't answer that this isn't a wise security implementation.
Later, I'd create a custom user model to have just the email as the login id. But I'll be happy with an answer that just let me automatically associate a accounts of the default user model that has a required username.
I'm using Django==1.5.4 and django-allauth==0.13.0
Note (2018-10-23): I'm not using this anymore. Too much magic happening. Instead I enabled SOCIALACCOUNT_EMAIL_REQUIRED and 'facebook': { 'VERIFIED_EMAIL': False, ... }. So allauth will redirect social logins on a social signup form to enter a valid email address. If it's already registered an error shows up to login first and then connect the account. Fair enough for me atm.
I'm trying to improve this kind of use case and came up with the following solution:
from allauth.account.models import EmailAddress
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class SocialAccountAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
"""
Invoked just after a user successfully authenticates via a
social provider, but before the login is actually processed
(and before the pre_social_login signal is emitted).
We're trying to solve different use cases:
- social account already exists, just go on
- social account has no email or email is unknown, just go on
- social account's email exists, link social account to existing user
"""
# Ignore existing social accounts, just do this stuff for new ones
if sociallogin.is_existing:
return
# some social logins don't have an email address, e.g. facebook accounts
# with mobile numbers only, but allauth takes care of this case so just
# ignore it
if 'email' not in sociallogin.account.extra_data:
return
# check if given email address already exists.
# Note: __iexact is used to ignore cases
try:
email = sociallogin.account.extra_data['email'].lower()
email_address = EmailAddress.objects.get(email__iexact=email)
# if it does not, let allauth take care of this new social account
except EmailAddress.DoesNotExist:
return
# if it does, connect this new social login to the existing user
user = email_address.user
sociallogin.connect(request, user)
As far as I can test it, it seems to work well. But inputs and suggestions are very welcome!
You will need to override the sociallogin adapter, specifically, the pre_social_login method, which is called after authentication with the social provider, but before this login is processed by allauth.
In my_adapter.py, do something like this
from django.contrib.auth.models import User
from allauth.account.models import EmailAccount
from allauth.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class MyAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
# This isn't tested, but should work
try:
user = User.objects.get(email=sociallogin.email)
sociallogin.connect(request, user)
# Create a response object
raise ImmediateHttpResponse(response)
except User.DoesNotExist:
pass
And in your settings, change the social adapter to your adapter
SOCIALACCOUNT_ADAPTER = 'myapp.my_adapter.MyAdapter`
And you should be able to connect multiple social accounts to one user this way.
As per babus comment on this related thread, the proposed answers posted before this one (1, 2) introduce a big security hole, documented in allauth docs:
"It is not clear from the Facebook documentation whether or not the fact that the account is verified implies that the e-mail address is verified as well. For example, verification could also be done by phone or credit card. To be on the safe side, the default is to treat e-mail addresses from Facebook as unverified."
Saying so, I can signup in facebook with your email ID or change my email to yours in facebook and login to the website to get access to your account.
So taking this into consideration, and building on #sspross answer, my approach is to redirect the user to the login page, and notify her/him of the duplicate, and inviting him to log in with her/his other account, and link them once they are logged in. I acknowledge that differs from the original question, but in doing so, no security hole is introduced.
Thus, my adapter looks like:
from django.contrib.auth.models import User
from allauth.account.models import EmailAddress
from allauth.exceptions import ImmediateHttpResponse
from django.shortcuts import redirect
from django.contrib import messages
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
class MyAdapter(DefaultSocialAccountAdapter):
def pre_social_login(self, request, sociallogin):
"""
Invoked just after a user successfully authenticates via a
social provider, but before the login is actually processed
(and before the pre_social_login signal is emitted).
We're trying to solve different use cases:
- social account already exists, just go on
- social account has no email or email is unknown, just go on
- social account's email exists, link social account to existing user
"""
# Ignore existing social accounts, just do this stuff for new ones
if sociallogin.is_existing:
return
# some social logins don't have an email address, e.g. facebook accounts
# with mobile numbers only, but allauth takes care of this case so just
# ignore it
if 'email' not in sociallogin.account.extra_data:
return
# check if given email address already exists.
# Note: __iexact is used to ignore cases
try:
email = sociallogin.account.extra_data['email'].lower()
email_address = EmailAddress.objects.get(email__iexact=email)
# if it does not, let allauth take care of this new social account
except EmailAddress.DoesNotExist:
return
# if it does, bounce back to the login page
account = User.objects.get(email=email).socialaccount_set.first()
messages.error(request, "A "+account.provider.capitalize()+" account already exists associated to "+email_address.email+". Log in with that instead, and connect your "+sociallogin.account.provider.capitalize()+" account through your profile page to link them together.")
raise ImmediateHttpResponse(redirect('/accounts/login'))
I've just found this comment in the source code:
if account_settings.UNIQUE_EMAIL:
if email_address_exists(email):
# Oops, another user already has this address. We
# cannot simply connect this social account to the
# existing user. Reason is that the email adress may
# not be verified, meaning, the user may be a hacker
# that has added your email address to his account in
# the hope that you fall in his trap. We cannot check
# on 'email_address.verified' either, because
# 'email_address' is not guaranteed to be verified.
so, it is impossible to do by design.
If they change how they do the login, how do I automatically associate their Google, FB and local accounts?
It is possible, but you have to be careful about security issues. Check scenario:
User create account via email and password on your site. User does not have Facebook.
Attacker creates account on Facebook with user email. (Hypothetic scenario, but you do not control if social network verify email).
Attacker login to your site with Facebook and automatically get access to user original account.
But you can fix it. I describe solution to ticket https://github.com/pennersr/django-allauth/issues/1149
Happy scenario should be:
User create account via email and password on your site. User logged out.
User forget about his account and try to login via his Facebook.
System authenticate user via Facebook and find out, he already created account via other method (emails are same). System redirect user to normal login page with message "You already create your account using the email and password. Please log in this way. After you log in, you will be able to use and login using Facebook."
User login via email and password.
System automatically connect his Facebook login with his account. Next time user can use Facebook login or email and password.

Some user attributes not showing up in django admin

In my custom authentication backend I extract the username, email, first and last name from an LDAP response and try to stick them into a newly generated User object if the user doesn't yet exist:
user = User(username=username, email=result[0][1].get('mail')[0], first_name=result[0][1].get('givenName')[0], last_name=result[0][1].get('sn')[0])
user.save()
And another variant I tried:
user = User.objects.create_user(username, result[0][1].get('mail')[0])
user.first_name = result[0][1].get('givenName')[0]
user.last_name = result[0][1].get('sn')[0]
user.save()
While the username and email show up in the admin after the user's initial successful authentication attempt I can't get the first and last name to display. Logging the values from the LDAP response shows that these exist.
Any idea what's going wrong here?
Ok, it was indeed my own stupidity: should not only have restarted the frontend webserver but also uWSGI! I could add to my defense that these are my baby steps with uWSGI...