OIDCトークンは誰が持ち誰が更新するか

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 の全体像

ログインまで(1〜8):

IdP (rellf-auth)RP サーバーRP ブラウザIdP (rellf-auth)RP サーバーRP ブラウザ(2) state, nonce, PKCE を生成(5) 認証、code を発行RP サーバーが保持。ブラウザには一切渡さない(8) id_token を検証し、RP 側のユーザーセッションを作成id_token の役目はここで終わり(1) 「ログイン」を押す(3) IdP の /oidc/authorize にリダイレクト(4) メール+パスワード入力(6) RP の callback URL に code 付きでリダイレクトcallback (code)(7) /oidc/token に code を POST(サーバー間通信)access_token + id_token + refresh_tokenopaque session cookie を発行

(8) の id_token 検証の内訳:

ログイン後のアクセスと更新(9〜10):

IdP (rellf-auth)リソース APIRP サーバーRP ブラウザIdP (rellf-auth)リソース APIRP サーバーRP ブラウザセッションから access_token を取り出すセッションを更新。ブラウザは何も知らないopt[(10) access_token が期限切れ(15分)になったら](9) session cookie を送るaccess_token を Bearer ヘッダーに載せて叩く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 側の実装パターン:

3. 期限内

4. 期限切れ

4a. 成功

4b. 失敗

APIリクエストのたびに

1. session cookie からセッションを引く

2. access_token の exp を確認

そのまま API に送る

refresh_token で更新を試みる

新トークンをセッションに保存

セッション破棄

再ログインへ

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

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