Devise に TOTP ベースの MFA を足すとき、作業手順だけで進めると「動くけど危ない」状態を作りがちです。
この記事では Rails 8.1 / Ruby 4.0 環境で実際に組み込んだ際にハマった点と、セキュリティと DX を両立させる勘所をまとめました。
コードはシンプルな構成ですが、運用を意識したチェックリストとして読んでもらえればと思います。
目次
この記事で得られること
- Rails 8.1 / Ruby 4.0 + Devise + ROTP/RQRCode で TOTP を載せるときの設計の方針
- 時間ドリフトやログアウト動線など、見落としがちな落とし穴の回避策
- Docker 上での開発環境構築からサインアップ → QR 登録 → 2 コード検証 → ログインまでの流れ
全体の設計方針
- 「登録時の二段検証」で後戻りを防ぐ サインアップ直後に QR を表示し、現在コードと次の時間枠コードの両方を通すことで、誤登録や時間ずれを早期に弾きます。
- 秘密情報は絶対に見せない ダッシュボードなどで
otp_secretを表示しない。ログも残さない。これだけで事故の 8 割は防げます。 - ログアウトは DELETE のみ GET ログアウトは CSRF を招くので、
button_to ... method: :deleteで統一。 - 時間ドリフトは ±1 ステップに限定 「ゆるすぎ」も「厳しすぎ」も避けるため、許容ウインドウを明示して検証。
- コンテナ前提で DB/TLS を割り切る 内部通信では
ssl_mode=DISABLEDでまず通し、外部公開時にのみ TLS を整備。
開発環境(Docker)
- Ruby: 4.0.0.preview2 / Rails: 8.1.1 / Bundler: 2.7.2 / MySQL 8
bundle _2.7.2_ exec rails s -b 0.0.0.0 -p 3404DATABASE_URL例:mysql2://root:password@db:3306/mfa_development?ssl_mode=DISABLED
セットアップの最短コマンド
gem install bundler:2.7.2
bundle _2.7.2_ install
bundle _2.7.2_ exec rails db:create db:migrate
# bundle _2.7.2_ exec rails db:seed # サンプル不要なら省略bundle _2.7.2_ exec rails s -b 0.0.0.0 -p 3404実装チェックリスト
Gem
devise(認証)rotp(TOTP 生成/検証)rqrcode(QR 生成)
モデル app/models/user.rb
- Devise モジュールは
database_authenticatable,rememberable,recoverable,validatable,registerable before_createでotp_secretを自動発行otp_verified?で TOTP 検証(デフォルト drift ±1)provisioning_uriで Authenticator 登録用 URI を生成(issuer はOTP_ISSUER環境変数。既定はMfaSample)
コントローラ
Users::RegistrationsController- サインアップ後は自動ログインしないで pending →
otp_setupへ遷移 otp_setup: QR を表示verify_otp: 現在と次のコードを検証。2 つ OK なら session をクリアし自動ログインしてダッシュボードへ
- サインアップ後は自動ログインしないで pending →
Users::SessionsController- Warden でパスワード認証後に TOTP を追加検証
ApplicationController- Devise パラメータに
otp_codeを許可
- Devise パラメータに
ビュー
- サインアップ → QR 表示 → 2 コード入力 → ダッシュボード
- ダッシュボードはメールとログアウトボタンのみ。秘密情報を出さない
- ログアウトは
button_to ... method: :delete
ルーティング
devise_for :users, controllers: ...devise_scope :user配下にotp_setup/otp_verifyroot "home#index"とget "/dashboard"でシンプルに
設定まわり
config/initializers/devise.rb: sign_out は DELETE のみに統一config/initializers/otp.rb:OTP_ISSUERを環境変数で設定config/environments/development.rb:default_url_optionsをポート 3404 に合わせる
よくある落とし穴と対策
- Bundler が 4 系になって依存解決できない →
gem install bundler:2.7.2で固定し、Gemfile.lockのBUNDLED WITHも 2.7.2 へ。 - Docker build に
tmp/mysql.sockが混入 →.dockerignoreにtmpなどを追加。 - DB に繋がらない/名前解決できない → Compose のホストを
db:3306に。DATABASE_URLも合わせる。 - 自己署名 TLS で MySQL 接続失敗 → 開発用途なら
ssl_mode=DISABLEDを URL に付与。 - Devise ルーティングが競合/404 → 既定のルートを使い、独自定義を減らす。
otp_setupはdevise_scope内に置く。 - Devise mapping エラー →
config/initializers/devise.rbでdevise/orm/active_recordを require する。 - ログアウトが GET で動いて CSRF が怖い → DELETE のみに戻し、ビューを
button_toで書き換える。 - TOTP の許容時間が広すぎ/狭すぎ → 現行 + 次のコードを drift ±1 で検証する実装を選択。
フローの動かし方(ユーザー目線)
- サインアップ(メール/パスワード)
- 表示された QR を認証アプリで読み込む
- 現在の TOTP と次の時間枠の TOTP を入力して確認
- 自動ログイン → ダッシュボード
- 以後はメールアドレス + パスワード + 現行 TOTP でログイン
運用上の宿題
- MFA 再登録やリカバリコードは未実装(端末紛失時の運用を必ず設計する)
- テストは未整備(モデル/コントローラ/フローの自動テストを追加推奨)
- パスワードリセット時の MFA ポリシー未定義(組織の要件に合わせて決める)
手順をなぞるだけでは「動くけど危ない」まま終わります。
落とし穴と対策を先に押さえ、最小構成から自分たちの要件に合わせて、セキュリティと DX を両立させた MFA の導入を目指しましょう。
