ストーリー
田
田中VPoE
アイデンティティ管理の全体像を掴んだ。次は「認証」を深掘りする。「あなたは誰か」を証明する仕組みだ
あなた
OAuth 2.0とJWTは使ったことがあります
あ
田
田中VPoE
実装したことがあるのと、セキュリティアーキテクチャとして設計できるのは別の話だ。OAuth 2.0のどのフローをどの場面で使うか、JWTの署名方式に何を選ぶか、MFAの方式をどう選定するか。これらを体系的に理解していないと、見た目は動くが穴のある認証基盤ができる
田
田中VPoE
そうだ。例えば、SPAからImplicit GrantでアクセストークンをURLフラグメントで受け取る実装。動くが、トークンがブラウザ履歴やRefererヘッダーで漏洩する。Authorization Code Flow + PKCEを使うべきだ
OAuth 2.0 / OIDC のフロー選定
フロー比較
| フロー | 用途 | セキュリティ | 推奨度 |
|---|
| Authorization Code + PKCE | SPA、モバイルアプリ | 高(認可コード + PKCE検証) | 推奨 |
| Authorization Code | サーバーサイドWebアプリ | 高(Client Secret保護) | 推奨 |
| Client Credentials | サーバー間通信(M2M) | 中(Client Secret必須) | 推奨(M2M用) |
| Implicit Grant | SPA(旧方式) | 低(トークンがURLに露出) | 非推奨 |
| Resource Owner Password | 信頼できるアプリ(旧方式) | 低(パスワードをアプリに渡す) | 非推奨 |
| Device Authorization | TVやIoTデバイス | 中 | 限定的に推奨 |
Authorization Code + PKCE フロー
Authorization Code + PKCE フロー(SPA向け推奨):
[ユーザー] ──→ [SPA]
│
│ 1. code_verifier(ランダム文字列)を生成
│ code_challenge = SHA256(code_verifier)
│
│ 2. 認可リクエスト(code_challenge付き)
↓
[認可サーバー(IdP)]
│
│ 3. ユーザー認証 + 同意
│
│ 4. 認可コード発行(フロントチャネル)
↓
[SPA]
│
│ 5. トークンリクエスト
│ (認可コード + code_verifier)
↓
[認可サーバー]
│
│ 6. code_verifier を検証
│ SHA256(code_verifier) == code_challenge ?
│
│ 7. アクセストークン + リフレッシュトークン発行
↓
[SPA] ──→ [API](アクセストークンで認証)
JWT(JSON Web Token)の設計
JWT の構造と署名方式
| 署名方式 | アルゴリズム | 鍵の種類 | 用途 |
|---|
| RS256 | RSA + SHA-256 | 非対称鍵(公開鍵 + 秘密鍵) | マイクロサービス(検証側は公開鍵のみ) |
| ES256 | ECDSA + SHA-256 | 非対称鍵(楕円曲線) | 推奨(RSA256より高速、鍵が小さい) |
| HS256 | HMAC + SHA-256 | 共有鍵 | 単一サービス(検証側にも秘密鍵必要) |
| EdDSA | Ed25519 | 非対称鍵 | 最新推奨(高速、高セキュリティ) |
JWTのセキュリティベストプラクティス
| プラクティス | 理由 | 実装 |
|---|
| 短いTTL | トークン漏洩時の影響を限定 | accessToken: 15分、refreshToken: 7日 |
| aud/issクレームの検証 | トークンの不正利用を防止 | 必ずサーバーサイドで検証 |
| algヘッダーの固定 | alg | サーバーで許可するalgを固定 |
| JWKSエンドポイント | 公開鍵のローテーション対応 | /.well-known/jwks.json |
| リフレッシュトークンローテーション | リプレイ攻撃の防止 | 使用済みリフレッシュトークンを無効化 |
// JWT検証の安全な実装例(TypeScript)
import * as jose from "jose";
const JWKS = jose.createRemoteJWKSet(
new URL("https://auth.example.com/.well-known/jwks.json")
);
async function verifyToken(token: string): Promise<jose.JWTPayload> {
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: "https://auth.example.com",
audience: "https://api.payconnect.com",
algorithms: ["ES256"], // 許可するアルゴリズムを固定
maxTokenAge: "15m", // 最大有効期間
});
return payload;
}
MFA(多要素認証)
MFA方式の比較
| 方式 | セキュリティ | ユーザー体験 | フィッシング耐性 | 推奨度 |
|---|
| パスキー(FIDO2/WebAuthn) | 最高 | 優(生体認証) | あり | 最推奨 |
| ハードウェアキー(YubiKey) | 最高 | 良(物理操作) | あり | 推奨(高セキュリティ) |
| 認証アプリ(TOTP) | 高 | 良 | なし | 推奨 |
| プッシュ通知 | 中〜高 | 優 | 限定的 | 条件付き推奨 |
| SMS OTP | 低〜中 | 良 | なし | 非推奨(SIMスワップ攻撃のリスク) |
| メール OTP | 低 | 可 | なし | 非推奨 |
パスキー(FIDO2/WebAuthn)の仕組み
パスキー認証フロー:
登録(Registration):
[ユーザー] ──→ [ブラウザ] ──→ [認証サーバー]
1. サーバーがチャレンジを送信
2. ユーザーが生体認証(指紋/顔)でデバイス上の秘密鍵を使用
3. チャレンジに署名して返送
4. サーバーが公開鍵を保存
認証(Authentication):
[ユーザー] ──→ [ブラウザ] ──→ [認証サーバー]
1. サーバーが新しいチャレンジを送信
2. ユーザーが生体認証で秘密鍵を使用
3. チャレンジに署名して返送
4. サーバーが保存済み公開鍵で署名を検証
フィッシング耐性:
- 秘密鍵はデバイスから出ない
- オリジン(ドメイン)がバインドされている
- 偽サイトではパスキーが動作しない
リスクベース認証
コンテキストに基づく認証強度の動的制御
| シグナル | 低リスク | 高リスク |
|---|
| 場所 | いつものオフィス | 海外からのアクセス |
| デバイス | 登録済みデバイス | 未知のデバイス |
| 時間帯 | 業務時間内 | 深夜 |
| 行動 | 通常の操作パターン | 大量のデータダウンロード |
| ネットワーク | 社内ネットワーク | Tor/VPN経由 |
| リスクレベル | 認証要件 |
|---|
| 低リスク | パスワード + 既知デバイス → アクセス許可 |
| 中リスク | パスワード + MFA → アクセス許可 |
| 高リスク | パスワード + MFA + セキュリティチームへの通知 |
| 非常に高リスク | アクセスブロック + セキュリティ調査 |
まとめ
| ポイント | 内容 |
|---|
| OAuth 2.0 フロー | SPA: Auth Code + PKCE、M2M: Client Credentials、Implicit: 非推奨 |
| JWT設計 | ES256/EdDSA推奨、短いTTL、aud/iss検証、alg固定 |
| MFA | パスキーが最推奨、SMS OTPは非推奨 |
| リスクベース認証 | コンテキストに応じて認証強度を動的に制御 |
チェックリスト
次のステップへ
次は「認可モデルの設計」を学びます。RBAC、ABAC、ReBAC等の認可モデルを比較し、適切な認可アーキテクチャを設計しましょう。
推定読了時間: 30分