認可コードは「使い捨てのチケット」だ。再利用の隙が命取りになる理由
現場でインシデント対応をしていると、OAuth 2.0の実装で「認証さえ通ればいい」という甘い考えに基づいた設計をよく目にする。特に認可コード(Authorization Code)の取り扱いは、セキュリティの防波堤だ。
認可コードは「アクセストークンを取得するための引換券」に過ぎない。もしこの券が複製できたり、一度使ったはずの券がもう一度使えたりしたらどうなるか?攻撃者は「認可コードの横取り」や「リプレイ攻撃」を仕掛け、ユーザーになりすましてトークンを奪取する。
今日は、理論的な話はそこそこに、実務で絶対に守るべき「認可コードの短寿命化とワンタイム強制」の鉄則を叩き込む。
—
攻撃者の視点:なぜ再利用が致命的なのか
攻撃者は、ブラウザの履歴、プロキシログ、あるいは中間者攻撃(MITM)を狙って、漏洩した認可コードを回収しようとする。
もし、認可コードが「10分間有効」で「複数回使用可能」な設計になっていたらどうなるか。攻撃者は正規ユーザーがアクセストークンを交換するプロセスを傍受し、正規ユーザーより先にそのコードを使って自分のアクセストークンを発行させることができる。これが「コード再利用攻撃」の典型的なシナリオだ。
これを防ぐには、以下の2点をサーバーサイドで厳格に実装する必要がある。
1. 超短寿命化: 有効期限は30秒〜1分以内とする。
2. ワンタイム強制: 使用された瞬間にデータベース上のステータスを「無効」に書き換え、二度目のアクセスを拒絶する。
—
実装の要:PHPによるセキュアな検証ロジック
「コードが正しいか」をチェックするだけでなく、「使用済みフラグ」をアトミックに管理することが肝心だ。以下は、LaravelやSymfony等を使わない、素のPHPによる概念実装だが、本質はここにある。
/
function exchangeCodeForToken($code, $clientId, $clientSecret) {
$pdo = getDbConnection();
// トランザクション開始:Race Conditionを防ぐために重要
$pdo->beginTransaction();
try {
// FOR UPDATEでロックをかけ、他のプロセスが同時に同じコードを処理できないようにする
$stmt = $pdo->prepare(“SELECT FROM oauth_codes WHERE code = ? FOR UPDATE”);
$stmt->execute([$code]);
$record = $stmt->fetch();
// 1. コードの存在確認と使用済みチェック
if (!$record || $record[‘is_used’] === 1) {
throw new Exception(“無効なコード、または再利用が試行されました”);
}
// 2. 有効期限チェック(生成から60秒以内か?)
if (strtotime($record[‘created_at’]) < (time() - 60)) {
throw new Exception("コードの有効期限が切れています");
}
// 3. 使用済みフラグを立てる(ワンタイム強制)
$update = $pdo->prepare(“UPDATE oauth_codes SET is_used = 1 WHERE code = ?”);
$update->execute([$code]);
$pdo->commit();
// ここでトークン発行処理へ進む…
return generateAccessToken();
} catch (Exception $e) {
$pdo->rollBack();
// ログには詳細を残し、クライアントには汎用的なエラーを返す
error_log($e->getMessage());
return null;
}
}
ポイント解説
FOR UPDATE: これを忘れると、並列リクエストによって同じコードが同時に通過し、二重発行されるリスク(競合状態)が生まれる。- アトミックな更新: 処理全体をトランザクションで囲み、整合性を保証する。
—
インフラレベルでの防御:WAFとログ監視
実装だけで安心するな。認可コードの漏洩や不正利用を検知するための「守り」も必要だ。
Nginxでのレート制限(Rate Limiting)
認可コードの交換エンドポイントに対し、同一IPからの短時間での連続アクセスを制限する。
nginx.conf の http ブロックなどで定義
limit_req_zone $binary_remote_addr zone=token_limit:10m rate=5r/s;
location ブロックに適用
location /oauth/token {
limit_req zone=token_limit burst=10 nodelay;
# …
}
監視の勘所
もし、is_used が既に 1 になっているコードに対してアクセスが繰り返された場合、それは攻撃の予兆だ。このログを検知したら、即座に当該クライアントIDを遮断、あるいは管理者にアラートを飛ばす仕組みを作っておけ。
—
最後に:セキュリティは「性悪説」で設計せよ
認可コードの再利用対策は、OAuth 2.0仕様(RFC 6749)でも強く推奨されていることだが、フレームワークに任せきりで中身を理解していないエンジニアが意外と多い。
- 認可コードは、一度使ったら即座に抹消する。
- データベースは必ずロックをかけて書き込む。
- 有効期限は人間が手入力して再送する暇がないほど短くする。
これらは、泥臭いけれど極めて効果的な防壁だ。便利なライブラリを使うのは良いが、その裏で何が起きているのか、DBのトランザクションがどう動いているのかを想像できるエンジニアであれ。それが、信頼されるセキュリティエンジニアへの第一歩だ。
コメント