Devise

認証ライブラリRubyRuby on Rails認証セッション管理パスワードリセットOAuth

認証ライブラリ

Devise

概要

DeviseはWardenをベースとした、Ruby on Rails向けの柔軟な認証ソリューションです。2025年現在、Ruby on Railsエコシステムにおいて最も広く使用されている認証ライブラリの地位を確立しており、本格的なユーザー認証システムを迅速に構築できる包括的な機能を提供しています。データベース認証、パスワード復旧、メール確認、アカウント登録、セッション管理、トークンベース認証、OAuth統合など、現代のWebアプリケーションに必要な認証機能を網羅的にサポートします。モジュラー設計により必要な機能のみを選択でき、Railsとの深い統合により設定の簡素化と強力なカスタマイズ性を実現しています。

詳細

Devise 4.x系は、Wardenミドルウェアを基盤とした堅牢な認証システムを提供します。10の主要モジュール(Database Authenticatable、Confirmable、Recoverable、Registerable、Rememberable、Trackable、Timeoutable、Validatable、Lockable、Omniauthable)を組み合わせることで、アプリケーションの要件に応じた認証機能を構築できます。ActiveRecordとの完全統合により、ユーザーモデルに認証機能を簡単に追加でき、Railsのフォームヘルパー、バリデーション、I18n、メーラーと連携します。カスタムビュー、コントローラー、ルート設定により、ユーザーインターフェースと認証フローを完全にカスタマイズ可能です。

主な特徴

  • モジュラー設計: 必要な認証機能のみを選択して使用可能
  • Rails統合: ActiveRecord、ActionMailer、ルーティングとの完全統合
  • セキュリティ: bcryptによるパスワード暗号化、CSRF保護、セキュアセッション管理
  • OAuth対応: OmniAuthとの統合によるソーシャルログイン機能
  • 多機能: パスワードリセット、メール確認、アカウントロック、ログイン追跡
  • カスタマイズ性: ビュー、コントローラー、認証フローの完全カスタマイズ対応

メリット・デメリット

メリット

  • Rails生態系との深い統合により迅速で効率的な認証システム構築が可能
  • 豊富な機能により、複雑な認証要件にも対応可能
  • モジュラー設計により必要最小限の機能で開始し、段階的に拡張可能
  • 充実したドキュメント、コミュニティサポート、長期実績による信頼性
  • OAuth統合により主要なソーシャルプラットフォームとの連携が容易
  • セキュリティベストプラクティスが自動的に適用される

デメリット

  • Rails専用ライブラリで、他のRubyフレームワークでは使用不可
  • 豊富な機能ゆえに学習コストが高く、初心者には複雑
  • カスタマイズには深いDevise・Rails知識が必要
  • 設定の複雑性により、シンプルな認証には過剰な場合がある
  • モノリシックな設計により、マイクロサービス環境では制約がある
  • デフォルト設定が多く、意図しない動作が発生する場合がある

参考ページ

書き方の例

基本的なインストールとセットアップ

# Gemfile - Deviseの追加
gem 'devise'

# OmniAuth統合用(オプション)
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-facebook'
gem 'omniauth-github'
gem 'omniauth-rails_csrf_protection'
# Deviseのインストール
bundle install

# Devise初期化ファイル生成
rails generate devise:install

# ユーザーモデル生成
rails generate devise User

# データベースマイグレーション実行
rails db:migrate

# Deviseビュー生成(カスタマイズ用)
rails generate devise:views

# Deviseコントローラー生成(高度なカスタマイズ用)
rails generate devise:controllers users
# config/initializers/devise.rb - Devise基本設定
Devise.setup do |config|
  # メーラー送信者アドレス
  config.mailer_sender = '[email protected]'

  # セッション保存キー
  config.case_insensitive_keys = [:email]
  config.strip_whitespace_keys = [:email]

  # パスワード設定
  config.password_length = 8..128
  config.reset_password_within = 6.hours

  # アカウントロック設定
  config.lock_strategy = :failed_attempts
  config.unlock_strategy = :both
  config.maximum_attempts = 10
  config.unlock_in = 1.hour

  # Remember me設定
  config.remember_for = 2.weeks
  config.extend_remember_period = false

  # セッション期限設定
  config.timeout_in = 30.minutes

  # メール確認設定
  config.reconfirmable = true
  config.confirm_within = 3.days

  # ログイン試行追跡
  config.sign_in_after_reset_password = true
  config.sign_in_after_confirmation = true

  # サインアウト設定
  config.sign_out_via = :delete
end

ユーザーモデルとモジュール設定

# app/models/user.rb - ユーザーモデル設定
class User < ApplicationRecord
  # Deviseモジュール設定
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :confirmable, :lockable, :trackable, :timeoutable,
         :omniauthable, omniauth_providers: [:google_oauth2, :facebook, :github]

  # カスタムバリデーション
  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 }

  # アソシエーション
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_one_attached :avatar

  # カスタムメソッド
  def full_name
    "#{first_name} #{last_name}"
  end

  def display_name
    full_name.present? ? full_name : email
  end

  # OmniAuth連携用メソッド
  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
      
      # メール確認をスキップ(OmniAuthでは既に確認済み)
      user.skip_confirmation!
    end
  end

  # パスワード要求のスキップ(OmniAuthユーザー用)
  def password_required?
    (provider.blank? || !password.blank?) && super
  end
end

OmniAuth統合とソーシャルログイン

# app/controllers/users/omniauth_callbacks_controller.rb - OmniAuth コールバック
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設定
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保護
OmniAuth.config.request_validation_phase = OmniAuth::AuthenticityTokenProtection.new

カスタムコントローラーとルーティング

# config/routes.rb - ルーティング設定
Rails.application.routes.draw do
  # Devise標準ルート
  devise_for :users, controllers: {
    sessions: 'users/sessions',
    registrations: 'users/registrations',
    passwords: 'users/passwords',
    confirmations: 'users/confirmations',
    unlocks: 'users/unlocks',
    omniauth_callbacks: 'users/omniauth_callbacks'
  }

  # カスタムルート追加
  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 - 登録コントローラーカスタマイズ
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)
    # パスワード変更なしでアカウント更新を許可
    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

ビューカスタマイズとメール

<!-- app/views/devise/sessions/new.html.erb - ログインフォーム -->
<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">ログイン</h4>
            <p class="card-text">アカウントにサインインしてください</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: "メールアドレスを入力" %>
          </div>

          <div class="form-group">
            <%= f.label :password, class: "form-label" %>
            <%= f.password_field :password, autocomplete: "current-password", 
                                class: "form-control", placeholder: "パスワードを入力" %>
          </div>

          <% if devise_mapping.rememberable? %>
            <div class="form-check">
              <%= f.check_box :remember_me, class: "form-check-input" %>
              <%= f.label :remember_me, "ログイン状態を保持する", class: "form-check-label" %>
            </div>
          <% end %>

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

        <!-- ソーシャルログイン -->
        <div class="divider">
          <span>または</span>
        </div>

        <div class="social-auth">
          <%= link_to "Googleでログイン", user_google_oauth2_omniauth_authorize_path, 
                      method: :post, class: "btn btn-google btn-block" %>
          <%= link_to "Facebookでログイン", user_facebook_omniauth_authorize_path, 
                      method: :post, class: "btn btn-facebook btn-block" %>
          <%= link_to "GitHubでログイン", user_github_omniauth_authorize_path, 
                      method: :post, class: "btn btn-github btn-block" %>
        </div>

        <!-- リンク -->
        <div class="auth-links">
          <%= render "devise/shared/links" %>
        </div>
      </div>
    </div>
  </div>
</div>

セキュリティとアクセス制御

# app/controllers/application_controller.rb - 認証制御
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

  # ユーザー認証後のリダイレクト先カスタマイズ
  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

  # 権限チェック
  def ensure_admin
    redirect_to root_path unless current_user&.admin?
  end
end

テストとRSpec統合

# spec/support/devise.rb - テスト設定
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設定
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 - ユーザーモデルテスト
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: '太郎', last_name: '田中') }

    it 'returns the full name' do
      expect(user.full_name).to eq('太郎 田中')
    end
  end

  describe '.from_omniauth' do
    let(:auth) do
      OmniAuth::AuthHash.new({
        provider: 'google_oauth2',
        uid: '123456789',
        info: {
          email: '[email protected]',
          first_name: '太郎',
          last_name: '田中'
        }
      })
    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