Skip to content

How To: Email only #

Dan Frenette edited this page Jul 26, 2020 · 29 revisions

This guide will show you how to remove the password inputs from your registration page, in favor of asking users to enter a password in the confirmation step. Now clicking the link in the confirmation email will take the user to a webpage that asks them to set their password.

This customization is often used:

  • When you want a gradual engagement experience where your users are asked to provide minimal information at each step of your registration process.
  • When you create accounts on your users' behalf, for example when clients contact you.

Current approach

This is the easiest approach. It simply modifies the confirmation action to redirect the user to the existing Devise view to set a password. A previous approach is described below, but is not recommended. A more complex approach, which is more customizable, is described in another article.

For Devise 3 and 4

  1. If you haven't already, enable the confirmable module
  2. If you haven't already, follow the Devise instructions for configuring views
  3. Remove the password and password_confirmation inputs from your views/devise/registrations/new.html.erb
  4. Override the Devise definition of password_required? in your model:
      protected
        def password_required?
          confirmed? ? super : false
        end
  5. Tweak the language in views/devise/passwords/edit.html.erb by changing:
    1. the page title from "Change your password" to "Set your password"
    2. the submit button value from "Change my password" to "Set my password"
  6. If you haven't already, follow the Devise instructions for configuring controllers specifically for ConfirmationsController
  7. Override the Devise definition of after_confirmation_path_for in your ConfirmationsController:
      protected
        def after_confirmation_path_for(resource_name, resource)
          token = resource.send(:set_reset_password_token)
          edit_password_path(resource, reset_password_token: token)
        end
  8. Add a test in your model to catch any changes in Devise's behavior in the future (because set_reset_password_token is a protected method):
      describe '#set_reset_password_token' do
        it 'returns the plaintext token' do
          potential_token = subject.send(:set_reset_password_token)
          potential_token_digest = Devise.token_generator.digest(subject, :reset_password_token, potential_token)
          actual_token_digest = subject.reset_password_token
          expect(potential_token_digest).to eql(actual_token_digest)
        end
      end

For Devise 2

  1. Follow the steps above except the final 2
  2. Override the Devise definition of after_confirmation_path_for in your ConfirmationsController:
      protected
        def after_confirmation_path_for(resource_name, resource)
          token = resource.send(:generate_reset_password_token)
          edit_password_url(resource, reset_password_token: token)
        end
  3. Add a test in your model to alert you to any changes in Devise's behavior in the future (because generate_reset_password_token is a protected method):
      describe '#generate_reset_password_token' do
        it 'returns the plaintext token' do
          potential_token = subject.send(:generate_reset_password_token)
          actual_token = subject.reset_password_token
          expect(potential_token).to eql(actual_token)
        end
      end

Custom redirect after setting password

If you want to change where your user is redirected after setting their password, override the Devise definition of after_resetting_password_path_for in your PasswordsController:

  protected
    def after_resetting_password_path_for(resource)
      some_other_url
    end

Failure to set a password

If a user does not set a password after clicking the confirmation link (unlikely but possible), they simply have no password and cannot log in. They cannot re-use the confirmation link - their confirmation is now complete - so they must use the "Forgot your password?" link to trigger a password reset email. In that case, the Devise recoverable module must be enabled.

Note: An account is considered confirmed when a user clicks the confirmation link, regardless of whether the user sets a password after clicking the link. If you would rather ignore attempts to confirm unless the user provides a password, look at the other approach.

Previous approach (not recommended)

This approach is more complex than the one described above, and was not designed for the latest versions of Devise or Rails. It is considered deprecated and appears here for historical reasons.

With this approach, if the user does not set their password, they are not confirmed - they must set their password in order to confirm their account.

With this approach, you can also keep the default Devise # where a new user enters both an email and password (so the user has the option to set a password at registration or confirmation). In that case, just skip the first step.

Note: if you use multiple user resource tables (like :clients and :admins) see the example at the bottom.

(This technique was first documented by Claudio Marai. The following steps combine Claudio's example code with code contributed in the comments section of his post and updates everything for Devise 2. NOTE: This is for Rails 3 and Devise ~> 2.0. The following view code uses formtastic and haml gems because they make the code cleaner, with less typing. See simple_form for another easy way to create forms.)

1. Modify the #new view in app/view/users/registrations (optional step)

This step is optional: This modification allows a user to # with only their email address. Skip this step if you're only interested in allowing admins to create users with only an email address.

You want to make it easy for new users to # -- just ask for their email address and not worry about passwords for now. That's the "gradual engagement" approach!

    %h2 #. All we need is your email address.
    = semantic_form_for(resource, :as => resource_name, :url => user_registration_path(resource)) do |form|
      = devise_error_messages!
      = form.inputs do
        = form.input :email, :input_html => {:autofocus => true}
      = form.actions do
        = form.action :submit, :label => "#"
    = render 'shared/links'

2. Create a #show view in app/view/confirmations

In this view, we ask the new user to create a password and confirm it. We embed the confirmation_token in a hidden input field so that the controller will receive it. (We'll explain the confirm_path in a later step):

    %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 :password, :input_html => {:autofocus => true}
        = form.input :password_confirmation
        = form.input :confirmation_token, :as => :hidden
      = form.actions do
        = form.action :submit, :label => 'Confirm Account'

For Rails 4/Devise 3:

    %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 :password, :input_html => {:autofocus => true}
        = form.input :password_confirmation
        = form.input :confirmation_token, :as => :hidden, :input_html => { :value => @original_token }
      = form.actions do
        = form.action :submit, :label => 'Confirm Account'

3. Create a #password_match? method and overwrite Devise's #password_required?

We need a convenient way to test that :password_confirmation matches :password and report any errors. We also need to overwrite Devise's #password_required? method so the user can # without specifying a password. So in your model, add these public methods:

    class User < ActiveRecord::Base
      def password_required?
        super if confirmed?
      end

      def password_match?
        self.errors[:password] << "can't be blank" if password.blank?
        self.errors[:password_confirmation] << "can't be blank" if password_confirmation.blank?
        self.errors[:password_confirmation] << "does not match password" if password != password_confirmation
        password == password_confirmation && !password.blank?
      end
    end

4. Overwrite the Devise confirmations controller's #show

Create a new controller app/controllers/confirmations_controller.rb to overwrite Devise's #show. If the user is already confirmed, they are only confirming a change in email address so call super. And do you remember calling #confirm_path in the #show view? The #confirm method will be called when the user submits the form. But we'll only confirm the user if :password matches :password_confirmation, otherwise re-render the #show view so the user can try again.

Calling super if the user is already confirmed makes the first step (above) optional and only needed if you want a "gradual engagement" sign-up.

    class ConfirmationsController < Devise::ConfirmationsController
      def show
        self.resource = resource_class.find_by_confirmation_token(params[:confirmation_token]) if params[:confirmation_token].present?
        super if resource.nil? or resource.confirmed?
      end

      def confirm
        self.resource = resource_class.find_by_confirmation_token(params[resource_name][:confirmation_token]) if params[resource_name][:confirmation_token].present?
        if resource.update_attributes(params[resource_name].except(:confirmation_token)) && 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

For Rails 4/Devise 3: Strong attributes will require permit, so be sure to include the passed parameters on the update_attributes.

    class ConfirmationsController < Devise::ConfirmationsController

      def show
        if params[:confirmation_token].present?
          @original_token = params[:confirmation_token]
        elsif params[resource_name].try(:[], :confirmation_token).present?
          @original_token = params[resource_name][:confirmation_token]
        end

        self.resource = resource_class.find_by_confirmation_token Devise.token_generator.
          digest(self, :confirmation_token, @original_token)

        super if resource.nil? or resource.confirmed?
      end

      def confirm
        @original_token = params[resource_name].try(:[], :confirmation_token)
        digested_token = Devise.token_generator.digest(self, :confirmation_token, @original_token)
        self.resource = resource_class.find_by_confirmation_token! digested_token
        resource.assign_attributes(permitted_params) unless params[resource_name].nil?

        if resource.valid? && resource.password_match?
          self.resource.confirm!
          set_flash_message :notice, :confirmed
          sign_in_and_redirect resource_name, resource
        else
          render :action => 'show'
        end
      end

     private
       def permitted_params
         params.require(resource_name).permit(:confirmation_token, :password, :password_confirmation)
       end
    end

5. Tell Devise to use the new controller

Finally, tell Devise about the new controller and its #confirm action. (Notice the pluralized resource name in the #devise_for method call and the singular resource name in the #devise_scope method call):

    devise_for :users, :controllers => {:confirmations => 'confirmations'}

    devise_scope :user do
      put "/confirm" => "confirmations#confirm"
    end

For Rails 4 with Devise 3 use patch "/confirm" => "confirmations#confirm".

Examples for multiple resources

Here's an example for multiple resources, which is why we use resource and resource_name and resource_class in the confirmations controller. We use named routes so Devise can detect the different resource scopes and use the same confirmations controller.

First, the routes:

    devise_for :clients,     :controllers => {:confirmations => 'confirmations'}
    devise_for :admins,      :controllers => {:confirmations => 'confirmations'}
    devise_for :supervisors, :controllers => {:confirmations => 'confirmations'}

    devise_scope :client do
      put "/clients/confirm" => "confirmations#confirm", :as => :client_confirm
    end
    devise_scope :admin do
      put "/admins/confirm" => "confirmations#confirm", :as => :admin_confirm
    end
    devise_scope :supervisor do
      put "/supervisors/confirm" => "confirmations#confirm", :as => :supervisor_confirm
    end

You have to create app/views/clients/registrations/new.html.haml, app/views/admins/registrations/new.html.haml, and app/views/supervisor/registrations/new.html.haml templates.

Next, the #show view (we just change the confirm_path helper call):

    %h2 You're almost done! Now create a password to securely access your account.
    = semantic_form_for(resource, :as => resource_name, :url => send(:"#{resource_name}_confirm_path")) do |form|
      = devise_error_messages!
      = form.inputs do
        = form.input :password, :input_html => {:autofocus => true}
        = form.input :password_confirmation
        = form.input :confirmation_token, :as => :hidden
      = form.actions do
        = form.action :submit, :label => 'Confirm Account'
Clone this wiki locally