django-allauth change user email with/without verification - django

I am using plain django-allauth without any social accounts. Every user should have exactly one email address associated with his account, i.e. the one that was used for registration/verification. I would like to enable my users to change this email.
So my first question is, should I have the new email being verified again by sending out the verifcation email? My gut feeling says, I better have this new email being verified. But I have no real arguments for that.
My second question is, if if want that to be verified, is that process somehow supported already with django-allauth? I have seen the EmailView and AddEmailForm. But those are based on the assumption that one account can have more than 1 email address (which is not what I want).
Thanks

I think the new email address should be verified. If your application sends periodic emails, you don't want to fire off loads of emails to just any email address that a user enters.
What I did was allow multiple email addresses until the second email is verified. Then, listen for django-allauth's email_confirmed signal as elssar suggested. As soon as the address is verified, set the new email address as the primary email, then delete any previous EmailAddess. Here's a simplified example of what I ended up doing:
models:
from django.dispatch import receiver
from allauth.account.models import EmailAddress
from allauth.account.signals import email_confirmed
class CustomUser(models.Model):
...
def add_email_address(self, request, new_email):
# Add a new email address for the user, and send email confirmation.
# Old email will remain the primary until the new one is confirmed.
return EmailAddress.objects.add_email(request, self.user, new_email, confirm=True)
#receiver(email_confirmed)
def update_user_email(sender, request, email_address, **kwargs):
# Once the email address is confirmed, make new email_address primary.
# This also sets user.email to the new email address.
# email_address is an instance of allauth.account.models.EmailAddress
email_address.set_as_primary()
# Get rid of old email addresses
stale_addresses = EmailAddress.objects.filter(
user=email_address.user).exclude(primary=True).delete()
views:
def update_user_details(request):
user = request.user
new_email = request.POST.get('new_email')
user.custom_user.add_email_address(request, new_email)
...

There are a few ways you could go about doing this.
Either listen to the email_confirmed signal handler, and have a function that checks whether user has two EmailAccount objects associated with his account, and if so, delete the other EmailAccount object.
The other would be set EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL and EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL in your settings and have related view delete the extra email address, if it exists.
Another way would be to just override the EmailView and/or AddEmailForm and have them do what you want.
For changing email without confirmation, you could just have your view call the EmailAddress.change method.

Related

Save avatar from facebook in allauth

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.

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.

sending activation emails-Django

I am creating an inactive user and want to send them email for activating there accounts like the one django-registration send when we create an account.
This is my views.py
user = User.objects.create_user(userName, userMail,userPass)
user.is_active=False
user.save()
You should review the topical guide on sending emails. Basically, you'll just use the components from django.core.mail to send an activation email with all the necessary information after you've created the user instance.
It's important that that email contains further information on how the user is supposed to activate their account. The way django-registration does it is that it has a separate model associated with the User instance that specified a unique identifier which would be used in the activation view to identify which user account is supposed to be activated, i.e. creating a GET request to http://foo/accounts/activate/550e8400-e29b-41d4-a716-446655440000 would activate the user account with the associated UUID.
There are some other intricate details that make django-registration a thorough and well-polished solution, in spite of being a bit dated (i.e. no class-based views), so I second #NCao in suggesting that you take enough time to review the sources from the official repository and ripoff and duplicate all the necessary bits.
Basically after a user signed up, you want to set user.is_active=False.
Then you send an URL with the user's information(for example, id) to the user's email.
When the user click the link, it will trigger an activation function. Within the activation function, it firstly extracts the user's information based on the URL (id). Then you can query the user object by calling user.objects.get(id=id). After that, you can set user.is_active=True and save user.
Here is the code to send email:
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
fromaddr='your email address' #(Gmail here)
username='your user name'
password='your password'
def send_email(toaddr,id):
text = "Hi!\nHow are you?\nHere is the link to activate your
account:\nhttp://127.0.0.1:8000/register_activate/activation/?id=%s" %(id)
part1 = MIMEText(text, 'plain')
msg = MIMEMultipart('alternative')
msg.attach(part1)
subject="Activate your account "
msg="""\From: %s\nTo: %s\nSubject: %s\n\n%s""" % (fromaddr,toaddr,subject,msg.as_string())
#Use gmail's smtp server to send email. However, you need to turn on the setting "lesssecureapps" following this link:
#https://www.google.com/settings/security/lesssecureapps
server = smtplib.SMTP('smtp.gmail.com:587')
server.ehlo()
server.starttls()
server.login(username,password)
server.sendmail(fromaddr,[toaddr],msg)
server.quit()
You may also want to check this out: https://github.com/JunyiJ/django-register-activate
Hope it helps!

Control a function (E.g send mail) from a users profile page

I have a profile page like so: http://i.stack.imgur.com/Rx4kg.png . In management I would like a option "Notify by mail" that would control my send_email functions in every application I want. As example I'm using django-messages and it sends private messages aswell as emails when you send a message. I would like for the user to be able to specify if he wants emails aswell when he gets a message.
messages/utils.py
def new_message_email(sender, instance, signal,
subject_prefix=_(u'New Message: %(subject)s'),
template_name="messages/new_message.html",
default_protocol=None,
*args, **kwargs):
"""
This function sends an email and is called via Django's signal framework.
Optional arguments:
``template_name``: the template to use
``subject_prefix``: prefix for the email subject.
``default_protocol``: default protocol in site URL passed to template
"""
if default_protocol is None:
default_protocol = getattr(settings, 'DEFAULT_HTTP_PROTOCOL', 'http')
if 'created' in kwargs and kwargs['created']:
try:
current_domain = Site.objects.get_current().domain
subject = subject_prefix % {'subject': instance.subject}
message = render_to_string(template_name, {
'site_url': '%s://%s' % (default_protocol, current_domain),
'message': instance,
})
if instance.recipient.email != "":
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL,
[instance.recipient.email,])
except Exception, e:
#print e
pass #fail silently
Apparently instance.recipient.email is the email for the recipient user. So my questions are: How do I go about creating an option in my profile management that can be used in my new_message_email to check if the user wants emails or not? My own thoughts are that I need to save a value in the database for the user and then check for that value in new_message_email function. How I do that isn't clear though. Do I create a new function in my userprofile/views.py and class in userprofile/forms.py? And have my userprofile/overview.html template change them? Some specifics and thoughts if this is the right approach would help alot!
You probably want to start off by creating a user profile so that you have a good way to store weather or not the user wants these emails sent to them. This is done using the AUTH_PROFILE_MODULE setting in your settings.py.
Once you have the data stored, you should be able to access it from instance.recipient (assuming that instance.recipient is a User object). So you could change your code to:
if instance.recipient.get_profile().wants_emails and instance.recipient.email != "":
send_mail(subject, message, settings.DEFAULT_FROM_EMAIL,
[instance.recipient.email,])
Done and done.

How can I detect multiple logins into a Django web application from different locations?

I want to only allow one authenticated session at a time for an individual login in my Django application. So if a user is logged into the webpage on a given IP address, and those same user credentials are used to login from a different IP address I want to do something (either logout the first user or deny access to the second user.)
Not sure if this is still needed but thought I would share my solution:
1) Install django-tracking (thankyou for that tip Van Gale Google Maps + GeoIP is amazing!)
2) Add this middleware:
from django.contrib.sessions.models import Session
from tracking.models import Visitor
from datetime import datetime
class UserRestrictMiddleware(object):
"""
Prevents more than one user logging in at once from two different IPs
"""
def process_request(self, request):
ip_address = request.META.get('REMOTE_ADDR','')
try:
last_login = request.user.last_login
except:
last_login = 0
if unicode(last_login)==unicode(datetime.now())[:19]:
previous_visitors = Visitor.objects.filter(user=request.user).exclude(ip_address=ip_address)
for visitor in previous_visitors:
Session.objects.filter(session_key=visitor.session_key).delete()
visitor.user = None
visitor.save()
3) Make sure it goes after the VisitorTrackingMiddleware and you should find previous logins are automatically bumped when someone new logs in :)
If you're already using django-tracking as suggested here, there's a much easier way to implement this:
Define a signal handler:
# myapp/signals.py
def kick_my_other_sessions(sender, request=None, user=None, **kwargs):
from tracking.models import Visitor
from django.contrib.sessions.models import Session
keys = [v.session_key for v in Visitor.objects.filter(user=request.user).exclude(session_key=request.session.session_key)]
Session.objects.filter(session_key__in=keys).delete()
Create a listener for the user_logged_in signal:
# myapp/__init__.py
from myapp.signals import kick_my_other_sessions
from django.contrib.auth.signals import user_logged_in
user_logged_in.connect(kick_my_other_sessions, sender=User)
This will institute a sort of "last user to login wins" system. If you want to allow multiple logins by the same user from the same ip, you can add an .exclude() to the Visitors lookup.
Django's middleware will probably help you achieve this. The issue is that you will probably want to allow multiple anonymous sessions from the same IP address, even authenticated sessions for different users, but not authenticated sessions for the same user.
You'll want to:
Create a user profile model to store the IP address of a user's last login. See Django's Storing additional information about users documentation.
Implement a custom authentication backend. This backend, when triggered and successfully authenticating a user (just call super) would wipe out the user's last login IP in the profile model.
Implement a subclass of Django's django.contrib.sessions.SessionMiddleware class. Implement process_request. If the request.user object's profile model has no IP address, set it and allow the request. If it has an IP, and the IP is different from the current request's IP (request.META.REMOTE_ADDR), then do whatever you like to either log out the other user, or return an error to the requestor.
Update your settings.py file so that your custom auth backend is processed first, and so that your custom session middleware is also processed first. This involves updating settings.AUTHENTICATION_BACKENDS and settings.MIDDLEWARE_CLASSES.
You'll need to do this with custom middleware.
In your middleware process_request() method you will have access to the request object so you can do something like the following:
session_key = request.session.session_key
ip_address = request.META.get('REMOTE_ADDR', '')
Now you know the IP address, so check a model you create that (roughly) would look like this:
class SessionIPS(models.Model):
session = models.ForeignKey(Session)
IP = models.CharField(max_length=20)
So when a session is created or deleted you will modifiy your session ip's table accordingly, and when a request comes in make sure the IP address isn't being used for another session. If if is, then return a Http404 (or something like it) from the middleware.
A pluggable app that can show you a lot more detail (and even includes IP address in its own model) is django-tracking.