2段階認証(2FA)仕様書
ステータス: Draft / 作成日: 2026-05-27 依存: なし(
users+sessions基盤を拡張。OAuth 拡張 と独立)
1. 概要
メール/パスワードおよび OAuth ログイン後に TOTP(Time-based One-Time Password)による第二認証を追加する。
2FA が免除されるケース:
- パスキーログインは「所持 + 生体」の多要素認証であるため、TOTP を要求しない(パスキー仕様書 参照)
テナント強制ポリシー:
- テナントオーナーは全メンバーに 2FA を強制できる
- 強制後に未設定のユーザーはログイン後に 2FA 設定を完了するまで他 API を使用不可
実装ライブラリ: totp-rs
2. データモデル
2.1 totp_credentials
2.2 recovery_codes
2.3 users テーブル変更
ALTER TABLE users ADD COLUMN totp_enabled BOOLEAN NOT NULL DEFAULT false;
totp_enabled = trueかつtotp_credentials.is_verified = trueの場合に 2FA が有効。
無効化時はtotp_enabled = false+totp_credentials+recovery_codesを削除。
2.4 tenants テーブル変更(強制ポリシー)
ALTER TABLE tenants ADD COLUMN require_2fa BOOLEAN NOT NULL DEFAULT false;
3. マイグレーション
ALTER TABLE users ADD COLUMN totp_enabled BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE tenants ADD COLUMN require_2fa BOOLEAN NOT NULL DEFAULT false;
CREATE TABLE totp_credentials (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
secret_enc TEXT NOT NULL,
is_verified BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE recovery_codes (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
code_hash VARCHAR NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_recovery_codes_user ON recovery_codes(user_id);
4. TOTP セットアップフロー
1. POST /v1/auth/2fa/totp/setup
← セッション必須(ログイン済みユーザー)
→ サーバーが TOTP シークレット(Base32, 20 バイト)を生成
→ secret_enc を DB に保存(is_verified = false)
→ otpauth:// URI と QR コード用 Base64 PNG を返す
2. ユーザーが認証アプリ(Google Authenticator・Authy・1Password 等)でQRを読み込む
3. POST /v1/auth/2fa/totp/verify-setup
Request: { "code": "123456" }
→ TOTP コード検証(前後 1 ステップ許容)
→ 成功: is_verified = true, users.totp_enabled = true
→ リカバリーコードを 10 個生成して返す(この 1 回のみ平文表示)
POST /v1/auth/2fa/totp/setup レスポンス:
{
"otpauth_uri": "otpauth://totp/TaskApp:user@example.com?secret=BASE32SECRET&issuer=TaskApp",
"qr_code_png": "data:image/png;base64,..."
}
POST /v1/auth/2fa/totp/verify-setup レスポンス(初回設定時のみリカバリーコードを返す):
{
"recovery_codes": [
"XXXX-XXXX-XXXX",
"YYYY-YYYY-YYYY",
"..."
]
}
リカバリーコードはこの 1 回しか表示されない。ユーザーに必ず保存を促すこと。
5. ログイン時の 2FA フロー(半セッション)
2FA が有効なユーザーがメール/パスワードまたは OAuth でログインした場合:
1. 第一認証成功(パスワード検証 or OAuth コールバック)
→ Redis セッションに { user_id, half_authed: true } を保存
→ 200 OK { "requires_2fa": true }
2. クライアントが POST /v1/auth/2fa/verify へ TOTP コード or リカバリーコードを送信
3. 検証成功
→ Redis セッションを { user_id, half_authed: false } に更新(完全認証)
→ 204 No Content
4. 通常の API リクエスト
→ AuthUser extractor が half_authed: true を検出したら 403 を返す
セッション状態の遷移:
[未ログイン]
│ POST /v1/login (password OK)
▼
[half_authed=true] ← この状態では /v1/auth/2fa/verify 以外の API は 403
│ POST /v1/auth/2fa/verify (code OK)
▼
[half_authed=false] ← 通常の認証済み状態
エクストラクタ責務分離設計
apps/backend/src/extractors.rs に以下 3 つのエクストラクタを定義する。既存の AuthUser を直接変更すると PAT 認証経路や既存 API に影響が及ぶため、責務を分離して段階的に追加する。
AuthUser への変更方針:
- PAT 認証の場合は
half_authedチェックをスキップ(PAT はセッションを持たず、常に完全認証扱い) - セッション認証の場合のみ
half_authedフラグを確認し、trueなら403
// AuthUser::from_request_parts 内(セッション認証パスのみ追加)
let half_authed = session.get::<bool>("half_authed").unwrap_or(false);
if half_authed {
return Err(AuthError::Forbidden);
}
HalfAuthedUser エクストラクタ:
/v1/auth/2fa/verify 専用。half_authed: true のセッションのみ受理し、false または PAT は 403。
pub struct HalfAuthedUser {
pub user_id: Uuid,
}
impl FromRequestParts<AppState> for HalfAuthedUser {
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
let session = session_from_request_parts(parts, state).await?; // PAT は不可(セッションのみ)
let user_id = session.get::<Uuid>("user_id").ok_or(AuthError::Unauthorized)?;
let half_authed = session.get::<bool>("half_authed").unwrap_or(false);
if !half_authed {
return Err(AuthError::Forbidden);
}
Ok(HalfAuthedUser { user_id })
}
}
6. TOTP 検証仕様
7. リカバリーコード
- 生成数: 10 個
- 形式:
XXXX-XXXX-XXXX(大文字英数字 12 文字、ハイフン区切り) - 保存: HMAC-SHA256(server_secret, code) のハッシュのみ DB に保存(平文は保存しない)
- 使用: 1 回使用したら
used_atを SET(再利用不可) - 再生成: 新しいコードを 10 個生成し古いものを全削除(残数が不安な場合に使用)
POST /v1/auth/2fa/verify リクエスト(TOTP またはリカバリーコードのどちらか):
{ "code": "123456" }
または
{ "recovery_code": "XXXX-XXXX-XXXX" }
8. 2FA 無効化
DELETE /v1/auth/2fa/totp
Request: { "code": "123456" } (現在の TOTP コード or リカバリーコードが必要)
処理:
- コードを検証
users.totp_enabled = falsetotp_credentialsを DELETErecovery_codesを全 DELETE
9. テナント強制ポリシー
テナントオーナーが require_2fa = true に設定した場合:
POST /v1/tenants/{tenant_id}/require-2fa
Request: { "enabled": true }
権限: テナントオーナーのみ
強制後のフロー:
ログイン(パスワード/OAuth)
│ 該当テナントで require_2fa = true
│ かつ users.totp_enabled = false
▼
half_authed セッション + { "requires_2fa_setup": true }
│ POST /v1/auth/2fa/totp/setup → verify-setup
▼
通常認証済みセッション
2FA 設定完了前はそのテナントのリソースへのアクセスを 403 で拒否する。
パスキーログインユーザーはテナント強制ポリシーの対象外(パスキー自体が MFA)。
10. API
11. セキュリティ
12. フロントエンド(Phase B)
ログイン後の 2FA 入力画面
┌─────────────────────────────────────────────┐
│ 2段階認証 │
├─────────────────────────────────────────────┤
│ 認証アプリのコードを入力してください │
│ │
│ [ _ _ _ _ _ _ ] │
│ [確認] │
│ │
│ リカバリーコードを使用 │
└─────────────────────────────────────────────┘
セキュリティ設定画面
/settings/security
┌─────────────────────────────────────────────┐
│ 2段階認証 [有効 ✅] │
├─────────────────────────────────────────────┤
│ 認証アプリ: 設定済み │
│ │
│ リカバリーコード: 残り 8 / 10 │
│ [リカバリーコードを再生成] │
│ │
│ [2段階認証を無効にする] │
└─────────────────────────────────────────────┘