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

What's the reset password flow? #604

Closed
ysyyork opened this Issue Apr 10, 2016 · 44 comments

Comments

Projects
None yet
@ysyyork
Copy link

ysyyork commented Apr 10, 2016

I am a little confused about how the flow goes when user want to reset their passwords.

In my app, user will call post /password first to get the reset password email. Then he/she will click the link and it will redirect to my redirect_url with token, uid and I believe this redirect_url will lead the user to the password form. Then using that form with the token and uid, call put /password to reset the password.

I wonder if this is the right flow. If yes, then there is one thing I feel strange. Say we have a user getting the reset password email and click the link to the form. Then he/she closes the page by accident. So he/she comes to the email and again to click the same link but it will return a 404 resource not found issue. I looked into the code and seeing that when calling get /password/edit, the controller first execute this
@resource = resource_class.reset_password_by_token({ reset_password_token: resource_params[:reset_password_token] })

Then inside the reset_password_by_token method, it accept password and password_confirmation as parameters as well as reset_password_token. Lastly, it will update the reset_password_token which cause the email link invalid when hit it the second time. So I think when calling reset_password_by_token method in the edit action, it should also get the password and password_confirmation right? Otherwise, the link will go invalid.

@JamesChevalier

This comment has been minimized.

Copy link

JamesChevalier commented Apr 27, 2016

I'm just as confused about this as you are. I'd love it if the result of this Issue is that a new page is created in the Wiki which completely and thoroughly documents the password reset flow (including where any front end pages are expected to be created).

Here's a starting point ... I try to be as clear as possible about where my understanding completely falls apart:

  • user goes to the front end site, types their email address into a field and clicks a link to reset password
  • that link sends a request to api: POST /auth/password with email (the email supplied in the field) & redirect_url (unsure what this is supposed to be) parameters
  • the api responds to this request by sending an email to the address provided ... this email appears to be the reset_password_instructions.html.erb file from devise, which includes a link to a devise URL that is not within the API (which seems wrong to me)
  • the user clicks the link in the email, which brings them to the devise page, and I lose the plot ... this seems like it should be a devise_token_auth URL & that something should happen in the api (perhaps authenticate that this request actually is who they claim to be, by comparing the token provided to the reset_password_token in the database), then redirect the user to the front end site to supply the password & password_confirmation
  • i'm off the reservation at this point, but my guess is that the front end site should collect the password & password confirmation fields, and send a request to api: PUT /auth/password with the password & password_confirmation parameters
@midnight-wonderer

This comment has been minimized.

Copy link

midnight-wonderer commented May 8, 2016

I'm also lost but I have a feeling that the redirect_url posted to /auth/password should bring user to the frontend which contain a new password form. The form then submit both reset password token and new password altogether in one go.

Anybody have some working example/demo?

@midnight-wonderer

This comment has been minimized.

Copy link

midnight-wonderer commented May 8, 2016

After some code inspection I suspect this is the flow.
The flow is almost identical to what @JamesChevalier described.
redirect_url should be frontend url that contain form for password resetting.

After user click on the link the devise_token_auth basically set a flag on the result

        # allow user to change password once without current_password
        @resource.allow_password_change = true;

and then redirect to fronend form (supplied by redirect_url) with temporary authorization credential.

The fronend then invoke passwords#update with password params and credential headers provided as query parameter of redirected address.
With the help of flag set by previous step devise_token_auth will call
update_attributes on @resource (User) to update the password without current password required.

This is very bad flow IMO and introduce lots of inconsistency in the application. The most annoying issue is what @ysyyork described. I will rewriting it all in my app. I don't think the maintainer will accept it upstream. For me what I described in previous comment is how it should be.

@JamesChevalier

This comment has been minimized.

Copy link

JamesChevalier commented May 11, 2016

Thanks @MidnightWonderer that helped me sort out the entire flow. Here's how I understand it:

  • user goes to a page on the front end site which contains a form with a single text field, they type their email address into this field and click a button to submit the form
  • that form submission sends a request to the API: POST /auth/password with some parameters: email (the email supplied in the field) & redirect_url (a page in the front end site that will contain a form with password and password_confirmation fields)
  • the API responds to this request by generating a reset_password_token and sending an email (the reset_password_instructions.html.erb file from devise) to the email address provided within the email parameter
    • we need to modify the reset_password_instructions.html.erb file to point to the API: GET /auth/password/edit
    • for example, if you have your API under the api/v1 namespaces: <%= link_to 'Change my password', edit_api_v1_user_password_url(reset_password_token: @token, config: message['client-config'].to_s, redirect_url: message['redirect-url'].to_s) %> (I came up with this link_to by referring to this line)
  • the user clicks the link in the email, which brings them to the 'Verify user by password reset token' endpoint (GET /password/edit)
  • this endpoint verifies the user and redirects them to the redirect_url if they are who they claim to be (if their reset_password_token matches a User record)
  • this redirect_url is a page on the frontend which contains a password and password_confirmation field
  • the user submits the form on this frontend page, which sends a request to API: PUT /auth/password with the password and password_confirmation parameters
  • the API changes the user's password and responds back with a success message
  • the front end needs to manually redirect the user to its login page after receiving this success response
  • the user logs in

I added this as a Wiki Page so that it can be referenced by everyone who might have the same question.

@kjakub

This comment has been minimized.

Copy link

kjakub commented May 30, 2016

After reciveing confirmation mail and clicking link in email :

http://www.localhost:3000/password/change?client_id=4xr_kzCDv9AvTVmsowCs0w&config=default&expiry=&reset_password=true&token=Tdk68v-z6gmvm7AbMsEH&uid=user%40hotmail.com

I land (after redirection) on...

simple page with form
`

Change your password

<%= params %>

<%= form_tag('/auth/password', method: :put) do %>

<%= label_tag :password, "New password" %>
<%= password_field_tag :password %>
<%= label_tag :password_confirmation, "Confirm new password" %>
<%= password_field_tag :password_confirmation %>
<%= submit_tag "Change my password" %>
<% end %> `

after clicking submit with following form data:

utf8:✓ _method:put authenticity_token:hFs/6jWU3IIqF1PBsmgRjbLoCf3c6YjMdvBSTTHhhNOcMM9XPbGPnkkFNG9/rn+BNM5BV/oOoQ0/fQMkz9DKHg== password:12345678 password_confirmation:12345678 commit:Change my password

i always get
{"success":false,"errors":["Unauthorized"]}

@piotr-galas

This comment has been minimized.

Copy link

piotr-galas commented Jun 6, 2016

@kjakub you need to send also authentication headers. Update password method check if user is authenticate

@piotr-galas

This comment has been minimized.

Copy link

piotr-galas commented Jun 6, 2016

@JamesChevalier user need to be authenticate when he is redirected to 'redirect_url' . So reset password work like "login button". I mean user can login this way. I wondering is it acceptable? I thought that user should login after reset not during.

@umekun123

This comment has been minimized.

Copy link

umekun123 commented Jun 6, 2016

When I call post: "auth/password" to send reset password link to a registered email, I set redirect url as "auth/password/edit" with email for params.

However, after I received the password reset email and click the "Change my password" link, it jumps to "auth/password/edit" address with token.

Then, I always get "The page you were looking for doesn't exist." page.

I thought if I use "auth/password/edit" for redirect url, it shows Password Reset page automatically, but it is not correct?

Should I create the password reset page with proper forms by myself?

@andrewferk

This comment has been minimized.

Copy link

andrewferk commented Sep 23, 2016

@umekun123 I've only started using devise_token_auth, but I'd say "yes", you should create the password reset page.

Also, can this issue be closed? It seems like it was solved with the Reset Password Flow in the wiki.

@stoconnor

This comment has been minimized.

Copy link

stoconnor commented Nov 12, 2016

I've been working on this flow for a bit, documentation is a little vague and hard to follow, particularly if you're not rendering html pages from your server. Current implementation just doesn't seem like a good flow if using devise_token_auth in an API. Rendering a HTML page from a JSON API just didn't sit right me. As a result I've done the password reset flow as follows, also using ng-token-auth:

  1. User requests a password reset through $auth.requestPasswordReset
  2. Open a new dialog after request, telling user to check their email for a Password Reset Code. Dialog should have three input fields: Reset Code, Password, Password Confirmation and Submit button.
  3. Add config.reset_password_within = 1.hour to initializers\devise.rb
  4. Modify reset_password_instructions.html.erb mailer to expose the password token to the user, something like the following:
<p><%= t(:hello).capitalize %> <%= @resource.email %>!</p>

<p><%= t '.request_reset_link_msg' %></p>

<p><%=  "The following is your Password Reset Code, this code will expire in 1 hour: " + @token %></p>

<p><%= t '.ignore_mail_msg' %></p>
<p><%= t '.no_changes_msg' %></p>

5 . Add a new password reset function in your controller, this will find user via reset code entered and update password for that user accordingly :

def password_reset
  resetCode = (params['resetCode'])
  reset_password_token = Devise.token_generator.digest(self, :reset_password_token, resetCode)
  user = User.find_by reset_password_token: reset_password_token
  if user == nil
    return render :json => { :success => false, :notice => 'Unable to find user' }
  end
  if user.reset_password_sent_at < 1.hour.ago
    return render :json => { :success => false, :notice => 'Password Token has now expired' }
  elsif user.update_attributes(params[:user])
    return render :json => { :success => true, :notice => "Password has been updated" }
  else
    return render :json => { :success => false, :notice => 'Unknown error has occured' }
  end
end

6 . Create a new route for your controller function, you will need to skip authentication for this action also.
7 . User can then return to the app to submit their password reset code and updated password via the new route you just created.

There is also the option to log the user in automatically after this in your app. 1 hour might be too long also for such an exposed function. This flow will bring a user back to your app which I personally think is a better flow than app > email > server password page > confirmation page > app. Hope this helps someone.

@franciscoml67

This comment has been minimized.

Copy link

franciscoml67 commented Nov 21, 2016

I know that I'm digging up quite a (by now) stable issue, but I'm having problems with this again.
I'm getting a 401 Unauthorized error from the API in the last few steps (PUT /auth/password).

Do I need to send the headers? If so, how can I do that?

EDIT:

And how do I retrieve the uid, access_token and client in this process?

@kaiomagalhaes

This comment has been minimized.

Copy link

kaiomagalhaes commented Nov 24, 2016

I'm facing the same issue as @franciscoml67 any idea about how should the request be done to the api?

@kaiomagalhaes

This comment has been minimized.

Copy link

kaiomagalhaes commented Nov 25, 2016

@andrewferk it was not solved, on the wiki it doesn't explain how we deal when the user doesn't remember his password. We are not logged in in this case so so far I didn't find a way to make it happen without a great workaround

@kaiomagalhaes

This comment has been minimized.

Copy link

kaiomagalhaes commented Nov 25, 2016

@piotr-galas what should happen in the case the user isn't authenticated like when he forgot his password?

@kaiomagalhaes

This comment has been minimized.

Copy link

kaiomagalhaes commented Nov 25, 2016

@JamesChevalier what should happen in the case described above?

@JamesChevalier

This comment has been minimized.

Copy link

JamesChevalier commented Nov 26, 2016

Sorry, @kaiomagalhaes - I don't understand your question.

My understanding is that the Reset Password Flow is the process the user goes through when they do not remember their password.

@kaiomagalhaes

This comment has been minimized.

Copy link

kaiomagalhaes commented Nov 26, 2016

Agreed @JamesChevalier. But how do we get the access token if we can't login?

@piotr-galas

This comment has been minimized.

Copy link

piotr-galas commented Nov 26, 2016

@kaiomagalhaes why we can't login? After click on link we are logged in. So we can. But in my opinion it is wrong approach. E.g. you can click on link, and instead of reset password you can access protected page of application. I rebuild this behavior in my application.
I take access token from link(my reset form not need authorization) and instead of create authorized cookie, I use token only to reset password. I didn't log user in. After reset user can login in normal way.

@kaiomagalhaes

This comment has been minimized.

Copy link

kaiomagalhaes commented Nov 27, 2016

Nevermind @piotr-galas. My mistake.

@ysyyork

This comment has been minimized.

Copy link
Author

ysyyork commented Jun 4, 2017

@kaiomagalhaes if you GET auth/password/edit, the controller will set the token, client etc for you and redirect to your redirect_url where is the reset password form page located. refer to https://github.com/lynndylanhurley/devise_token_auth/blob/master/app/controllers/devise_token_auth/passwords_controller.rb#L74

def edit
      @resource = resource_class.reset_password_by_token({
        reset_password_token: resource_params[:reset_password_token]
      })

      if @resource && @resource.id
        client_id  = SecureRandom.urlsafe_base64(nil, false)
        token      = SecureRandom.urlsafe_base64(nil, false)
        token_hash = BCrypt::Password.create(token)
        expiry     = (Time.now + DeviseTokenAuth.token_lifespan).to_i

        @resource.tokens[client_id] = {
          token:  token_hash,
          expiry: expiry
        }

        # ensure that user is confirmed
        @resource.skip_confirmation! if @resource.devise_modules.include?(:confirmable) && !@resource.confirmed_at

        # allow user to change password once without current_password
        @resource.allow_password_change = true;

        @resource.save!
        yield @resource if block_given?

        redirect_to(@resource.build_auth_url(params[:redirect_url], {
          token:          token,
          client_id:      client_id,
          reset_password: true,
          config:         params[:config]
        }))
      else
        render_edit_error
      end
end
@ysyyork

This comment has been minimized.

Copy link
Author

ysyyork commented Jun 4, 2017

I think the reason why the link can only be used once is caused by misusing the reset_password_by_token method. From devise, what I read is like this: https://github.com/plataformatec/devise/blob/ee01bac8b0b828b3da0d79c46115ba65c433d6c8/lib/devise/models/recoverable.rb#L143

This method aims to find the user by reset_password_token as well as setting up the new password. Thus, after that the reset_password_token will be changed cus this method should be called when you actually change the password. But here, all we need is to find the user by reset_password_token, thus this shouldn't change the token itself.

And a quick fix to this bug is override the passwords_controller like below:

class PasswordsController < DeviseTokenAuth::PasswordsController
  def edit
    original_token       = resource_params[:reset_password_token]
    reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token)
    @resource = resource_class.find_or_initialize_with_error_by(:reset_password_token, reset_password_token)

    if @resource && @resource.id
      client_id  = SecureRandom.urlsafe_base64(nil, false)
      token      = SecureRandom.urlsafe_base64(nil, false)
      token_hash = BCrypt::Password.create(token)
      expiry     = (Time.now + DeviseTokenAuth.token_lifespan).to_i

      @resource.tokens[client_id] = {
          token:  token_hash,
          expiry: expiry
      }

      # ensure that user is confirmed
      @resource.skip_confirmation! if @resource.devise_modules.include?(:confirmable) && !@resource.confirmed_at

      # allow user to change password once without current_password
      @resource.allow_password_change = true;

      @resource.save!
      yield @resource if block_given?

      redirect_to(@resource.build_auth_url(params[:redirect_url], {
          token:          token,
          client_id:      client_id,
          reset_password: true,
          config:         params[:config]
      }))
    else
      render_edit_error
    end
  end
end
@sebabouche

This comment has been minimized.

Copy link

sebabouche commented Jun 9, 2017

That part is not well documented in the README:

  • After user has clicked the link in his email…
  • Its browser/app does a GET request on the backend (auth/password/edit) to validate the reset_password_token and redirect_url. This can only be done once, for security reasons.
  • If the reset_password_token is valid, this url redirects to the url you have specified in redirect_url (or the one you set in an initializer as default_password_reset_url ). And on this url you can access as query params token, client_id, reset_password, expiry and uid.
  • You have to set all these (not sure which ones are not required) as Request Headers so that when the user will trigger a PUT request to auth/password/ with password and password_confirmation, the backend will know who is changing it's password. Now user can do multiple requests, in case password is too short or mismatch.
@dylanlewis89

This comment has been minimized.

Copy link

dylanlewis89 commented Aug 22, 2017

☝️ thanks for that flow, really helped me out.

A follow-up question:

After following this flow, I'm getting 401 unauthorized from my PUT request to auth/password. It appears that the PasswordsController is implementing the set_user_by_token before that action can take place. And set_user_by_token appears to require the user to be logged in in order to set @resource properly. This doesn't seem right to me since a user should be able to update their password via this flow without being logged in, correct?

Perhaps I'm missing something but wanted to see if others had issues with this.


UPDATE: It does look as though this flow is working but it's possible, depending on your configured header names, that you may need to make the following changes to your headers rather passing back the query params exactly as they appear in the URL:

  • client_id as client
  • token as access-token
@denishaskin

This comment has been minimized.

Copy link

denishaskin commented Sep 10, 2017

About to start on getting this into our app, and the whole discussion in this issue is a little depressing :-) . So it sounds like this does not work out of the box, but requires overrides and modifications? It's unclear whether the wiki page describes how it should work, or how it does work?

Oh well, guess I need to just roll up my sleeves and dive in...

@lynndylanhurley

This comment has been minimized.

Copy link
Owner

lynndylanhurley commented Sep 10, 2017

Maybe this will help?

At the end of this the user will be signed in to a valid session, and the client should direct the user to reset their password.

@denishaskin

This comment has been minimized.

Copy link

denishaskin commented Sep 10, 2017

@lynndylanhurley I think that helps, yes (not having dived in yet). The Client Displays Success step is then when Client allows the User to set a new password, correct, since there is now a valid authenticated session for the user?

@lynndylanhurley

This comment has been minimized.

Copy link
Owner

lynndylanhurley commented Sep 10, 2017

The Client Displays Success step is then when Client allows the User to set a new password, correct, since there is now a valid authenticated session for the user?

That's correct. That Client Displays Success may not be the best name for that step. Should have probably called it Client is Authenticated, Should Display Password Reset Form or something.

@denishaskin

This comment has been minimized.

Copy link

denishaskin commented Sep 10, 2017

And this should be implementable out-of-the-box, without needing any overrides or bug fixes and etc?

@lynndylanhurley

This comment has been minimized.

Copy link
Owner

lynndylanhurley commented Sep 10, 2017

I've been using it on several production projects per year without any modifications. But your app might have different requirements than mine. In that case there should be enough info in the README to configure or override this lib to suit your needs.

One more thing that's not documented: after the API validates the password reset token, the redirect back to the client will have a querystring param called password_reset_success. That's how the client will know to display the password reset form.

@denishaskin

This comment has been minimized.

Copy link

denishaskin commented Sep 11, 2017

I think what's not clear from your diagram is that (as others have documented earlier in this discussion) the link in User Visits Link From Email is a link to a URL on the API server. Is that correct?

If that is the case, that explains why folks are modifying things. It definitely strikes me as weird for the API interface to get accessed directly (even if it is just a redirect), instead of always through the client application.

I'll work with that (assuming that's correct), but if I had time I'd consider making a PR to provide an alternative that doesn't require that (as other have done. above).

@lynndylanhurley

This comment has been minimized.

Copy link
Owner

lynndylanhurley commented Sep 11, 2017

I think what's not clear from your diagram is that (as others have documented earlier in this discussion) the link in User Visits Link From Email is a link to a URL on the API server. Is that correct?

That is correct. I'll update the diagram to reflect that. What is the problem with that exactly?

@denishaskin

This comment has been minimized.

Copy link

denishaskin commented Sep 11, 2017

Not really a "problem", per se. Just conceptually to me, all of the user's interactions should be with the client app, and the only thing interacting with the backend API should be the client app. This is the one place where the browser makes a request directly to the backend API (not via the client app). Even though it's just a redirect, and the user should never see it, it feels like it breaks the conceptual model.

@andrewferk

This comment has been minimized.

Copy link

andrewferk commented Sep 11, 2017

I'm working on a team that is using devise and devise_token_auth in our API and have customized the password reset to work as an API endpoint vs a direct endpoint with a redirect.

We have customized the Devise::Mailer#reset_password_instructions mailer and associated view to use the url of our react/redux client instead of an API endpoint. The frontend stores the reset password token and asks the user for their new password. Then, the reset password token and new password are sent as a PUT /auth/password AJAX request to the API. We also customized the DeviseTokenAuth::PasswordsController#update action to support this new flow. The user no longer directly visits our API to reset a password.

We've customized other workflows, too. It's all doable, just ignore the horrible design of devise and devise_token_auth.

@lynndylanhurley

This comment has been minimized.

Copy link
Owner

lynndylanhurley commented Sep 11, 2017

Not really a "problem", per se. Just conceptually to me, all of the user's interactions should be with the client app, and the only thing interacting with the backend API should be the client app.

Having it work this way simplified the original client libraries that were built with this project. When the client was initialized (i.e. on page load), it already needed to check for tokens in the URL querystring. By sending the new session tokens from the API in the password reset success redirect, no additional work was necessary on the client.

If you think there's a better way to handle this, please send a PR.

@denishaskin

This comment has been minimized.

Copy link

denishaskin commented Sep 12, 2017

I completely get that you originally built this for your projects' needs, and I greatly appreciate you providing this library for others to use. Thank you! If I can find the time, maybe the least I can do is a PR to help clarify the documentation, since it seems a lot of people run into this confusion/expectation.

@zachfeldman

This comment has been minimized.

Copy link
Collaborator

zachfeldman commented Oct 7, 2017

@denishaskin we would love your PR!

@amyin

This comment has been minimized.

Copy link

amyin commented Nov 14, 2017

I am doing the same thing as @andrewferk because I felt uncomfortable sending a "session secret" token in the url params in the redirect. I am using my own method for update that resets the password given a password_reset_token otherwise just calls super to handle the logged-in user case. For me that means that the PasswordsController#edit functionality is no longer used. Curious to know how you did it Andrew?

@amyin

This comment has been minimized.

Copy link

amyin commented Nov 15, 2017

In case someone also wants to have all email linking directly to the frontend, here is my PasswordsController#update and related spec (will try to add a formal PR to the gem over winter break!)

   # this is where users arrive after visiting the password reset confirmation link
    # from email, hitting the frontend and then the frontend resending the token
    # along with the user inputted new password
    def update
      unless resource_params[:reset_password_token]
        # use standard devise update method if current_password is being used
        # otherwise use custom reset password flow
        return super
      end

      @resource = resource_class.with_reset_password_token(
        resource_params[:reset_password_token]
      )

      render_invalid_reset_token_error unless @resource

      # ensure that user is confirmed since token must be from email
      if @resource.devise_modules.include?(:confirmable) && !@resource.confirmed_at
        @resource.skip_confirmation!
      end

      # make sure account doesn't use oauth2 provider
      unless @resource.provider == 'email'
        return render_update_error_password_not_required
      end

      # ensure that password params were sent
      unless password_resource_params[:password] &&
             password_resource_params[:password_confirmation]
        return render_update_error_missing_password
      end

      if @resource.update_attributes(password_resource_params)
        yield @resource if block_given?
        # does not authenticate user. User must still log in
        return render_update_success
      else
        return render_update_error
      end
    end

rspec

  # devise_token_auth has email links that go to the api instead of the frontend
  # the first click to the email goes to #edit and
  # then redirects to the frontend with tokens in the url params
  # tokens (session secrets) are insecure as url params so we are writing
  # a custom reset password solution that hits the frontend
  # with a password_reset_token and then the frontend will send an api request
  # with the password_reset_token and new password fields
  # API will respond with success and user will have to login again
  describe '#update' do
    subject { patch '/users/password', params: params }
    let(:params) do
      {
      reset_password_token: @password_reset_token,
      password: 'password',
      password_confirmation: 'password'
    }
    end

    before do
      @password_reset_token = user.send_reset_password_instructions
    end

    it 'returns 200' do
      subject
      expect(response.status).to eq(200)
    end

    it 'updates the user password' do
      subject
      expect(user.reload.valid_password?('password')).to be_truthy
    end

    it 'does not allow the reset_password_token to be used twice' do
      subject
      expect(user.reload.reset_password_token).to be_nil
    end

    context 'no reset password' do
      let(:params) do
        {
        password: 'password',
        password_confirmation: 'password'
      }
      end

      it 'does not reset password without token AND without current password' do
        expect { subject }.to_not change {
          user.reload.valid_password?('test123!')
        }.from(true)
      end

      it 'returns 401' do
        subject
        expect(response.status).to eq(401)
      end
    end

    context 'passwords do not match' do
      let(:params) do
        {
        reset_password_token: @password_reset_token,
        password: 'password',
        password_confirmation: 'another-password'
      }
      end

      it 'does not reset password' do
        expect { subject }.to_not change {
          user.reload.valid_password?('test123!')
        }.from(true)
      end

      it 'returns 422' do
        subject
        expect(response.status).to eq(422)
      end

      it 'returns meaningful error message' do
        subject
        expect(parsed_response_body['errors'][0]['message'])
          .to include("Password confirmation doesn't match Password")
      end

      it 'allows another request with same reset_password_token' do
        subject
        expect(User.with_reset_password_token(@password_reset_token)).to eq(user)
      end
    end

    context 'unconfirmed user' do
      let(:user) { create(:user, :unconfirmed) }
      it 'confirms the user account' do
        subject
        expect(user.reload).to be_confirmed
      end
    end
  end
@BenjaminKim

This comment has been minimized.

Copy link

BenjaminKim commented Dec 4, 2017

@ysyyork is talking about #691 issue and I agree him.
This situation is very bad as @ysyyork said.

Say we have a user getting the reset password email and click the link to the form. Then he/she closes the page by accident. So he/she comes to the email and again to click the same link but it will return a 404 resource not found issue.

And even worser situation is that some mail clients visit the links of the mail content automatically for security reason. In this case the user never cannot reset the password.

@zachfeldman

This comment has been minimized.

Copy link
Collaborator

zachfeldman commented Dec 4, 2017

@BenjaminKim see my comment in the other issue thread, please submit a PR if you think, "the situation is very bad".

@KelseyDH

This comment has been minimized.

Copy link

KelseyDH commented Dec 12, 2017

I ran into a gotcha while trying to implement an API-only password reset link for iOS and Android apps based on the above discussion.... which is that since mobile clients consuming my API are not web browsers, any link pointing or redirecting to them will not open unless they use a custom app scheme url (e.g so something like myapp://callback/reset_password_token?reset_password_token= instead of http://myapp.com/ ... )

While custom url schemes work in many web app contexts, for password reset emails it's unworkable: it turns out many mail clients -- including Gmail -- strip custom url schemes from links to make these customized links useless.

The only way I could find to get around this limitation? To have the Android and iOS app redirects happen via my Rails app, by sending them there first in the password reset link, and then redirecting them to the appropriate custom scheme url that's needed.

Conclusion? API-only email link password resets for iOS and Android are unworkable: if you want the user to click a link that sends them back into the client, you're going to need to create an accessible url endpoint for users to visit first. An added benefit of this is you can now target the redirect based on the User-Agent. (E.g. if from a web browser: open a web form. If using iOS, redirect to a custom app scheme...)

Lesson: +1 for rendering a devise frontend with html.

@dnorthrupva

This comment has been minimized.

Copy link

dnorthrupva commented Jan 10, 2018

I have one question - I get the flow, but I'm confused about one thing.

If I want to require current_password to be required for MOST updates, but if I follow the flow we'll come to an issue where when I get the redirect_url with the UID etc appended, I still have no way of passing the 'current_password', or am I missing something?

@MaicolBen

This comment has been minimized.

Copy link
Collaborator

MaicolBen commented Mar 30, 2018

@dnorthrupva You don't need the reset password flow if you are changing the password, just call the PUT auth/password/ with the current password

@MaicolBen

This comment has been minimized.

Copy link
Collaborator

MaicolBen commented Apr 3, 2018

I think the flow is pretty covered in https://github.com/lynndylanhurley/devise_token_auth/blob/master/docs/faq.md#whats-the-reset-password-flow. About the alternative flow for only-API mentioned here should be followed in #1072

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment