OIDCのトークンは誰が持ち、誰が更新するのか — RP視点でのトークンライフサイクル整理

ID token・access token・refresh token・session cookieの役割と期限、更新の責務を整理する。「生トークンをブラウザに出さない」設計の根拠と、IdP/RP/ブラウザの責務分界を明確にする。

そもそもの問い

自前の OIDC プロバイダーを運用していて、RP 側のトークン管理を設計する際に「localStorage vs cookie」の議論になった。しかしよく考えると、その議論自体が前提を間違えている。Confidential client なら生トークンはブラウザに出さず、サーバーサイドに保管して opaque な session cookie だけブラウザに渡すのが正道。

そこからトークンの役割・期限・更新の責務を改めて整理した。

4つのトークンの役割

session cookieid_tokenaccess_tokenrefresh_token
目的ブラウザ↔RPサーバーの紐付けユーザー属性の受け渡しAPI アクセスの認可access_token の更新
有効期限RP が決める(数時間〜数週間)短命(15分程度)短命(15分程度)長命(7日程度)
保管場所ブラウザ (httpOnly cookie)RP サーバーのメモリ(一時的)RP サーバーのセッションRP サーバーのセッション
更新方法RP が自前で延長更新しないrefresh_token で IdP に POSTrotate: 使うたびに新しいものに差し替え
無効化RP がセッション削除使い捨て期限切れを待つ鍵ローテーション or blocklist

Authorization Code Flow の全体像

[RP ブラウザ] → [RP サーバー] → [IdP (rellf-auth)]

1. ユーザーが RP で「ログイン」を押す
2. RP サーバーが state, nonce, PKCE を生成
3. ブラウザを IdP の /oidc/authorize にリダイレクト

4. ユーザーが IdP 上でメール+パスワード入力
5. IdP が認証、code を発行
6. ブラウザが RP の callback URL に code 付きでリダイレクト

7. RP サーバーが /oidc/token に code を POST(サーバー間通信)
   → access_token + id_token + refresh_token が返る
   → RP サーバーが保持。ブラウザには一切渡さない

8. RP サーバーが id_token を検証
   - 署名検証 (JWKS)
   - issuer, audience, nonce チェック
   - sub, email, groups を読み取る
   → RP 側のユーザーセッションを作成
   → opaque session cookie をブラウザに発行
   → id_token の役目はここで終わり

9. ブラウザ → RP サーバー → リソース API のアクセス時
   - ブラウザは session cookie を RP サーバーに送る
   - RP サーバーがセッションから access_token を取り出す
   - access_token を Bearer ヘッダーに載せてリソース API を叩く

10. access_token が期限切れ(15分)になったら
    - RP サーバーが refresh_token で /oidc/token に POST
    - 新しい access_token + refresh_token を受け取る
    - セッションを更新。ブラウザは何も知らない

ブラウザにトークンが一切出ない。ブラウザが触るのは opaque session cookie だけ。

ID token と access token は別物

混同しやすいが、目的が違う。

ID tokenAccess token
目的この人は誰か(認証)この API 操作を許可するか(認可)
送り先RP が受け取って中身を読むリソースサーバーに送る
中身sub, email, groups 等のユーザー属性scope, 権限、有効期限
使い方RP がセッションを作る材料(1回読んで終わり)API の Bearer ヘッダーに載せる

ID token を API の Bearer ヘッダーに入れるのは誤用。ID token は「RP がユーザー情報を受け取るための封筒」であって、API アクセスの認可には access token を使う。

なぜ access token は短命で refresh token は長命か

access token を短命にする理由

access token は Bearer として色々な場所に送られる — RP のバックエンド API、リソースサーバー、場合によっては third-party API。送信先が多い分、漏洩の機会が多い(ログ、中間プロキシ、クライアント側の XSS 等)。JWT は stateless なので漏洩しても個別に revoke できない。短命にすることで被害の時間窓を狭める。

refresh token を長命にする理由

ユーザーに毎回ログインさせないため。refresh token の送信先は IdP の /oidc/token エンドポイント1箇所だけ。OIDC の仕様上、他のどこにも送らない。送信先が限られる分、漏洩面が access token より狭い。

rotation する理由

refresh token が盗まれた場合の検出。使うたびに新しいトークンに差し替えるので、正規ユーザーと攻撃者が同じ refresh token を使うと片方が無効になる。stateful なストア(DynamoDB 等)を持てば「使用済みトークンの再利用 = 盗難」と判定して token family ごと revoke することもできる。

ライフサイクルの流れ

ログイン
  → id_token, access_token, refresh_token が発行される
  → RP が session cookie を発行(独自の期限)
  → id_token を読んでセッションに sub/email を保存 → id_token 破棄

〜15分後(access_token 切れ)
  → RP が refresh_token で更新
  → 新 access_token + 新 refresh_token を受け取る
  → session cookie はそのまま

〜7日後(refresh_token 切れ)
  → 更新不可 → RP がセッション破棄 → 再ログインへ

ユーザーが再ログインを求められる頻度は refresh_token の期限(7日)で決まり、access_token の期限(15分)はセキュリティ上の被害窓だけに影響する。

更新の責務 — IdP は何もしない

IdP(rellf-auth)は「エンドポイントを提供するだけ」で、更新のタイミング判断や実行は RP の責務。

やること誰がやる
code → token 交換RP サーバー
id_token の検証・ユーザー情報取得RP サーバー
session cookie の発行・管理RP サーバー
access_token の期限切れ検出RP サーバー(exp を見るか、API の 401 で検出)
access_token の更新実行RP サーバー(refresh_token で POST)
refresh_token の保存・差し替えRP サーバー
refresh_token 切れ → 再ログイン誘導RP サーバー
トークンの発行・署名IdP
JWKS の提供IdP

RP 側の実装パターン:

[APIリクエストのたびに]

1. session cookie からセッションを引く
2. access_token の exp を確認
3. 期限内 → そのまま API に送る
4. 期限切れ → refresh_token で更新を試みる
   4a. 成功 → 新トークンをセッションに保存 → API に送る
   4b. 失敗 → セッション破棄 → 再ログインへ

マルチプロダクトでの選択肢

RP ごとにこのフローを実装する必要があるが、全部手書きではない。

選択肢内容いつ採用
OIDC ライブラリを使うnext-auth, go-oidc, omniauthプロダクト少数のうち
共通 SDK を作って配るrellf-auth 用の認証ミドルウェアを1つ作る自社プロダクトが増えてきたら
API Gateway で集約認証処理を Gateway に寄せ、RP は後ろにいるだけマイクロサービス化が進んだら

IdP 側が OIDC 標準に準拠していれば、どの選択肢でも対応できる。

grant type の整理

grant type対応状況用途
authorization_code✅ 実装済みユーザーありのログインフロー
refresh_token✅ 実装済みaccess_token の更新
client_credentials🔜 次に実装サービス間通信(ユーザー不在)
device_code将来検討CLI, TV 等ブラウザなしデバイス
implicit❌ 非対応非推奨(OAuth 2.1 で廃止)
password (ROPC)❌ 廃止方向code flow に統一
← Back to all posts