So I had the following issue today with a project:
def has_verified_email(self):
return self.emailaddress_set.filter(verified=True).exists()
Thus, the ?next=/some_url/ should be added to the Email confirmation link, so that the user does not need to click on the same button again.
I solved the Issue in the following way:
# settings.py
ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'optional'
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_ADAPTER = 'path.to.adapters.AccountAdapter'
# path.to.adapters.AccountAdapter
class AccountAdapter(DefaultAccountAdapter):
def is_open_for_signup(self, request):
return getattr(settings, 'ACCOUNT_ALLOW_REGISTRATION', True)
def render_mail(self, template_prefix, email, context):
context['next_url'] = '?next=%s' % self.request.path # <-- Append path to email context
return super(AccountAdapter, self).render_mail(template_prefix, email, context)
def get_email_confirmation_redirect_url(self, request):
"""
The URL to return to after successful e-mail confirmation.
"""
if request.user.is_authenticated():
if app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL:
return \
app_settings.EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL
elif request.GET.get('next'): # <-- Check if request has 'next' parameter
return request.GET.get('next') # <-- Return the next parameter instead
else:
return self.get_login_redirect_url(request)
else:
return app_settings.EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL
# templates/account/email/email_confirmation_message.txt
...
To confirm this is correct, go to {{ activate_url }}{{ next_url }}
...
So this works propery. If a user wants to access, let's say, /dashboard/, but email verification is enabled for this URL, then he will have the URL example.com/dashboard/, but will see the Verify Your E-mail Address page. /dashboard/ will be added to the email context['next_url']. If the user now clicks on the activation link, he will be automatically logged in and redirected to the original URL he tried to access without a verified email.
It would be amazing, if you can implement this generically. It would be nice, if the default adapter recognizes, if there is a ?next parameter for the signup page or if a user tried to access a page that required email verification. Once that is recognized, it should take higher priority in redirection. If nothing was provided, then the adapter can use the regular logic for redirection.
Maybe there is a cleaner solution to this. Any feedback is welcomed.
Let me know what you think!
I am also trying to implement this and I came pretty much to the equivalent code to yours but in my case the link in the email is always ?next=/accounts/signup/. For some reason, my adapter's get_email_confirmation_redirect_url() method is not being called at all.
As a side note, be careful with ACCOUNT_CONFIRM_EMAIL_ON_GET = True - this means anyone with the confirmation link can login as the user who received the link, over and over again until you delete the email confirmation record from the database.
A better option is to change the confirmation page template to not have a confirmation button and instead do a POST via AJAX (simulating the user clicking the "confirm" button) and then redirecting to wherever the server responds.
Nevermind, I misunderstood the order in which the adapter methods were called. get_email_confirmation_redirect_url() isn't called until the user clicks the confirmation button on the confirmation page (or gets automatically confirmed just by visiting the page, depending on the setting I mentioned in my previous comment).
I was assuming this method was called to generate the URL that gets sent in the email, which, now that I think of it, doesn't even make much sense.
And an important difference in our use cases that I forgot to mention: I'm trying to achieve the redirection when the user confirms his email after sign up, which means that setting context['next_url'] = '?next=%s' % self.request.path will always wiled /accounts/signup as the next url.
That's what you get for programming when you're sleepy.
The side note from my previous comment is still valid, though.
@borfast Your sidenote is true, I read about it. But I was thinking, if someone has access your email account to retrieve the link, then being able to login to the portal i am programming is of little concern :P. Correct me if I am wrong.
Plus, I am hoping that the people who develop all-auth implement this feature soon, so I don't have to use a hacky solution. Till then, the above code works for that use case.
Well, I agree that if someone has access to your email then you have another problem to worry about. However, the one I mentioned is still a problem, because now the attacker has access to another account of yours, and depending on what that account grants access to, it can be a major headache.
In other words, the fact that someone was able to intercept the link from the user is no excuse for us programmers to make that user's life even harder by allowing access to his account on our application.
There's another subtle detail here: note that I never said an attacker needs to gain access to the user's email account in order to obtain the link. There are other ways to do that, including sniffing unencrypted traffic to your application, via social engineering, by taking a photo of the user's screen when they're looking at the account confirmation email... use your imagination and take your pick.
The assumption that if an attacker has intercepted the confirmation URL then he also has access to the user's email account, and thus the user has bigger concerns, is not always correct, and that makes it even less justifiable for us programmers to make the attacker's life easier.
I also hope something like this gets implemented but there's a problem with the approach we both took (I mentioned it previously): if the user is signing up, this code will always take the user to /accounts/signup. I ended up using session variables to identify where the user was going, which is also not perfect but at least works for my use case.
Yeah you're right. That's why I posted my solution here in the hopes, that the original developers of all-auth will implement such a feature in a solid way. I hope they looked at this.
Can you post your solution? Maybe I can switch to that instead, if it is safer.
I'm afraid I no longer have access to the code, it was a private repository of a project I was brought in to help for a short period and I'm no longer associated with it.
from allauth.account.adapter import DefaultAccountAdapter
class CustomAccountAdapter(DefaultAccountAdapter):
def get_email_confirmation_url(self, request, emailconfirmation):
next_url = request.POST.get('next')
email_conf_url = super(CustomAccountAdapter, self).get_email_confirmation_url(request, emailconfirmation)
if next_url:
return '%s?next=%s' % (email_conf_url, next_url)
else:
return email_conf_url
def get_email_confirmation_redirect_url(self, request):
next_url = request.GET.get('next')
if next_url:
return next_url
else:
return super(CustomAccountAdapter, self).get_email_confirmation_redirect_url(request)
from allauth.account.adapter import DefaultAccountAdapter class CustomAccountAdapter(DefaultAccountAdapter): def get_email_confirmation_url(self, request, emailconfirmation): next_url = request.POST.get('next') email_conf_url = super(CustomAccountAdapter, self).get_email_confirmation_url(request, emailconfirmation) if next_url: return '%s?next=%s' % (email_conf_url, next_url) else: return email_conf_url def get_email_confirmation_redirect_url(self, request): next_url = request.GET.get('next') if next_url: return next_url else: return super(CustomAccountAdapter, self).get_email_confirmation_redirect_url(request)
Just need to add the new adapter to the settings:
ACCOUNT_ADAPTER = 'my.path.to.CustomAccountAdapter'
To avoid using ACCOUNT_CONFIRM_EMAIL_ON_GET because of the problems above, it is also possible to store a session. Unfortunately this works only if the user used the same browser right after signing up. Similarly, ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION needs the user to use the same browser just after signing up.
For example the code could be:
class MyAccountAdapter(DefaultAccountAdapter):
def get_email_confirmation_url(self, request, emailconfirmation):
next_url = request.POST.get('next')
request.session['next_on_email_confirmation'] = next_url
email_conf_url = super(MyAccountAdapter, self).get_email_confirmation_url(
request, emailconfirmation)
return email_conf_url
def get_email_confirmation_redirect_url(self, request):
if 'next_on_email_confirmation' in request.session:
return request.session['next_on_email_confirmation']
else:
return super(MyAccountAdapter, self).get_email_confirmation_redirect_url(request)
Most helpful comment