Registrazione in due step con Devise
[** UPDATE: 24/05/2012 **]: Grazie a Kevin Triplettora c’è una pagina sul wiki di Devise che fa riferimento a questo post combinando anche i vari commenti e contributi ricevuti. Visitate la pagina, soprattutto se utilizzate Rails 3 e Devise 2: https://github.com/plataformatec/devise/wiki/How-To:-Two-Step-Confirmation
Nei miei progetti in Ruby on Rails generalemente utilizzo Devise come gem per l’autenticazione degli utenti.
Nell’ultima applicazione sviluppata avevo necessità di personalizzare Devise in modo che gli utenti potessero registrarsi fornendo solo l’indirizzo email.
La password di accesso doveva essere impostata nello step di conferma dell’account.
Dopo alcuni test, sono giunto alla seguente soluzione.
1. Per prima cosa ho dovuto sovrascrivere il ConfirmationsController. Quindi nel file routes.rb ho impostato devise perchè utilizzase il mio controller custom (il mio model degli utenti si chiamava Account).
Ho dovuto sovrascrivere anche il RegistrationsController per personalizzare la pagina di registrazione:
1 2 3 4 5 | </p> <p>devise_for :accounts, :controllers => {:confirmations => "confirmations", :registrations => "registrations"} do</p> <p>put "confirm_account", :to => "confirmations#confirm_account"</p> <p>end</p> <p> |
Come vedete ho anche aggiunto un metodo custom, confirm_account che userò nel quarto step, come vedremo tra poco.
2. A questo punto devo fare in modo di evitare la validazione della password di devise.
Per questo scopo ho scritto un initializer chiamato devise_customization.rb in /config/initalizers/ come segue:
1 2 3 4 | </p> <p>module Devise</p> <p>module Models</p> <p>module Validatable |
1 2 3 4 | def password_required?</p> <p>false</p> <p>end</p> <p> |
1 2 3 4 | end</p> <p>end</p> <p>end</p> <p> |
In questo modo ho sovrascritto il modulo validatable di devise skippando la validazione della password.
3. Il passo successivo è la personalizzazione delle view.
Poichè utilizzo i miei ConfirmationsController e RegistrationsController custom ho copiato le view standard di devise sotto /views/confirmations/ per le confirmations e sotto /views/registrations/ per le registrations.
Le ho poi modificate come segue per adattarle al mio workflow di registrazione.
La view /registrations/new è diventata cosi (utilizzo haml per i layout)
1 2 3 4 5 6 7 8 9 10 11 12 | </p> <p>= form_for(resource, :as => resource_name, :url => account_registration_path(@account)) do |f|</p> <p>= devise_error_messages!</p> <p>%p</p> <p>= f.label :email</p> <p>= f.email_field :email</p> <p>%p.clearfix</p> <p>= f.submit 'Signup'</p> <p>= link_to 'Home', root_url</p> <p>%br/</p> <p>= render :partial => 'devise/shared/links'</p> <p> |
In questa view ho semplicemente rimosso i campi password e password_confirmation.
La view /confirmations/show è invece diventata:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | </p> <p>= form_for(resource, :url => confirm_account_path) do |f|</p> <p>= devise_error_messages!</p> <p>%p</p> <p>= f.label :email</p> <p>= @account.email</p> <p>= f.hidden_field :confirmation_token</p> <p>%p</p> <p>= f.label :password</p> <p>%br/</p> <p>= f.password_field :password</p> <p>%p</p> <p>= f.label :password_confirmation</p> <p>%br/</p> <p>= f.password_field :password_confirmation</p> <p>%p.clearfix</p> <p>= f.submit 'Confirm Account'</p> <p>= link_to 'Home', root_url</p> <p>%br/</p> <p>= render :partial => 'devise/shared/links'</p> <p> |
4. Ok, ora che ho definito le mie view è tempo di scrivere il nuovo metodo di conferma dell’account nel ConfirmationsController. Posso invece lasciare inalterato il RegistrationsController.
1 2 3 4 5 6 7 8 | </p> <p>class ConfirmationsController < Devise::ConfirmationsController</p> <p>def show</p> <p>@account = Account.find_by_confirmation_token(params[:confirmation_token])</p> <p>if !@account.present?</p> <p>render_with_scope :new</p> <p>end</p> <p>end |
1 2 3 4 5 6 7 8 9 10 11 | def confirm_account</p> <p>@account = Account.find(params[:account][:confirmation_token])</p> <p>if @account.update_attributes(params[:account]) and @account.password_match?</p> <p>@account = Account.confirm_by_token(@account.confirmation_token)</p> <p>set_flash_message :notice, :confirmed</p> <p>sign_in_and_redirect("account", @account)</p> <p>else</p> <p>render :action => "show"</p> <p>end</p> <p>end</p> <p> |
1 2 | end</p> <p> |
Il metodo show semplicemente si occupa di trovare l’account in base al token ricevuto e poi renderizzza la view show.
Il punto chiave è il metodo confirm_account dove viene trovato l’account, eseguito l’update dei suoi attributes e se il metodo password_match? ritorna true allora l’account viene confermato chiamando il metodo standard di devise confirm_by_token
5. L’ultima cosa che rimane da fare è definire il metodo password_match? all’interno del model Account.
1 2 3 | </p> <p>class Account < ActiveRecord::Base</p> <p>... |
1 2 3 4 5 6 | def password_match?</p> <p>self.errors[:password] << 'password not match' if password != password_confirmation</p> <p>self.errors[:password] << 'you must provide a password' if password.blank?</p> <p>password == password_confirmation and !password.blank?</p> <p>end</p> <p> |
1 2 | end</p> <p> |
E questo è tutto!
Ora potete riavviare il server rails e registrarvi nella vostra applicazione scegliendo la password solo in fase di conferma dell’account.
Tags: customization, devise, haml, rails, registration, ruby, ruby on rails, signup

















Thanks for sharing your technique. It would make a great addition (or link) on the Devise wiki How To-s at github.
Passing the user id as a hidden field is a bad idea. It would be trivial to change that, submit the form and get access to another account.
Hello Kris, thanks for reporting. In fact should be used the the cofirmation_token in place of the id.
I’ve fixed the example code.
Claudio, I’ve been working on this for about two weeks and I’m just stuck. Everything I have looks exactly like you have showed except my model is called ‘user’. I’m getting a `no route matches ‘confirm_account’` message. I tried to change ‘put’ to ‘match’ in my routes.rb, but that threw a lot more errors (at least it was finding it, though…maybe?)
I’d really like to get this working. Do you have any advice?
Hi Kevin, I don’t know what cause the error.
Have you try to see the routes with rake routes?
If you want post your code so I can try to find the problem.
Got it worked out. You can see the details here: http://stackoverflow.com/questions/6588865/rails-devise-two-step-confirmation-route-error
I’m a newbie, so please excuse my poor understanding of things.
Thanks for this! I had just decided that asking for a password at signup, and then requiring confirmation, makes more sense for my project. I will let you know how it works out!
Claudio,
I think there’s a missing “if” statement in code you provide in the account model for “def password_match?”
2
3
4
5
self.errors[:password] << 'password not match' password != password_confirmation
self.errors[:password] << 'you must provide a password' if password.blank?
password == password_confirmation and !password.blank?
end
this works for me:
2
3
4
5
self.errors[:password] << 'password not match' if password != password_confirmation
self.errors[:password] << 'you must provide a password' if password.blank?
password == password_confirmation and !password.blank?
end
Thanks again for this blog post. You really helped me out.
I added the “if”.
Thank you Fred for your bug fixing
Hi
I used your method in my rails 3 app with webrick and nginx but its taking too much time for user registration and user confirmations although i used the same code you provided.
Regards
I believe you also need to change Account.find to Account.find_by_confirmation_token in the confirm_account action of the controller.
I appreciate you taking the time to write this up. It is very close to what I need for my site and it is helping me tremendously. I followed your instruction and I ran into an error. (I am sure I just did something wrong but I was hoping you might be able to help if you have time). It works all the way up to the entering the password part. Once I submit the password it gives and error.
ActiveRecord::RecordNotFound in ConfirmationsController#confirm_admin
Couldn’t find Admin with id=WjqDa7hz2Y9bUh5EKxBf
Rails.root: Websites/Development/Rails/appignani
app/controllers/confirmations_controller.rb:10:in `confirm_admin’
It should not be looking for ID and as far as the code I can see and entered it is not. Here is the confirm_admin code:
2
3
4
5
6
7
8
9
10
@admin = Admin.find(params[:admin][:confirmation_token])
if @admin.update_attributes(params[:admin]) and @admin.password_match?
@admin = Admin.confirm_by_token(@admin.confirmation_token)
set_flash_message :notice, :confirmed
sign_in_and_redirect("admin", @admin)
else
render :action => "show"
end
end
I appreciate any help you can give on this. Thank you.
I figured it out. It is what the last post before mine suggested.
Awsome tip. Took me 10 minutes after reading your blog to have it working for myself.
I was looking to implement something similar when I ran across this post. I was considering using the built-in token_authenticatable module in Devise to provide access to a newly created account where the user has not set a password. I was going to set the password to something random and force a change when the user confirms the account. I was going to allow access to confirmation and password changing via the token_authentication. I am glad I ran across this because I don’t think I need to use the token_authenticatable module, I can just use the confirmation token! I think I will still just set the password to something random for a non-confirmed user since I don’t want to override the password validation methods in Devise as I have other models that need it.
Anyways, thanks for the write, lots of good ideas in here!
Works great!
Step #4 under confirm_account method, Account.find should be Account.find_by_confirmation_token
For my case I found that I could simply override password_required in my user model like so:
2
3
confirmed?
end
This allows the user to confirm their account without setting a password only when their account has not yet been confirmed. After confirmation, the password is required.
This is exactly what I wanted – thanks for writing it up!
I had some trouble setting this up on my end however – I get a SQLite3 Constraint Exception error with this. More details here: http://stackoverflow.com/questions/10442510/rails-two-step-signup-with-devise-errors
I was wondering if anyone else faced similar problems setting this up. I would really appreciate any help!
Excellent post, thanks! I have multiple Devise resources and I want each resources to be able to use this feature. So here’s what I did…
In my confirmation controller:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def show
self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
if !resource.present?
render_with_scope :new
end
end
def confirm
self.resource = resource_class.find_by_confirmation_token(params[resource_name][:confirmation_token])
if resource.present? && resource.update_attributes(params[resource_name]) && resource.password_match?
resource = resource_class.confirm_by_token(resource.confirmation_token)
set_flash_message :notice, :confirmed
sign_in_and_redirect(resource_name.to_s, resource)
else
render :action => "show"
end
end
end
Very subtle difference, but now any Devise resource can use this controller. (I added the check for present? in case someone tries to submit the form a second time.) Oh — you also have to change the routing configuration (which note that the new version of Devise now deprecates passing a block to #device_for:
2
3
4
5
6
7
8
9
devise_for :admins, :controllers => {:confirmations => 'confirmations'}
devise_scope :clients do
put "confirm", :to => "confirmations#confirm"
end
devise_scope :admins do
put "confirm", :to => "confirmations#confirm"
end
Thanks again, Claudio — and if you have time, ha ha, please change above your Account.find to Account.find_by_confirmation_token above. Just in case someone is too lazy and they don’t read these comments. Again, thanks for documenting this sweet feature!
Also, thanks to Shane, I made my model methods and grammatized the error messages:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
super if confirmed?
end
def password_match?
if password_confirmation.blank?
self.errors[:password_confirmation] << "cannot be blank"
elsif password != password_confirmation
self.errors[:password_confirmation] << "does not match password"
end
if password.blank?
self.errors[:password] << "cannot be blank"
end
password == password_confirmation && !password.blank?
end
Serves me right for posting late at night! It worked for my clients model but devise gets confused with the same routing information for multiple resources. So here’s what works with multiple Devise resources:
In my routes.rb:
2
3
4
5
6
7
8
9
10
11
12
13
devise_for :admins, :controllers => {:sessions => 'sessions', :confirmations => 'confirmations'}
devise_for :super_users, :controllers => {:sessions => 'sessions', :confirmations => 'confirmations'}
devise_scope :client do
match "/client/confirm" => "confirmations#confirm", :as => :client_confirm
end
devise_scope :admin do
match "/admin/confirm" => "confirmations#confirm", :as => :admin_confirm
end
devise_scope :super_user do
match "/super_user/confirm" => "confirmations#confirm", :as => :super_user_confirm
end
(Notice the singular v. plural of the resource names. I’m sure put v. match would also work.)
And here’s the hack I had to do in order to use the same view for each resource (I use the semantic forms gem):
2
3
4
5
6
7
8
9
10
11
%h2 You're almost done! Now create a password to securely access your account.
= semantic_form_for(resource, :as => resource_name, :url => confirm_path) do |form|
= devise_error_messages!
= form.inputs do
= form.input :email, :input_html => { :disabled => true }
= form.input :password
= form.input :password_confirmation
= form.input :confirmation_token, :as => :hidden
= form.actions do
= form.action :submit, :label => 'Confirm Account'
I disabled the email field so the client can see their email address but not change it. Sorry for the mis-post! Carry on.
One more thing: in my confirmations controller, I had to reference the token this way:
resource_class.confirm_by_token(params[resource_name][:confirmation_token])
Otherwise, it raised an exception for me. YMMV
Me again, but with sleep I noticed that if the user changes their email address then they are sent a confirmation email and, after clicking the confirmation link, the user is presented with the same confirmation screen asking them to enter in their new password. Whoops. The client is not expecting this behavior so here is how I modified my controller code:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
before_filter :top_nav_for_public
def show
self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
debugger
if resource.present? && resource.confirmed?
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
set_flash_message :notice, :confirmed
sign_in_and_redirect(resource_name, resource)
else
render_with_scope :new
end
end
def confirm
debugger
self.resource = resource_class.find_by_confirmation_token(params[resource_name][:confirmation_token])
if resource.present? && resource.update_attributes(params[resource_name]) && resource.password_match?
self.resource = resource_class.confirm_by_token(params[resource_name][:confirmation_token])
set_flash_message :notice, :confirmed
sign_in_and_redirect(resource_name.to_s, resource)
else
render :action => "show"
end
end
end
I also added self.resource= statements, which I forgot to do in the earlier code. Again, great blog post Claudio!
Whoops — sorry, take out the two debugger statements and you have the correct code.
Dang, so Devise deprecated and then removed #render_with_scope, but that’s okay, it made #show simpler:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
before_filter :top_nav_for_public
def show
self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token])
if resource.present? && resource.confirmed?
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
set_flash_message :notice, :confirmed
sign_in_and_redirect(resource_name, resource)
end
end
def confirm
self.resource = resource_class.find_by_confirmation_token(params[resource_name][:confirmation_token])
if resource.present? && resource.update_attributes(params[resource_name]) && resource.password_match?
self.resource = resource_class.confirm_by_token(params[resource_name][:confirmation_token])
set_flash_message :notice, :confirmed
sign_in_and_redirect(resource_name, resource)
else
render :action => "show"
end
end
end
Much nicer!
I’ve created a Devise wiki page referencing this post and combining the comments. Please check this wiki page, especially if you’re using Rails 3 and Devise 2:
https://github.com/plataformatec/devise/wiki/How-To:-Two-Step-Confirmation
Feel free to add your examples for Rails 2 and Devise 1. Thanks again.
Thank you Kevin for your big contribution! I’ve updated the post with a reference to the Devise wiki page.
Claudio, thanks for the article. Kevin, thanks for the Wiki page. The URL is access-controlled, though available in Google cache. Could you please open up the Wiki page.
The wiki page got renamed, the new URL is https://github.com/plataformatec/devise/wiki/How-To:-Email-only-sign-up — the page should be open now and accessible by everyone. Thanks, Mario, for the shout out!
Sorry, I meant Claudio (I have to stop playing my Nintendo)
Thank you Kevin,
I’ve updated the reference.
After the form on the confirmations show page is submitted, how can I customize the redirect for that form?
find_by_confirmation_token(nil).update_attributes is dangerous