Devise
認証ライブラリ
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