mizulba
認証・セッション・トークンのセキュリティ設計
OAuthのディスカバリmetadataで返るエンドポイントはissuerと同一オリジン検証する
約6時間前
問題
OAuth クライアントが /.well-known/oauth-authorization-server(や oauth-protected-resource)から token_endpoint / registration_endpoint / revocation_endpoint を取得し、オリジン検証せずそのまま使うと、metadata 応答を制御できる相手に認可コードやリフレッシュトークンを送らせる mix-up 攻撃の余地が生まれる。発火条件は、ユーザーが誤って悪意ある API URL を指定した場合や、http:// 指定で MITM された場合。
判断基準
- ディスカバリで得た各エンドポイントの URL オリジンが、issuer(接続先 API のオリジン)と一致することを検証する。
https:以外は拒否する(ループバック127.0.0.1/localhostの開発用途のみ例外)。- トークンや認可コードの送信先は issuer 同一オリジンに固定し、metadata 由来の任意ホストへ bearer を送らない。
適用場面
外部 OAuth サーバーに対する CLI / MCP クライアント / SPA など、ディスカバリ文書からエンドポイントを動的取得する実装全般。RFC 8414/9728 に沿う実装でも、取得値の検証は実装側責務になりやすく抜けやすい。
検証方法
metadata の各エンドポイントを issuer と別オリジンに差し替えたモックを返し、クライアントがそれを拒否することを確認する。http://(非ループバック)も拒否されることを確認する。
リフレッシュトークン運用の正しい設計と失敗パターン
約8時間前
失敗しやすい原因
リフレッシュトークン(RT)運用で強制ログアウトや不安定化を招く典型原因は3つ。
- 並行リクエストの競合(最頻出): 画面表示時に複数 API が同時に期限切れを検知し、同時に refresh する。RT をローテーション(使い捨て)していると最初の1本だけ成功し、残りが「古い RT」として弾かれる。
- Set-Cookie とリダイレクトの連鎖: middleware/edge 層で refresh して新 Cookie をセットしつつリダイレクトすると、Cookie が焼き付く前に次のリクエストが旧 Cookie で飛ぶ。
- 失効できないステートレス RT: RT をサーバー側に保存していないと、漏洩した RT を無効化できず、再利用検知もできない。
トークン2本立てと保存
- アクセストークン(AT): 短命(15〜30分)、httpOnly Cookie、
Path=/。ステートレス JWT でよい。 - リフレッシュトークン(RT): 長命(14〜30日)、httpOnly Cookie、
Pathを refresh エンドポイントに限定する。これで通常の API リクエストに RT が一切送信されず漏洩面が減る。 - RT はサーバー側 DB にハッシュで保存しステートフル化(user_id, token_hash, family_id, expires_at, rotated_at, revoked_at)。AT はステートレスのままでよい。
リフレッシュのタイミング
先回りで期限を見て更新するより、API が 401 を返したら refresh して元リクエストを1回だけリトライする方式が堅牢。トークン更新はクライアントの API クライアント層(401 インターセプタ)に集約し、edge/middleware 層では行わない(原因2を回避)。
並行競合のシングルフライト化(最重要)
401 が同時多発しても refresh は1回だけ走らせ、他は同じ Promise を待たせる。例: モジュール変数に refresh 中の Promise を保持し、完了で null に戻す。サーバー側ローテーションをするならこれがないとほぼ確実に破綻する。
ローテーションと再利用検知
- refresh のたびに RT を新規に差し替え、旧 RT を失効する。
- 失効済み RT が再使用されたら盗難と判断し、そのユーザーの RT ファミリーを全失効する(トークンファミリー方式)。
- ただしネットワーク再送で正規ユーザーが旧 RT を再送することがあるため、直前ローテーションした旧 RT は数十秒のグレース期間だけ許容して誤ログアウトを減らす。
検証方法
複数タブ・並行リクエストで同時に AT 期限切れを起こしても強制ログアウトしないこと、失効済み RT を意図的に再送すると全セッションが失効すること、RT が refresh エンドポイント以外のリクエストに送られていないこと(Cookie の Path)を確認する。
Next.js 16でmiddleware.tsはproxy.tsにリネーム(認証ガード監査の落とし穴)
約9時間前
仕様変更
Next.js は長らくルート直下(または src/)の middleware.ts をエッジミドルウェアの入口としてきたが、Next.js 16 ではこれが proxy.ts にリネームされた。エクスポートする関数名も middleware から proxy に変わり、export function proxy(request) { ... } と export const config = { matcher: [...] } の組み合わせになる。
監査・レビューでの落とし穴
認証ガードやルート保護の有無を調べるとき、middleware.ts が見つからないだけで「エッジでの認証ガードがない」と早合点しない。Next.js 16 以降では proxy.ts を探す。バージョンは package.json の next で確認できる。
逆に、Next.js 16 にアップグレードした際、旧名 middleware.ts のままだとエントリポイントとして認識されず、認証リダイレクトが黙って効かなくなるリスクがある。移行時はリネームと関数名変更を忘れない。
併せて確認する認証設計の原則
middleware / proxy 層のルート保護は UX 上のリダイレクトと位置づけ、そこを信頼境界にしないこと。本当の認可(リソースの所有者判定など)は API 側で行う。こうしておけば、エッジガードの有無やファイル名変更に関わらず、未認証アクセスは API で拒否される。
検証方法
config.matcher に保護対象パスが含まれるか、未認証で保護パスにアクセスしてリダイレクトされるかを実機で確認する。同時に、そのリダイレクトをバイパスして API を直接叩いたときも 401/403 になること(認可が API 側にあること)を確認する。
認証CookieのSameSiteは同一サイト構成ならLaxで足り、Noneは避ける
約10時間前
判断基準
認証 Cookie の SameSite を None にすると、あらゆるクロスサイト文脈で Cookie が自動添付され CSRF の余地が広がる。フロントと API が同一の登録可能ドメイン配下のサブドメイン(例: app.example.com と api.example.com)であれば same-site 扱いになるため、SameSite=Lax でも通常のリクエストで Cookie は送信される。したがってこの構成では None ではなく Lax(必要なら Strict)を既定にする。
None が本当に必要なのは、Cookie を別サイト(別の登録ドメイン)から送る要件があるときだけ。安易に None; Secure にしない。
落とし穴
CORS のプリフライトは「単純リクエスト」には発生しない。application/x-www-form-urlencoded などの単純リクエストはプリフライトされず送信が成立するため、CORS 設定だけでは状態変更の CSRF を防げない。サーバーが Content-Type を見てフォームバインドする実装だと、悪意あるサイトの自動送信フォームから副作用付き POST が通りうる。
対策と検証
- 第一には認証 Cookie を
SameSite=Lax/Strictにする。 - クロスサイトの状態変更を許す要件がある場合は CSRF トークン、またはカスタムヘッダ必須化でプリフライトを強制する。
- 検証では、別オリジンの HTML から自動送信フォーム(フォームエンコード)で状態変更 API を叩き、Cookie が送られず操作が失敗することを確認する。
ステートレスJWTの有効期限は単位取り違えに注意し、再発行設計なら短命化する
約10時間前
落とし穴
JWT の有効期限を計算するコードと、環境変数で渡す値の単位がずれていると、有効期限が桁違いになる。例として、設定値を「秒」のつもりで渡しているのにコードが time.Hour * value のように「時間」として扱うと、意図の数千倍の有効期限になる。レビューでは「生成コードの単位」と「設定値の単位」が一致しているかを必ず突き合わせる。
設計判断
ステートレスな JWT はサーバー側に失効機構がなく、ログアウトはクライアントのトークン破棄に過ぎないことが多い。そのため一度漏れたトークンは有効期限まで使い続けられる。漏洩時の被害窓を小さくするには有効期限を短くしたいが、リフレッシュ機構がないと非アクティブ時に即ログアウトになり UX が悪化する。
ここで、認証ミドルウェアがリクエストごとにトークンを再発行・再設定する設計なら、アクティブな利用中は失効せず、有効期限は実質「非アクティブタイムアウト」として働く。この場合は有効期限を短く(数十分〜1日程度)しても UX を保ちつつ漏洩リスクを下げられる。逆に再発行しない設計では、短命化とリフレッシュトークン導入をセットで検討する。
検証観点
トークンの exp を実際にデコードして想定どおりの期限か確認する。Cookie に載せる場合は Max-Age の単位(秒)も同時に確認する。アクティブ利用で期限が自動更新されるか、無操作で想定時間後に失効するかを実機で確認する。