Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Password reset link result bad token in mobile environment (iOS) #2201

Open
sabatons opened this issue Feb 6, 2019 · 8 comments
Open

Password reset link result bad token in mobile environment (iOS) #2201

sabatons opened this issue Feb 6, 2019 · 8 comments

Comments

@sabatons
Copy link

sabatons commented Feb 6, 2019

Switching between mobile email app and browser app is pretty common scenario in mobile environment. After follow password reset url django-allauth return bad token text.
This is due to url was accessed from mail app.

iOS 12.1.3 , latest Gmail and Chrome
django-allauth==0.38.0

How to reproduce:

  1. Submit 'password reset with key' form
  2. Go to Gmail app and click on provided url
  3. Google Chrome opens with allauth's bad token page

Expected result:

User will be shown password reset form (which will be shown if you copy and paste provided url )

@sabatons sabatons changed the title Password reset bad token on iOS with Gmail app and Chrome app. Password reset link result bad token in mobile environment (iOS) Feb 6, 2019
@Danilka
Copy link

Danilka commented May 1, 2019

Here is how the current system works:

  1. You get a password reset link with a unique key in the email.
  2. You open the link, it calls PasswordResetFromKeyView that saves this key into your session and redirects back to the same page, but with a fake static key. (I can go on a long rant that this is a useless exercise since the original URL was exposed and this adds no extra security, but we will skip it.)
  3. The same PasswordResetFromKeyView fires again and it can already reset your password.

What happens on iOS, in particular in Gmail's app is that Gmail follows the link for you to its final URL in its own "internal browser" and then opens only the destination URL in the actual external browser. The problem here is that the session is now saved in your Gmail's browser, and the actual browser that you intend to use only sees this fake key and an empty session. Hence the library says that it cannot reset anything, because the token is bad.

To fix this, you can manually overwrite this view, get rid of this redirect dance, and just reset a password via the original URL with password reset key in its GET parameter.

In your views.py, add this overwritten method:

from allauth.account.forms import UserTokenForm
from allauth.account.views import PasswordResetFromKeyView as AllauthPasswordResetFromKeyView
from allauth.account.views import _ajax_response
from django.views.generic.edit import FormView

class PasswordResetFromKeyView(AllauthPasswordResetFromKeyView):

    def dispatch(self, request, uidb36, key, **kwargs):
        self.request = request
        self.key = key
        token_form = UserTokenForm(
            data={'uidb36': uidb36, 'key': self.key})
        if token_form.is_valid():
            # Store the key in the session and redirect to the
            # password reset form at a URL without the key. That
            # avoids the possibility of leaking the key in the
            # HTTP Referer header.
            # (Ab)using forms here to be able to handle errors in XHR #890
            token_form = UserTokenForm(
                data={'uidb36': uidb36, 'key': self.key})
            if token_form.is_valid():
                self.reset_user = token_form.reset_user
                return super(FormView, self).dispatch(request, uidb36, self.key, **kwargs)
        self.reset_user = None
        response = self.render_to_response(
            self.get_context_data(token_fail=True)
        )
        return _ajax_response(self.request, response, form=token_form)

Then in your urls.py add this view before allauth's urls like so:

from django.urls import include, path, url
from project.views import PasswordResetFromKeyView

urlpatterns = [
    url(r"^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", PasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
    url(include('allauth.urls')),
]

@elkd
Copy link

elkd commented Sep 18, 2019

@Danilka thank you for your input. I'm not an experienced programmer, can you please explain why are you validating the token_form twice with the same data?!

@pavitrakumar78
Copy link

Hi, are there any updated solutions for this? the one suggested by Danilka does not work anymore.

@pennersr
Copy link
Owner

pennersr commented Mar 7, 2022

@Danilka With respect to this:

since the original URL was exposed and this adds no extra security, but we will skip it.)

The approach taken prevents the token from being leaked via the HTTP_REFERER header -- so it does add security.

Related: https://groups.google.com/g/django-developers/c/RyDdt1TcH0c/m/yA-Iq09dAwAJ?pli=1

@pavitrakumar78
Copy link

@pennersr just a quick question: since this issue is not closed yet, can we assume since this is not an issue (?) with allauth but rather how these type of links work in iOS. So, it's unlikely that there will be a fix specific for this?

Another related issue I'm having is already mentioned in #2858
A solution that was tried by the author of the issue is also mentioned here: https://stackoverflow.com/questions/65482931/django-allauth-google-login-not-working-on-ios

Due to these reasons, we have to tackle this problem specifically for iOS by overriding methods/checking for device OS?

@paltman
Copy link

paltman commented Apr 28, 2022

I'm having trouble seeing exactly what the referrer problem is especially if you don't have any outbound links or link to any sources not controlled by your server.

In that vein, I think skipping the redirection should be perfectly fine. Please correct me if I'm mistaken. I want to understand the problem.

Here is an update to @Danilka's workaround that is a bit more concise (I don't need ajax) and works for me on latest library:

class PasswordResetFromKeyView(AllauthPasswordResetFromKeyView):
    def dispatch(self, request, uidb36, key, **kwargs):
        self.request = request
        self.key = key
        token_form = UserTokenForm(data={"uidb36": uidb36, "key": self.key})
        if token_form.is_valid():
            self.reset_user = token_form.reset_user
            handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
            return handler(request, uidb36, self.key, **kwargs)
        self.reset_user = None
        return self.render_to_response(self.get_context_data(token_fail=True))

@derek-adair
Copy link

derek-adair commented Sep 18, 2023

if you don't have any outbound links or link to any sources not controlled by your server.

What if they do? How would you suggest we implement this while guarding against such a thing. I am not a fan of a looming pit of failure that could result in major security issues for users.


As for what to do about this...

giphy

@pennersr uhhh. This is a doozy of an annoying issue. It is actually an infuriating thing when this kind of thing happens as a user. I was... SERIOUSLY freaking losing my mind at costco trying to set my password. I still haven't bothered with it 2 years later. Never did I imagine i'd have flashbacks to this experience.

I am a bit out of my depth here so I cannot add much to the actual conversation other than I'd like to see this kind of ux addressed and I can imagine users doing pretty wild things to get this to work. If this can't be fixed in allauth w/o adding security vulns, we need to document how to work around this and the risks associated w/ said workarounds.

@ekoka
Copy link

ekoka commented Apr 27, 2024

This article describes the token leak that the redirect is trying to mitigate: https://developer.mozilla.org/en-US/docs/Web/Security/Referer_header:_privacy_and_security_concerns. It also lists a few approaches to prevent it.

The redirect technique used here is known as the "exit page redirect". It's described here https://geekthis.net/post/hide-http-referer-headers/#exit-page-redirect

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants