Rails 8.1 × Ruby 4.0 で落とさないTOTP/MFA実装術

Devise に TOTP ベースの MFA を足すとき、作業手順だけで進めると「動くけど危ない」状態を作りがちです。
この記事では Rails 8.1 / Ruby 4.0 環境で実際に組み込んだ際にハマった点と、セキュリティと DX を両立させる勘所をまとめました。
コードはシンプルな構成ですが、運用を意識したチェックリストとして読んでもらえればと思います。

目次

この記事で得られること

  • Rails 8.1 / Ruby 4.0 + Devise + ROTP/RQRCode で TOTP を載せるときの設計の方針
  • 時間ドリフトやログアウト動線など、見落としがちな落とし穴の回避策
  • Docker 上での開発環境構築からサインアップ → QR 登録 → 2 コード検証 → ログインまでの流れ

全体の設計方針

  1. 「登録時の二段検証」で後戻りを防ぐ サインアップ直後に QR を表示し、現在コードと次の時間枠コードの両方を通すことで、誤登録や時間ずれを早期に弾きます。
  2. 秘密情報は絶対に見せない ダッシュボードなどで otp_secret を表示しない。ログも残さない。これだけで事故の 8 割は防げます。
  3. ログアウトは DELETE のみ GET ログアウトは CSRF を招くので、button_to ... method: :delete で統一。
  4. 時間ドリフトは ±1 ステップに限定 「ゆるすぎ」も「厳しすぎ」も避けるため、許容ウインドウを明示して検証。
  5. コンテナ前提で 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 3404
  • DATABASE_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_createotp_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 をクリアし自動ログインしてダッシュボードへ
  • Users::SessionsController
    • Warden でパスワード認証後に TOTP を追加検証
  • ApplicationController
    • Devise パラメータに otp_code を許可

ビュー

  • サインアップ → QR 表示 → 2 コード入力 → ダッシュボード
  • ダッシュボードはメールとログアウトボタンのみ。秘密情報を出さない
  • ログアウトは button_to ... method: :delete

ルーティング

  • devise_for :users, controllers: ...
  • devise_scope :user 配下に otp_setup / otp_verify
  • root "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.lockBUNDLED WITH も 2.7.2 へ。
  • Docker build に tmp/mysql.sock が混入.dockerignoretmp などを追加。
  • DB に繋がらない/名前解決できない → Compose のホストを db:3306 に。DATABASE_URL も合わせる。
  • 自己署名 TLS で MySQL 接続失敗 → 開発用途なら ssl_mode=DISABLED を URL に付与。
  • Devise ルーティングが競合/404 → 既定のルートを使い、独自定義を減らす。otp_setupdevise_scope 内に置く。
  • Devise mapping エラーconfig/initializers/devise.rbdevise/orm/active_record を require する。
  • ログアウトが GET で動いて CSRF が怖い → DELETE のみに戻し、ビューを button_to で書き換える。
  • TOTP の許容時間が広すぎ/狭すぎ → 現行 + 次のコードを drift ±1 で検証する実装を選択。

フローの動かし方(ユーザー目線)

  1. サインアップ(メール/パスワード)
  2. 表示された QR を認証アプリで読み込む
  3. 現在の TOTP と次の時間枠の TOTP を入力して確認
  4. 自動ログイン → ダッシュボード
  5. 以後はメールアドレス + パスワード + 現行 TOTP でログイン

運用上の宿題

  • MFA 再登録やリカバリコードは未実装(端末紛失時の運用を必ず設計する)
  • テストは未整備(モデル/コントローラ/フローの自動テストを追加推奨)
  • パスワードリセット時の MFA ポリシー未定義(組織の要件に合わせて決める)

手順をなぞるだけでは「動くけど危ない」まま終わります。
落とし穴と対策を先に押さえ、最小構成から自分たちの要件に合わせて、セキュリティと DX を両立させた MFA の導入を目指しましょう。

役に立ったらシェアしていただけると嬉しいです
  • URLをコピーしました!
  • URLをコピーしました!
目次