53

I have a Rails 4 application set up to use Devise, and I'm running a problem with password resets. I have the mailer set up, and the password reset email sends fine. The link provided has the correct reset_password_token assigned to it, which I checked with that database. However, when I submit the form with correctly formatted passwords, it gives an error saying that the reset token is invalid.

However, the exact same code works fine locally through rails s. The email sends, and I can actually reset the password. The code I use is just the standard Devise code, I haven't overridden any of it.

Perhaps it's something with Apache? I'm not too familiar with it. Does anyone have any ideas?

justindao
  • 2,273
  • 4
  • 18
  • 34

5 Answers5

134

Check the code in app/views/devise/mailer/reset_password_instructions.html.erb

The link should be generated with:

edit_password_url(@resource, :reset_password_token => @token)

If your view still uses this code, that will be the cause of the issue:

edit_password_url(@resource, :reset_password_token => @resource.password_reset_token)

Devise started storing hashes of the token, so the email needs to create the link using the real token (@token) rather than the hashed value stored in the database.

This change occurred in Devise in 143794d701

doctororange
  • 11,670
  • 12
  • 42
  • 58
  • 2
    I am having the same problem, and it still happens after changing the edit_password_url to use @token. Any idea what else can be causing this? Thanks! – Sakin Oct 10 '13 at 21:44
  • That did it! Thanks a ton! @Sakin Not sure what's going on there - have you checked yourself to make sure the tokens are the same? – justindao Oct 14 '13 at 17:03
  • 2
    Thank you very much. Wasted almost an hour there. – Kulbir Saini Mar 12 '14 at 21:03
  • This change occurred in version 3.1.0 – Weston Ganger Aug 27 '15 at 21:09
  • 1
    This can also occur if you are using a custom devise mailer, then upgrade devise, so the 'new' versions of the mailer erb files are written to the default location – jpw Jun 09 '16 at 06:15
  • 1
    Life saving answer. Thanks a lot dude, but don't know why I needed to user the old version to fix mi issue. I had `@token` and had to use `@resource.reset_password_token` for getting it working. Devise 4.1.1 – Francisco Quintero Jul 06 '16 at 19:37
11

In addition to doctororange's fix, if you're overwriting resource.find_first_by_auth_conditions, you need to account for the case where warden_conditions contains a reset_password_token instead of an email or username.

EDIT: To elaborate:

Devise adds functionality to your model when you say 'devise :registerable, :trackable, ...'.

In your User model (or Admin, etc), you can overwrite the Devise method named find_first_by_auth_conditions. This special method is used by the Devise logic to locate the record that is attempting to be logged in to. Devise passes in some info in a parameter called warden_conditions. This will contain an email, a user-name, or a reset_password_token, or anything else you add to your devise log-in form (such as an account-id).

For example, you might have something that looks like this:

(app/models/user.rb)
class User

  ...

  def self.find_first_by_auth_conditions warden_conditions
    conditions = warden_conditions.dup

    if (email = conditions.delete(:email)).present?
      where(email: email.downcase).first
    end
  end

end

However, The above code will break the password-reset functionality, because devise is using a token to locate the record. The user doesn't enter an email, they enter the token via a query-string in the URL, which gets passed to this method to try and find the record.

Therefore, when you overwrite this special method you need to make it more robust to account for the password-reset case:

(app/models/user.rb)
class User

  ...

  def self.find_first_by_auth_conditions warden_conditions
    conditions = warden_conditions.dup

    if (email = conditions.delete(:email)).present?
      where(email: email.downcase).first
    elsif conditions.has_key?(:reset_password_token)
      where(reset_password_token: conditions[:reset_password_token]).first
    end
  end

end
MaximusDominus
  • 2,017
  • 18
  • 11
7

If you are taking the URL from a log, it can appear like this:

web_1      | <p><a href=3D"http://localhost:3000/admin/password/edit?reset_password_to=
web_1      | ken=3DJ5Z5g6QNVQb3ZXkiKjTx">Change password</a></p>

In this case, using 3DJ5Z5g6QNVQb3ZXkiKjTx as the token will not work because =3D is really an = character encoded.

In this case, you need to use J5Z5g6QNVQb3ZXkiKjTx (with 3D removed)

ybart
  • 876
  • 8
  • 23
1

Although the accepted answer is correct, wanted to explain why this is happening so you can use it in some other cases as well. If you take a look at the method which is generating the password reset token:

def set_reset_password_token
    raw, enc = Devise.token_generator.generate(self.class, :reset_password_token)

    self.reset_password_token   = enc
    self.reset_password_sent_at = Time.now.utc
    self.save(validate: false)
    raw
end

You will see that the raw is being returned, and the enc is being saved in the database. If you are using the value from the database - enc to put into a password_reset_token in a hidden field of your form, then it will always say Token invalid as that is encrypted token. The one which you should use is the raw token.

This was done because in case some admin (or a hacker) can access the database, the admin could easily reset anyone's password by just using encrypted token, which is tried to be avoided.

Some information about this and some other changes in Devise can be found in the devise's change-log blog post or in the devise's issue discussion

Aleks
  • 4,866
  • 3
  • 38
  • 69
0

It may also be worth noting (in addition to @doctororange's post ablve) the following if you are using a custom confirmation mailer view.

The link in the view has also changed here. This is the NEW link code:

<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>

This is the OLD link code:

<p><%= link_to 'Confirm my account', user_confirmation_url(@resource, :confirmation_token => @resource.confirmation_token) %></p>
pixelearth
  • 13,674
  • 10
  • 62
  • 110