第15章 認証・認可システムの設計 — パスワード・セッション・MFA・監査ログ
この章で学ぶこと
- 認証 (Authentication) と認可 (Authorization) を自分の言葉で区別できる
- パスワードを bcrypt / argon2 でハッシュ化して保管する DB 設計ができる
- セッション管理・多要素認証 (MFA / TOTP) の最小限の DB 設計ができる
- アカウントロックを OWASP / NIST 800-63B 準拠で設計できる
- 監査ログを改ざん検出可能な形で残す設計ができる
第 10 章 キー概念 と 第 14 章 多対多 (RBAC) を理解していること。
認証と認可は別物
| 用語 | 意味 | 問いかけ |
|---|---|---|
| 認証 (Authentication) | 「誰か」を特定する | あなたは誰? |
| 認可 (Authorization) | 「何ができるか」を決める | この人は X をする権利があるか? |
ログイン処理 = 認証、「この記事を編集できるか」のチェック = 認可 です。両者は混同しやすいので、設計時に「今やっているのは認証 / 認可のどちらか」を意識します。
認可の DB 設計 (RBAC) は第 14 章 多対多で扱いました。本章は認証を中心に扱います。
パスワードは必ずハッシュ化して保存する
パスワードを平文で DB に保存することは絶対にしない。これは現代のすべてのセキュリティガイドラインで明示されている鉄則です (OWASP Authentication Cheat Sheet)。
bcrypt / argon2 を使う
パスワードハッシュには bcrypt か Argon2 を使います。MD5 / SHA-1 / SHA-256 は避けるべきです (高速すぎてブルートフォース攻撃に弱い)。
| アルゴリズム | 推奨度 | 特徴 |
|---|---|---|
| Argon2id | 最推奨 (OWASP Password Storage Cheat Sheet) | 2015 年 PHC 受賞、CPU + メモリ両方を消費 |
| bcrypt | 推奨 | 長く使われている実績、ライブラリが豊富 |
| scrypt | 可 | bcrypt より新しいが採用例少 |
| PBKDF2 | 可 (legacy 互換時) | FIPS 認証で必須の場面 |
| MD5 / SHA-256 | 絶対 NG | 高速すぎて総当り攻撃に脆弱 |
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL, -- bcrypt: 60 文字 / argon2: 95+ 文字
display_name VARCHAR(100) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ハッシュ化の例 (アプリ層、Node.js)
import bcrypt from 'bcrypt';
// 登録時 (cost factor は OWASP 推奨の最低 10 以上。ここでは 12 を使用)
const passwordHash = await bcrypt.hash(plainPassword, 12);
await db.query('INSERT INTO users (email, password_hash) VALUES ($1, $2)', [email, passwordHash]);
// ログイン時
const user = await db.query('SELECT password_hash FROM users WHERE email = $1', [email]);
const isValid = await bcrypt.compare(plainPassword, user.password_hash);
パスワードハッシュ化は「DB が漏洩したときに平文パスワードを守る」対策です。これとは別に、SQL インジェクションは「DB に不正にアクセスされる経路を塞ぐ」対策で、両方が必要です。
SQL インジェクション対策はパラメータ化クエリ (上の例の $1, $2 のような変数バインド) で防ぎます。文字列連結で SQL を組み立てると致命的な脆弱性になります。詳しくは OWASP SQL Injection Cheat Sheet を参照。
セッション管理
ログイン状態を維持する仕組みです。代表的なアプローチは:
| 方式 | DB 設計 | 特徴 |
|---|---|---|
| サーバセッション (Cookie + DB / Redis) | sessions テーブル or Redis | サーバ側で無効化可能 / ユーザーごとに状態を持つ |
| JWT (JSON Web Token) | DB に何も保存しない (自己完結) | ステートレス / サーバ側で即時無効化が困難 |
| Refresh Token (DB) + Access Token (JWT) | refresh_tokens テーブル | ハイブリッド、現代の主流 |
サーバセッションの DB 設計
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_sessions_user_expires ON sessions(user_id, expires_at);
ポイント:
- セッション ID は推測されにくいランダム値 (UUID v4) を使う
expires_atを超えたセッションは無効 (定期削除のバッチを仕込む)ip_address/user_agentを記録すると「不審なログイン」検知に使える
本格的なサービスでは sessions テーブルでなく Redis にセッションを保管することが多いです。読み取りが高速で、TTL (期限) も自動管理される利点があります。ただし、Redis ダウン時の影響範囲とバックアップ戦略を考慮する必要があります。
多要素認証 (MFA)
パスワードだけでなく「所持要素 (スマホ・トークン)」や「生体要素 (指紋)」を組み合わせて認証強度を上げる仕組みです。最も普及しているのは TOTP (時間ベースワンタイムパスワード、Google Authenticator など)。
CREATE TABLE user_mfa_credentials (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL CHECK (type IN ('totp', 'sms', 'webauthn')),
secret VARCHAR(255) NOT NULL, -- TOTP の場合は暗号化推奨
enabled BOOLEAN NOT NULL DEFAULT TRUE,
confirmed_at TIMESTAMPTZ, -- ユーザーが初回検証した日時
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, type)
);
CREATE TABLE user_mfa_backup_codes (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
code_hash VARCHAR(255) NOT NULL, -- バックアップコードもハッシュ化
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_mfa_backup_user ON user_mfa_backup_codes(user_id) WHERE used_at IS NULL;
ポイント:
- TOTP の
secretは DB 内で暗号化 (アプリ層で AES 等、復号鍵は別管理) - バックアップコードもハッシュ化して保管 (パスワードと同じ扱い)
- バックアップコードは「一度使ったら使えない」(
used_atを立てる)
アカウントロックの設計 (OWASP / NIST 準拠)
ブルートフォース攻撃を防ぐためにアカウントをロックする仕組みですが、安易な設計は DoS 攻撃の踏み台になります。
古典的な「N 回失敗で M 分ロック」の問題
「5 回失敗で 30 分ロック」のような設計は分かりやすい反面:
- 攻撃者が他人のアカウントを意図的にロックして使えなくする (DoS)
- ロックされた本人がログインできず、業務に支障
OWASP / NIST 推奨アプローチ
NIST SP 800-63B Rev. 4 は連続失敗を 100 回以下に制限すること (SHALL) を要求し、正規ユーザーがロックで締め出されるのを防ぐ補助手段として CAPTCHA・漸進的遅延を挙げています。OWASP ASVS V6 Authentication も同様に throttling / 漸進遅延を求めています。
CREATE TABLE login_attempts (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
ip_address INET NOT NULL,
success BOOLEAN NOT NULL,
attempted_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_agent TEXT
);
CREATE INDEX idx_login_attempts_email_time ON login_attempts(email, attempted_at DESC);
CREATE INDEX idx_login_attempts_ip_time ON login_attempts(ip_address, attempted_at DESC);
このログから「直近 1 時間の失敗回数」を集計して、CAPTCHA / 遅延 / ブロックを判定します。
-- 過去 1 時間の失敗回数 (アプリ層からの問い合わせ)
SELECT COUNT(*)
FROM login_attempts
WHERE email = $1
AND success = FALSE
AND attempted_at > NOW() - INTERVAL '1 hour';
「パスワードリセットでロック回避」を許可する設計 (OWASP 推奨) にしておくと、攻撃者の DoS 効果が下がります。正規ユーザーがパスワードリセットでログインできるなら、ロックされても困りません。
監査ログ — 改ざん検出意識
セキュリティ重要な操作 (ログイン / 権限変更 / データエクスポート) は監査ログに残します。改ざん検出を意識した設計が現代の標準です。
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(50) NOT NULL, -- 'login' / 'logout' / 'role_change' / 'data_export'
resource_type VARCHAR(50), -- 'user' / 'post' / 'role'
resource_id VARCHAR(50),
ip_address INET,
user_agent TEXT,
metadata JSONB, -- 操作の詳細 (旧値・新値など)
occurred_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_audit_user_time ON audit_logs(user_id, occurred_at DESC);
CREATE INDEX idx_audit_action_time ON audit_logs(action, occurred_at DESC);
-- 改ざんを防ぐためのカラム権限 (PostgreSQL の例)
-- 通常のアプリユーザーは INSERT のみ、UPDATE/DELETE は禁止
REVOKE UPDATE, DELETE ON audit_logs FROM app_user;
改ざん検出の強化策
| 手法 | 説明 |
|---|---|
| 追記専用 (APPEND ONLY) | UPDATE/DELETE を権限で禁止 |
| ハッシュチェーン | 各行に前の行のハッシュを含めて改ざんを検出 |
| 外部 WAL | 監査ログを別 DB / S3 にも複製 |
| 定期エクスポート | 改ざん不能なストレージ (S3 Object Lock など) に定期保管 |
業務システムでは、まず「追記専用 + 別 DB 複製」から始めるのが現実的です。法令遵守 (個人情報保護法 / SOX 法など) のレベル感に応じて段階的に強化します。
演習
次の文の正誤を判断し、間違っていれば正しい設計を答えてください。
- パスワードを保存するときは、MD5 ハッシュをかければ平文より安全になる
- JWT を使えばサーバ側のセッション管理が不要なので、ログアウトしたいときも即座に効く
- アカウントを「3 回失敗で永久ロック」にすれば、ブルートフォース攻撃は完全に防げる
- 監査ログはアプリ層から INSERT すれば、誰も書き換えられない
解答例
| 番号 | 判定 | 正しい設計 |
|---|---|---|
| 1 | ✕ | MD5 は速すぎてブルートフォースに脆弱。bcrypt / Argon2 を使う |
| 2 | ✕ | JWT はサーバが「期限切れ前に無効化」しづらい。Refresh Token + 失効リストなどの工夫が必要 |
| 3 | ✕ | 攻撃者が他人のアカウントを意図的にロックする DoS の踏み台に。漸進遅延 + CAPTCHA が安全 |
| 4 | ✕ | DB 権限で UPDATE/DELETE を許していると改ざんされる。REVOKE UPDATE, DELETE で追記専用に |
補足:
- 1: MD5/SHA-256 は GPU で秒間数十億回ハッシュ計算できるため、レインボーテーブル + 総当りで現代では破られる。bcrypt/Argon2 は意図的に遅い設計
- 2: JWT 即時無効化が必要なら、Redis の blocklist で「無効化された JWT ID」を管理する設計が多い
- 3: NIST 800-63B は連続失敗を 100 回以下に制限すること (SHALL) を要求。漸進遅延 + CAPTCHA は、正規ユーザーをロック DoS から守る補助手段という位置づけ
- 4: アプリ層が SQL Injection で侵されたら何でもできる。DB レベルで権限を絞ることで多層防御
既存の users テーブルがあるサービスに TOTP MFA を後から追加します。テーブル定義を書き、「MFA 有効ユーザーのログインフロー」を箇条書きで説明してください。
解答例
-- MFA 設定状態を users に追加
ALTER TABLE users ADD COLUMN mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE;
-- MFA 認証情報テーブル
CREATE TABLE user_mfa_credentials (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL CHECK (type IN ('totp', 'sms', 'webauthn')),
secret VARCHAR(255) NOT NULL, -- アプリ層で暗号化
enabled BOOLEAN NOT NULL DEFAULT TRUE,
confirmed_at TIMESTAMPTZ,
PRIMARY KEY (user_id, type)
);
-- バックアップコード
CREATE TABLE user_mfa_backup_codes (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
code_hash VARCHAR(255) NOT NULL,
used_at TIMESTAMPTZ
);
ログインフロー:
- ユーザーが email + password を送信
- password_hash で認証 (bcrypt.compare)
- 認証成功 →
users.mfa_enabledをチェック - MFA 無効なら → セッション発行してログイン完了
- MFA 有効なら → 一時トークン (短期セッション、MFA 完了前) を発行
- ユーザーが TOTP コードを送信
user_mfa_credentials.secretで TOTP 検証 (現在時刻 ± 30 秒)- 一致 → 通常セッションを発行
- 不一致 → バックアップコードのハッシュ照合を試す
- それも失敗なら → エラー、ログ記録
ありがちな間違い:
- MFA 失敗回数のカウントを忘れる: TOTP も総当り対象。失敗回数を
login_attemptsに記録 - TOTP の
secretを平文保存: DB 漏洩時に全 MFA が突破される。アプリ層で暗号化 - バックアップコードを平文 / MD5 で保存: パスワードと同じ扱い (bcrypt 等で hash)
- MFA 設定確認後すぐ
enabled = TRUE: 一度 TOTP コードで「ユーザーが正しく設定できた」を確認 (confirmed_atを立てる) してから有効化
まとめ
この章で学んだことを整理します。
- 認証 (誰か) と認可 (何ができるか) は別物
- パスワードは bcrypt / Argon2 でハッシュ化、SQL Injection 対策は別途必要
- セッションはサーバ側 (DB / Redis) で管理するのが安全、JWT は無効化に工夫
- MFA は TOTP が標準、
secretの暗号化とバックアップコードのハッシュ化を忘れない - アカウントロックは OWASP/NIST 準拠 (漸進遅延 + CAPTCHA + パスワードリセット救済) が安全
- 監査ログは追記専用 (UPDATE/DELETE 権限を REVOKE) + 別 DB 複製で改ざん検出
次章では、外部サービスとの認証連携 (OAuth 2.0 + OpenID Connect) を扱います。