Setting Up Email For Rails Devise Authentication

Techie     August 2022

Introduction

There are several ways to authenticate users on a Rails app and the easiest approach is to use existing tools such as the Devise gem, since it’s a mature solution that has thousands of hours of code review, design, testing, and time in production.

During the authentication process, your app needs to commmunicate to the users via email because features such as password reset and account confirmation are contingent on the email service. Rails has an inbuilt component called Action Mailer that allows your app to send emails with just a few tweaks.

This section reveals the simple configurations needed to allow Devise to send account confirmation request and password reset link emails to app users.


Prerequisites

NB: You may skirt these requirement versions if you are willing to experiment.

  1. Ruby 3.2.0
  2. Rails 7.0.3
  3. Gmail account


Part A: Creating a Rails 7 Project

Create a new rails project called devise_email by following this document: Creating A Rails 7 App: With esbuild, bootstrap and jquery.


Part B: Installing Devise

1 . Add devise gem in the Gemfile file.

# Gemfile

gem 'devise'
  


2 . Bundle install

$ bundle install
  


3 . Setup devise

$ rails g devise:install


4 . Generate user model

$ rails g devise user


5 . Add confirmable and recoverable to the users table in db/migrations/[…]_devise_create_users.rb.

# db/migrations/[...]_devise_create_users.rb

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      #t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      #t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      #t.string   :unlock_token # Only if unlock strategy is :email or :both
      #t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    add_index :users, :confirmation_token,   unique: true
    #add_index :users, :unlock_token,         unique: true
  end
end


6 . Add devise :confirmable in models/user.rb file.

# models/user.rb
# ...
devise :confirmable


7 . Run migrations

$ rails db:migrate


8 . Add :turbo_stream as a navigational format in config/initializers/devise.rb. Rails 7 throws this error if you dont have it: undefined method `user_url’ for #<Devise::RegistrationsController:0x0000000000cf08>

# config/initializers/devise.rb
# ...

config.navigational_formats = ['*/*', :html, :turbo_stream]


9 . Redirect user to login page if they are not signed in.

Add this code to controllers/application_controller.rb before any other actions.

# controllers/application_controller.rb

  before_action :authenticate_user!
# ...


10 . Add sign in and log out links to the navbar in app/views/layouts/application.html.erb

<!-- views/layouts/application.html.erb -->
<!-- .... -->  

  <body>

   <nav class="navbar navbar-expand-sm bg-dark navbar-dark fixed-top">
    <div class="container-fluid">
     <a class="navbar-brand" href="#">Devise Email Example</a>
     <a class="navbar-brand" href="#"></a>
     <% if current_user.nil? %>
      <%= link_to "sign in", new_user_session_path, method: :get, class: "btn btn-primary btn-sm"%>
     <% else %>
      <%= link_to "log out", destroy_user_session_path, method: :delete, class: "btn btn-primary btn-sm"%>
     <% end %>        
    </div>
   </nav>
  
   <% flash.each do |key, value| %>
    <div class="<%= flash_class(key) %>">
     <%= value %>
    </div>
   <% end %>
    <%= yield %>
  </body>


Part C: Setting Up Gmail App Passwords

ActionMailer will use your gmail account to send mail, but you can not use your normal gmail password with external applications. Gmail has a nifty feature called App passwords that lets you sign in to your Google Account from apps on devices that don’t support 2-Step Verification. Click here to set up App passwords. On the page, select ‘Mail’ under the ‘select app’ tab, then enter a custom name under the ‘select device’ tab.

Click on the ‘Generate’ button to generate the password. Just like your normal password, this app password grants complete access to your Google Account. Copy 16-character password shown, you will need it for the next step.


Part D: Setting Up Environment Variables For the Email Credentials

1 . In your rails project, create a ruby hidden file in /config directory that will contain the ENV variables. Prepend a dot . in the file name to make it hidden.

e.g .my_secret.rb, which will contain this:

# /config/.my_secret.rb

ENV['EMAIL_USER_ID'] = 'your_id@gmail.com'
ENV['EMAIL_PASSWORD'] = 'the_gmail_app_password'

NB: press Ctrl + Hto view hidden files on linux.


2 . You want the rails app to load .my_secret.rb file as soon as the server fires up. To do that, you will use the File and load methods inside the /config/environment.rb file in the rails project. Make sure to place this code just above the line: Rails.application.initialize!

# /config/environment.rb

# load my_secret.rb
app_credentials = File.join(Rails.root, 'config', '.my_secret.rb')
load(app_credentials) if File.exist?(app_credentials)

# Initialize the Rails application.
Rails.application.initialize!


3 . Ensure the .my_secret.rb file is not submitted to github whenever you commit your project to a github repository. To do that, open the .gitignore file and add the path to the file as shown below:

# .gitignore

/config/.my_secret.rb


With the these steps you are done setting up the environment variables for your database. Now if you open the rails console and enter this ENV[‘EMAIL_USER_ID’], it should output the content of that variable:

 

$ ENV['EMAIL_USER_ID']

$ your_id@gmail.com

$ ENV['EMAIL_PASSWORD'] 

$ the_gmail_app_password


Part E: Configure Devise to work with ActionMailer

1 . Add your email address in config/initializers/devise.rb

# config/initializers/devise.rb
# ...
config.mailer_sender = 'your_id@gmail.com'

config.reconfirmable = false


2 . Configure ActionMailer for the development environment in config/environments/development.rb.

# config/environments/development.rb
# ...
  config.action_mailer.raise_delivery_errors = true

  config.action_mailer.perform_caching = false
  
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    :address => "smtp.gmail.com", 
    :port => 587,   
    :domain => "gmail.com",
    :tls => true,          
    :enable_starttls_auto => true,
    :authentication => :login,
    :user_name => ENV['EMAIL_USER_ID'],
    :password => ENV['EMAIL_PASSWORD']
  }


Testing the app

To run the app, cd into the root of the project and issue this command:

$ bin/dev

Now navigate to localhost:3000. Create an account by clicking on the sign up link. This should send you an email containing the confirmation instructions. You will also get reset password emails whenever you reset your password by clicking on forgot your password link.


Additional Information

- Redirecting users

1 . Redirecting After The User Signs Up (Confirmation Pending)

If you want to redirect the user to a specific url after signing up, override the after_inactive_sign_up_path_for in the registrations_controller.


i). Create a new registrations_controller.rb in app/controllers directory.

# app/controllers/registrations_controller.rb

class RegistrationsController < ::Devise::RegistrationsController
  layout false
  # the rest is inherited, so it should work
  
  private
  def after_inactive_sign_up_path_for(resource_or_scope)
    if resource_or_scope == :user || resource_or_scope.class == User || resource_or_scope == User
      your_pending_registrations_path
    #elsif resource_or_scope == :admin || resource_or_scope.class == AdminUser  || resource_or_scope == AdminUser
    #  admin_root_path
    else
      super
    end
  end
  
end


ii). Override Devise default behaviour in the routes in config/routes.rb

 
# config/routes.rb
 devise_for :users, controllers: { registrations: 'registrations' }
 


2 . Redirecting From The Confirmation Email

You may want to redirect the user to a specific url after they clicked the link in the confirmation email. To do that, just override the after_confirmation_path_for in the confirmations_controller.


i). Create a new confirmations_controller.rb in app/controllers directory.

# app/controllers/confirmations_controller.rb

class ConfirmationsController < Devise::ConfirmationsController
  private
  def after_confirmation_path_for(resource_name, resource)
    sign_in(resource) # In case you want to sign in the user
    # some_other_new_after_confirmation_path
  end
end


ii). Override Devise default behaviour in the routes in config/routes.rb

 
# config/routes.rb
 devise_for :users, controllers: { confirmations: 'confirmations' }


Thanks for reading, see you in the next one!