devise: 'Confirmation token is invalid' when confirming after registering

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:

  • Devise 3.1.0 (041fcf90807df5efded5fdcd53ced80544e7430f)
  • Rails 4.0.0

Code sample

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

Possible explanation

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

Last thoughts…

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:

About this issue

  • Original URL
  • State: closed
  • Created 11 years ago
  • Comments: 26 (5 by maintainers)

Most upvoted comments

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>

@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. 😃

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.

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.

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)