2

I'm using Devise 3.2.3 on a Rails 4.0.3 app

In my user model I have an after_create: subscribe callback that subscribes a new user to a newsletter. After I introduced this callback every time a new user attempts to confirm their email they get confirmation token is invalid message. Nevertheless confirmation link from the resend confirmation email works.

I can obviously avoid using the :after_create callback, but that's quite painful.

User.rb:

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :trackable, :validatable,
         :confirmable, :rememberable

    has_many :things
    belongs_to :city
    validates_presence_of :city_id
  validates :email, :presence => true, :email => true

  after_create :subscribe


  def subscribe(frequency = :weekly)
    if [:weekly, :monthly].include? frequency
      response = Rails.configuration.mailchimp.lists.subscribe({
        id:           get_list_id(frequency),
        email:        { email: email },
        merge_vars:   { HASH: hashify(frequency), USER_ID: id }, # generate approptiate hash
        double_optin: false
      })
      # response
    end
    update_attributes(newsletter_frequency: frequency.to_s)
    response
  end
end
Sbbs
  • 1,610
  • 3
  • 22
  • 34
  • Can you please show us how your model code looks like? – gernberg Mar 07 '14 at 00:05
  • I ran into a problem similar to this recently. Have you recently changed your devise version? I think perhaps this change in the code may be what you are experiencing. https://github.com/plataformatec/devise/commit/143794d701bcd7b8c900c5bb8a216026c3c68afc#diff-fa817bf486c518644016136465d914b2R86 – Sean Mar 07 '14 at 00:15
  • @Sean, yes, I did upgrade it recently. Though I cannot exactly understand how does that break my code. – Sbbs Mar 07 '14 at 00:19
  • 1
    @SB if you look at line 116, the `Devise.token_generator.digest` call changes the token you are looking for. I think this change is so that you are not using the token that is in the database, but rather a different token(for security purposes). I had to revert back to the latest version which does not incorporate that change, until I decide the best way to proceed, which I believe is Devise v3.0.4 – Sean Mar 07 '14 at 00:38
  • If you look at your logs, when the user submits the form, is the token that is being used to look for the user the same as the one in the email? – Sean Mar 07 '14 at 00:40
  • @Sean I think the token that devise stores in DB is different from the one it sends in the email (it emails a short token, but stores a large one in DB). So, not sure how to verify what you suggested. But I'm pretty sure you are right that it's the newer version of Devise that breaks this. Sounds like this issue needs to be filed on github. – Sbbs Mar 07 '14 at 01:27

3 Answers3

1

It has been 4 months, I don't know whether you have solved this problem, but still I will answer it.

I encountered this problem yesterday, and I am using the same Devise and Rails version. First I read the console log and find when the user is created, the confirmation token is valid, and then, it's rewritten.

After debugging, I found the problem is update in after create. Why? I find the update statement also adds unconfirmed_email attribute, and it means when updating, the email is changed. It's strange, because I don't update email column. So I add the following code just before update statement

changed_attributes.each do |attr, origin|
  puts "#{attr}: #{origin.inspect} => #{self.send(attr)}"
end

And the result is:

id: nil => 108
email: "" => d33faf@qq.com

See? When updating, the email address is changed, and even the id is created, it looks like the record is not created yet. It's very strange, because the record should have been created because it's been called after create.

It's all about transaction, create action is surrounded in a whole transaction. after create is still in this transaction, and as the insert statement is not committed yet, so Rails think the email in database is nil and the email is changed.

So comes to the end, the update trigger the following callback in Devise:

before_update :postpone_email_change_until_confirmation_and_regenerate_confirmation_token, if: :postpone_email_change?

And then it found email is changed, so it regenerate the confirmation token.

Ok, how can we avoid it?

The simplest way is using update_columns instead of update, which will not trigger before_update callback, but it will bypass validation.

The better way is using after_commit on: :create instead of after_create, because it will be executed after the transaction. But the problem is currently it has a bug. See the issue I created on Github: https://github.com/rails/rails/issues/16286, and the question on SO: Prevent infinite loop when updating attributes within after_commit, :on => :create

Community
  • 1
  • 1
Sefier Tang
  • 770
  • 3
  • 8
  • 25
  • Confirming that changing an after_create call that makes a change to the model in question to after_commit does resolve the issue and is also discussed here: github.com/plataformatec/devise/issues/2615. Unfortunately, bug mentioned still exists so the infinite loop link is helpful. – uhezay May 15 '15 at 10:43
0

I think this SO post could probably guide you in the right direction. Devise "Confirmation token is invalid" when user signs up.

Specifically this blog post http://blog.plataformatec.com.br/2013/08/devise-3-1-now-with-more-secure-defaults/

Community
  • 1
  • 1
Sean
  • 983
  • 5
  • 13
  • No, this doesn't seem to be the problem, because all works perfectly unless I have after_create: callback. – Sbbs Mar 07 '14 at 01:29
  • 1
    interesting. Perhaps your may need to delve into the devise code for this one then. Sorry I can't be of any help :S – Sean Mar 07 '14 at 06:35
0

you need to use 'after_confirmation' method instead of using after_create callback.

def after_confirmation
  #your code here  
end
Mayank
  • 171
  • 1
  • 6