Devise

authentication libraryRubyRuby on Railsauthenticationsession managementpassword resetOAuth

Authentication Library

Devise

Overview

Devise is a flexible authentication solution for Ruby on Rails based on Warden. As of 2025, it has established itself as the most widely used authentication library in the Ruby on Rails ecosystem, providing comprehensive functionality to rapidly build robust user authentication systems. It comprehensively supports all authentication features required for modern web applications, including database authentication, password recovery, email confirmation, account registration, session management, token-based authentication, and OAuth integration. Through its modular design, you can select only the necessary features, and through deep integration with Rails, it achieves both simplified configuration and powerful customization capabilities.

Details

Devise 4.x series provides a robust authentication system based on the Warden middleware foundation. By combining 10 main modules (Database Authenticatable, Confirmable, Recoverable, Registerable, Rememberable, Trackable, Timeoutable, Validatable, Lockable, Omniauthable), you can build authentication functionality tailored to your application requirements. Complete integration with ActiveRecord allows you to easily add authentication features to user models, working in conjunction with Rails form helpers, validations, I18n, and mailers. Custom views, controllers, and route configurations enable complete customization of user interfaces and authentication flows.

Key Features

  • Modular Design: Select and use only the authentication features you need
  • Rails Integration: Complete integration with ActiveRecord, ActionMailer, and routing
  • Security: Password encryption with bcrypt, CSRF protection, secure session management
  • OAuth Support: Social login functionality through OmniAuth integration
  • Rich Features: Password reset, email confirmation, account locking, login tracking
  • Customization: Complete customization support for views, controllers, and authentication flows

Advantages and Disadvantages

Advantages

  • Deep integration with Rails ecosystem enables rapid and efficient authentication system construction
  • Rich functionality can handle complex authentication requirements
  • Modular design allows starting with minimal features and gradual expansion
  • Comprehensive documentation, community support, and long-term track record provide reliability
  • OAuth integration makes it easy to connect with major social platforms
  • Security best practices are automatically applied

Disadvantages

  • Rails-only library, cannot be used with other Ruby frameworks
  • Rich functionality leads to high learning costs and complexity for beginners
  • Customization requires deep knowledge of Devise and Rails
  • Configuration complexity can be excessive for simple authentication needs
  • Monolithic design creates constraints in microservice environments
  • Many default settings can cause unintended behavior

Reference Pages

Usage Examples

Basic Installation and Setup

# Gemfile - Add Devise
gem 'devise'

# OmniAuth integration (optional)
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-facebook'
gem 'omniauth-github'
gem 'omniauth-rails_csrf_protection'
# Install Devise
bundle install

# Generate Devise initializer file
rails generate devise:install

# Generate User model
rails generate devise User

# Run database migration
rails db:migrate

# Generate Devise views (for customization)
rails generate devise:views

# Generate Devise controllers (for advanced customization)
rails generate devise:controllers users
# config/initializers/devise.rb - Devise basic configuration
Devise.setup do |config|
  # Mailer sender address
  config.mailer_sender = '[email protected]'

  # Session storage key
  config.case_insensitive_keys = [:email]
  config.strip_whitespace_keys = [:email]

  # Password settings
  config.password_length = 8..128
  config.reset_password_within = 6.hours

  # Account lock settings
  config.lock_strategy = :failed_attempts
  config.unlock_strategy = :both
  config.maximum_attempts = 10
  config.unlock_in = 1.hour

  # Remember me settings
  config.remember_for = 2.weeks
  config.extend_remember_period = false

  # Session timeout settings
  config.timeout_in = 30.minutes

  # Email confirmation settings
  config.reconfirmable = true
  config.confirm_within = 3.days

  # Login attempt tracking
  config.sign_in_after_reset_password = true
  config.sign_in_after_confirmation = true

  # Sign out settings
  config.sign_out_via = :delete
end

User Model and Module Configuration

# app/models/user.rb - User model configuration
class User < ApplicationRecord
  # Devise module configuration
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :lockable, :trackable, :timeoutable,
         :omniauthable, omniauth_providers: [:google_oauth2, :facebook, :github]

  # Custom validations
  validates :first_name, presence: true, length: { maximum: 50 }
  validates :last_name, presence: true, length: { maximum: 50 }
  validates :phone_number, format: { with: /\A[\d\-\s\+\(\)]+\z/, allow_blank: true }

  # Associations
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_one_attached :avatar

  # Custom methods
  def full_name
    "#{first_name} #{last_name}"
  end

  def display_name
    full_name.present? ? full_name : email
  end

  # OmniAuth integration method
  def self.from_omniauth(auth)
    where(email: auth.info.email).first_or_create do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
      user.first_name = auth.info.first_name || auth.info.name&.split&.first
      user.last_name = auth.info.last_name || auth.info.name&.split&.last
      user.provider = auth.provider
      user.uid = auth.uid
      
      # Skip email confirmation (already confirmed via OmniAuth)
      user.skip_confirmation!
    end
  end

  # Skip password requirement (for OmniAuth users)
  def password_required?
    (provider.blank? || !password.blank?) && super
  end
end

OmniAuth Integration and Social Login

# app/controllers/users/omniauth_callbacks_controller.rb - OmniAuth callbacks
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def google_oauth2
    handle_auth("Google")
  end

  def facebook
    handle_auth("Facebook")
  end

  def github
    handle_auth("GitHub")
  end

  def failure
    set_flash_message! :alert, :failure, kind: OmniAuth::Utils.camelize(failed_strategy.name), reason: failure_message
    redirect_to new_user_session_path
  end

  private

  def handle_auth(kind)
    @user = User.from_omniauth(request.env["omniauth.auth"])

    if @user.persisted?
      flash[:notice] = I18n.t 'devise.omniauth_callbacks.success', kind: kind
      sign_in_and_redirect @user, event: :authentication
    else
      session["devise.#{kind.downcase}_data"] = request.env["omniauth.auth"].except('extra')
      flash[:alert] = @user.errors.full_messages.join("\n")
      redirect_to new_user_registration_url
    end
  end
end
# config/initializers/omniauth.rb - OmniAuth configuration
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, 
           Rails.application.credentials.google[:client_id],
           Rails.application.credentials.google[:client_secret],
           {
             scope: 'email,profile',
             prompt: 'select_account',
             image_aspect_ratio: 'square',
             image_size: 50,
             access_type: 'offline'
           }

  provider :facebook,
           Rails.application.credentials.facebook[:app_id],
           Rails.application.credentials.facebook[:app_secret],
           {
             scope: 'email,public_profile',
             info_fields: 'first_name,last_name,email,picture.width(200).height(200)'
           }

  provider :github,
           Rails.application.credentials.github[:client_id],
           Rails.application.credentials.github[:client_secret],
           scope: 'user:email'
end

# CSRF protection
OmniAuth.config.request_validation_phase = OmniAuth::AuthenticityTokenProtection.new

Custom Controllers and Routing

# config/routes.rb - Routing configuration
Rails.application.routes.draw do
  # Standard Devise routes
  devise_for :users, controllers: {
    sessions: 'users/sessions',
    registrations: 'users/registrations',
    passwords: 'users/passwords',
    confirmations: 'users/confirmations',
    unlocks: 'users/unlocks',
    omniauth_callbacks: 'users/omniauth_callbacks'
  }

  # Custom route additions
  devise_scope :user do
    get 'login', to: 'users/sessions#new'
    get 'logout', to: 'users/sessions#destroy'
    get 'signup', to: 'users/registrations#new'
  end

  root 'home#index'
end
# app/controllers/users/registrations_controller.rb - Registration controller customization
class Users::RegistrationsController < Devise::RegistrationsController
  before_action :configure_sign_up_params, only: [:create]
  before_action :configure_account_update_params, only: [:update]

  # POST /resource
  def create
    super do |resource|
      if resource.persisted?
        UserMailer.welcome_email(resource).deliver_later
      end
    end
  end

  # PUT /resource
  def update
    super
  end

  protected

  def configure_sign_up_params
    devise_parameter_sanitizer.permit(:sign_up, keys: [
      :first_name, :last_name, :phone_number, :avatar
    ])
  end

  def configure_account_update_params
    devise_parameter_sanitizer.permit(:account_update, keys: [
      :first_name, :last_name, :phone_number, :avatar
    ])
  end

  def update_resource(resource, params)
    # Allow account update without password
    if params[:password].blank? && params[:password_confirmation].blank?
      params.delete(:password)
      params.delete(:password_confirmation)
      params.delete(:current_password)
      resource.update_without_password(params)
    else
      resource.update_with_password(params)
    end
  end

  def after_sign_up_path_for(resource)
    stored_location_for(resource) || dashboard_path
  end

  def after_update_path_for(resource)
    profile_path
  end
end

View Customization and Email

<!-- app/views/devise/sessions/new.html.erb - Login form -->
<div class="authentication-wrapper">
  <div class="authentication-inner">
    <div class="card">
      <div class="card-body">
        <%= form_for(resource, as: resource_name, url: session_path(resource_name), 
                     local: true, html: { class: "authentication-form" }) do |f| %>
          <div class="form-header">
            <h4 class="card-title">Login</h4>
            <p class="card-text">Sign in to your account</p>
          </div>

          <%= render "devise/shared/error_messages", resource: resource %>

          <div class="form-group">
            <%= f.label :email, class: "form-label" %>
            <%= f.email_field :email, autofocus: true, autocomplete: "email", 
                              class: "form-control", placeholder: "Enter email address" %>
          </div>

          <div class="form-group">
            <%= f.label :password, class: "form-label" %>
            <%= f.password_field :password, autocomplete: "current-password", 
                                class: "form-control", placeholder: "Enter password" %>
          </div>

          <% if devise_mapping.rememberable? %>
            <div class="form-check">
              <%= f.check_box :remember_me, class: "form-check-input" %>
              <%= f.label :remember_me, "Keep me signed in", class: "form-check-label" %>
            </div>
          <% end %>

          <div class="form-actions">
            <%= f.submit "Login", class: "btn btn-primary btn-block" %>
          </div>
        <% end %>

        <!-- Social login -->
        <div class="divider">
          <span>or</span>
        </div>

        <div class="social-auth">
          <%= link_to "Login with Google", user_google_oauth2_omniauth_authorize_path, 
                      method: :post, class: "btn btn-google btn-block" %>
          <%= link_to "Login with Facebook", user_facebook_omniauth_authorize_path, 
                      method: :post, class: "btn btn-facebook btn-block" %>
          <%= link_to "Login with GitHub", user_github_omniauth_authorize_path, 
                      method: :post, class: "btn btn-github btn-block" %>
        </div>

        <!-- Links -->
        <div class="auth-links">
          <%= render "devise/shared/links" %>
        </div>
      </div>
    </div>
  </div>
</div>

Security and Access Control

# app/controllers/application_controller.rb - Authentication control
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  before_action :authenticate_user!
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [
      :first_name, :last_name, :phone_number
    ])
    devise_parameter_sanitizer.permit(:account_update, keys: [
      :first_name, :last_name, :phone_number
    ])
  end

  # Customize redirect destination after user authentication
  def after_sign_in_path_for(resource)
    stored_location_for(resource) || dashboard_path
  end

  def after_sign_out_path_for(resource_or_scope)
    root_path
  end

  # Permission check
  def ensure_admin
    redirect_to root_path unless current_user&.admin?
  end
end

Testing and RSpec Integration

# spec/support/devise.rb - Test configuration
RSpec.configure do |config|
  config.include Devise::Test::ControllerHelpers, type: :controller
  config.include Devise::Test::IntegrationHelpers, type: :request
  config.include Warden::Test::Helpers
end

# spec/factories/users.rb - Factory configuration
FactoryBot.define do
  factory :user do
    first_name { Faker::Name.first_name }
    last_name { Faker::Name.last_name }
    email { Faker::Internet.unique.email }
    password { 'password123' }
    password_confirmation { 'password123' }
    confirmed_at { Time.current }

    trait :unconfirmed do
      confirmed_at { nil }
    end

    trait :locked do
      locked_at { Time.current }
      failed_attempts { 10 }
    end

    trait :admin do
      admin { true }
    end

    trait :with_oauth do
      provider { 'google_oauth2' }
      uid { Faker::Number.unique.number(digits: 10) }
    end
  end
end

# spec/models/user_spec.rb - User model tests
RSpec.describe User, type: :model do
  describe 'validations' do
    it { should validate_presence_of(:email) }
    it { should validate_uniqueness_of(:email).case_insensitive }
    it { should validate_presence_of(:first_name) }
    it { should validate_presence_of(:last_name) }
  end

  describe 'devise modules' do
    it { should have_db_column(:email) }
    it { should have_db_column(:encrypted_password) }
    it { should have_db_column(:reset_password_token) }
    it { should have_db_column(:confirmation_token) }
  end

  describe '#full_name' do
    let(:user) { build(:user, first_name: 'John', last_name: 'Doe') }

    it 'returns the full name' do
      expect(user.full_name).to eq('John Doe')
    end
  end

  describe '.from_omniauth' do
    let(:auth) do
      OmniAuth::AuthHash.new({
        provider: 'google_oauth2',
        uid: '123456789',
        info: {
          email: '[email protected]',
          first_name: 'John',
          last_name: 'Doe'
        }
      })
    end

    it 'creates a new user from omniauth data' do
      expect {
        User.from_omniauth(auth)
      }.to change(User, :count).by(1)
    end

    it 'finds existing user by email' do
      existing_user = create(:user, email: auth.info.email)
      user = User.from_omniauth(auth)
      expect(user).to eq(existing_user)
    end
  end
end