When registering User who utilizes :confirmable, when the user clicks on the e-mail confirmation link, they receive an "Invalid confirmation token" error message.
This issue is present using:
041fcf90807df5efded5fdcd53ced80544e7430f)_user.rb_
class User
include Mongoid::Document
include Mongoid::Attributes::Dynamic
# Attributes
# ----------
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable, :lockable, :timeoutable
...
## Confirmable
field :confirmed_at, type: DateTime
field :confirmation_token, type: String
field :confirmation_sent_at, type: DateTime
field :unconfirmed_email, type: String
After registering, but before confirming, the User.confirmation_token is equal to 778fb (truncated for readability.) This token exactly matches the confirmation email's link (i.e. ?confirmation_token=778fb.)
However, after reading this post it appears that these tokens _shouldn't_ match, because Devise uses the email token to generate the matching User.confirmation_token with which to confirm them by.
Digging deeper into _confirmable.rb_...
The code that generates the User.confirmation_token:
# Generates a new random token for confirmation, and stores
# the time this token is being generated
def generate_confirmation_token
raw, enc = Devise.token_generator.generate(self.class, :confirmation_token)
@raw_confirmation_token = raw
self.confirmation_token = enc
self.confirmation_sent_at = Time.now.utc
end
The code that confirms a user:
# Find a user by its confirmation token and try to confirm it.
# If no user is found, returns a new user with an error.
# If the user is already confirmed, create an error for the user
# Options must have the confirmation_token
def confirm_by_token(confirmation_token)
original_token = confirmation_token
confirmation_token = Devise.token_generator.digest(self, :confirmation_token, confirmation_token)
confirmable = find_or_initialize_with_error_by(:confirmation_token, confirmation_token)
confirmable.confirm! if confirmable.persisted?
confirmable.confirmation_token = original_token
confirmable
end
From this code, it appears that the token, say 778fb, is persisted to User.confirmation_token, then sent out in the confirmation e-mail. When the user clicks the confirmation link, 778fb is passed into confirm_by_token, which generates another token (i.e. 945ac) which it searches the User table for finding nothing, thus invalid token.
It seems like in this case, User.confirmation_token should actually store 945ac but still send out 778fb in the confirmation link. I ran a simple test through my console, which seemed to confirm (no pun intended) this hypothesis:
new_token = Devise.token_generator.digest(User, :confirmation_token, '778fb')
u = User.first # The already registered, but unconfirmed user
u.confirmation_token = new_token
u.save
User.confirm_by_token('778fb') # Succeeds
Can someone explain why this is happening? It seems a little too obvious of a 'bug' to be true, but I haven't been able to work around it any other way.
See the following related links:
This happens for security reasons, storing the tokens directly in the database is a security risk. So we stored the digested versions. Just guarantee that your Devise mailers are sending the tokens using the @token variable, as mentioned in the blog post, and you shuold be fine. :)
Ahhh ok, that's definitely it. Thanks!
For anyone else who stumbles into this, you need to change your views/<user>/mailer/confirmation_instructions.html.erb to look something like this:
<p>Welcome <%= @resource.email %>!</p>
<p>You can confirm your account email through the link below:</p>
<p><%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @token) %></p>
:thumbsup: thank you!
@josevalim @delner, are you guys still dealing with anything like this?
I seem to be encountering an issue similar to this one. I am using :confirmable, and have
config.reconfirmable = true
in my Devise initializer. The only other thing I have done that is different from a vanilla devise setup (AFAIK) is change the layouts used in the Devise Views. However, I am getting the 'invalid confirmation token' error (ONLY on signup). After I 'resend confirmation instructions,' it works fine. The initial token seems to be invalid, and I'm not sure what could be causing it.
My view looks like this:
= link_to 'Confirm My Email', confirmation_url(@resource, confirmation_token: @token)
Again, the previous link works fine after getting the initial 'invalid token' screen, but when clicking the link after an initial sign up, it is borking.
@andrewpthorp Can you provide a way to reproduce the issue? Otherwise there isnt much we can do.
Honestly, I'm not sure. It seems like I am dealing with a very unique circumstance, as I can't find anyone else really dealing with this. It almost feels like the token is being reset after the email is firing, or something.
I do know that I am doing this as well:
custom_user_params = [:first_name, :last_name, :avatar, :public]
devise_parameter_sanitizer.for(:sign_up).concat(custom_user_params)
But that shouldn't have anything to do with it. I am also decorating current_user, using draper, but all of the methods are delegated to User. It's odd that this is happening. I'll try to dig in and figure out what it could be.
/cc @josevalim
As a matter of fact, if I just drop into console and run:
User.find(<id>).send_confirmation_instructions
It works fine, without filling out the form (ignoring the first email).
Was more just wondering if you heard about this at all, and what could be causing it. The other pieces that I have in my User are FriendlyId, rails delegations to an Account relationship, with an inverse_of on it.
Nothing too fancy. I'm sure there is something I'm doing that is screwing this up ;)
Thanks for the response, though - @josevalim - You are a true open source master! :)
@josevalim I just figured it out. I had the following in an after_create callback.
if account.nil?
self.account = Account.new
self.save
end
That self.save was updating the confirmation_token, because it was happening mid transaction. If I move it into an after_commit callback, or change it to self.account = Account.create, it works fine.
Interesting little edge case there.
Sorry for all of the messages. :)
@andrewpthorp Thanks for bringing it up. I had the same issue.
I also had an issue with the invalid confirmation token when doing an after_create callback that subscribes User to a newsletter, but unfortunately my workaround is not as painless as @andrewpthorp.
Someone suggested that this might be caused by Devise.token_generator.digest on line 116 here. Are there any plans to fix this and allow after callbacks?
@andrewpthorp I ran into the same issue. Your solution solved it, thanks!
@delner thanks for the answer! I should be more careful reading changelogs!
@andrewpthorp thanks!
@sevab you saved my life! Was debugging this for about 10 hours. How did you work around this issue?
@dbenjamin57, I think I ended up adding a newsletter_frequency column, setting it to nil on User registration, and then just running a cron job that would subscribe new users.
Just a note that I needed to change my after_create :method name to after_commit :method_name, on: :create but then ran into a looping bug. See http://stackoverflow.com/questions/22567358/prevent-infinite-loop-when-updating-attributes-within-after-commit-on-crea.
I think the send_reset_password_instructions below is done after the record is created in the database (without the hashed reset_password_token) . When the user subsequently clicked on the "Change my password" link in the email to change his password, there's nothing in the database to compare with token in the email so error "Reset password token is invalid" is returned.
after_create { |admin| admin.send_reset_password_instructions }
I am a newbie with rails and ruby so don't really know how to fix it. Hope someone else can.
@andrewpthorp I had the same issue, thanks for the fix :+1:
@andrewpthorp, thank you vm!
Thank you @andrewpthorp, I have exactly the same issue and you saved me hours of investigation !
The original breakdown of this issue was awesome. Thank you very much for that @delner (Ha! And I just realized I commented on your SO thread too).
I have a question that is related, but I feel like should be answered here for posterity:
How do I create @token on my own? I'm trying to do a visit user_confirmation_path(confirmation_token: token) from a Capybara test, but I'm currently using @user.confirmation_token which as @delner pointed out was wrong.
Is my only option really to find the link in the email body that gets pushed into the ActionMailer::Base.deliveries array?
Ah ok I figured it out. It's ugly as sin, but:
u = User.where(email: email).first
u.send :generate_confirmation_token
email_token = u.instance_variable_get(:@raw_confirmation_token)
u.save!
visit user_confirmation_path(confirmation_token: email_token)
If you are using visit, it is an integration test. If you don't check the e-mail, there is no guarantee you are actually sending the code via e-mail nor that the code is correct. The only way to be sure is by parsing it out of the e-mail which can honestly be done with a simple regex match on the body.
FWIW I'm using the confirmation token in a scenario where I give it out to a third-party email platform where it gets inserted into a confirmation email template. For that reason I have to extract it while in RegistrationsController#create using the hacky approach of instance_variable_get(:@raw_confirmation_token). Would be nice to have a proper accessor for that purpose.
I've gone through a plethora of answers that mention the @token to have the confirmations module work. It still doesn't. Even with a new application, @token doesn't work.
Most helpful comment
Ahhh ok, that's definitely it. Thanks!
For anyone else who stumbles into this, you need to change your
views/<user>/mailer/confirmation_instructions.html.erbto look something like this: