Skip to content

リフレッシュトークン

リフレッシュトークン は、RP が再認証なしに新しいアクセストークンを得るために交換する長寿命の資格情報です。「ログイン状態の維持」はこの仕組みで実現されます。

このページで触れる仕様
  • RFC 6749 — OAuth 2.0 Authorization Framework(§6 refresh)
  • RFC 9700 — OAuth 2.0 Security Best Current Practice(ローテーション・再利用検知)
  • OpenID Connect Core 1.0 — §11(offline_access
用語の補足
  • ローテーション(rotation) — リフレッシュトークン交換が成功するたび、古いトークンを無効化して新しいトークンを発行します。古→新のペアは、同じログインに連なる 連鎖 を形成します。
  • 再利用検知(reuse detection) — 既にローテーション済みのリフレッシュトークンが再提示されたら、OP は盗難シグナルとして扱い、連鎖全体を無効化します。下の警告セクションを参照。
  • 猶予期間 — ローテーション直後の小さな猶予窓。前のリフレッシュトークンを再提示しても 同じ 新ペアが返る(冪等)ので、クライアント側のリトライ競合を吸収できます。
  • offline_access scope — OIDC が定める「ユーザがその場にいなくてもアプリに動き続けてほしい」を表明する scope。既定では offline TTL バケットの選択に使われ、op.WithStrictOfflineAccess() を渡すと発行条件にもなります。

ローテーションのしくみ

grant_type=refresh_token が成功するたびに、リフレッシュトークンは ローテーション します — 古いトークンは無効化され、新しいトークンが返されます。

リフレッシュトークンのローテーション: 交換のたびに提示されたトークンを無効化して同じ連鎖内で新しいトークンを発行し、ローテーション済みトークンの再提示は再利用として検知され連鎖全体を失効させる。RP / クライアントOP攻撃者トークンを更新本ライブラリrt1 を盗むPOST /token · grant_type=authorization_code200 · access_token · refresh_token: rt1access_token 有効期限切れgrant_type=refresh_token · refresh_token=rt1リフレッシュトークンをローテーションrt1 を無効化 · rt2 を発行(同じ連鎖)200 · access_token · refresh_token: rt2grant_type=refresh_token · refresh_token=rt2200 · access_token · refresh_token: rt3攻撃者が rt1 を盗むrefresh_token=rt1 を再提示(消費済み)再利用を検知 → 連鎖全体を失効RevokeChain(rt1..rt3) · refresh.replay_detected

再利用検知は連鎖全体を無効化

すでにローテーション済みのリフレッシュトークンが再提示されると、OP は「資格情報が盗まれた」シグナルとして扱い、連鎖全体を失効させます — 盗まれたトークンも、それを起点に発行された正規のトークンも、両方とも無効になります。両者とも再認証が必要です。

これは意図的な挙動です — OP が出せる「何かがおかしい」という最強のシグナルだからです。

ローテーション / 再利用検知 / 連鎖失効とは

ブログ記事ではしばしば同じ意味で使われる 3 つの用語ですが、本ライブラリでは別物です。

  • ローテーション正常系 の成功経路。grant_type=refresh_token が成功するたびに新しいリフレッシュトークンを返し、前のトークンを無効化します。既定は single-use。
  • 再利用検知 — すでにローテーション済みのリフレッシュトークンが再度送られてきた状態。漏洩、マルウェア、混乱したクライアントによる複製のいずれかでしか起こり得ません。本ライブラリは盗難として扱います。
  • 連鎖失効(family revocation とも) — 再利用検知への応答。問題のトークンと同じ系統に属するリフレッシュトークン全部が無効化されます — 正規クライアントが今使っている子孫トークンも含めて。次の正規リフレッシュは失敗し、ユーザは再認証することになり、攻撃者の盗難トークンも死にます。

RFC 9700 §2.2.2 が public client に対して要求する挙動で、本ライブラリではクライアント種別を問わずすべてのリフレッシュトークン連鎖で同じ扱いになります。

猶予期間

正規クライアントが競合状態(同じリフレッシュトークンを 2 回フェッチしてしまった、など)に陥ったとき、本来なら再利用検知に引っかかってしまいます。op.WithRefreshGracePeriod(d) でローテーション後の猶予期間を調整できます。

go
op.WithRefreshGracePeriod(2 * time.Second)

ローテーション成功から d 秒以内であれば、前のトークンを再提示しても 同じ 新トークンが返されます(idempotent)。d 秒経過後の再利用は盗難として扱われます。

猶予期間(acceptance window)とは — なぜセキュリティホールにならないか

猶予期間中、OP は前のリフレッシュトークンを まだ現役のように 受理しますが、返すのは正規クライアントに既に渡したのと 同じ 冪等な応答だけです。single-use の緩和ではありません — この期間中に OP が 新しい トークンを発行することはなく、ネットワーク不調由来のリトライを吸収するために 同じ 新ペアを再生するだけです。猶予期間が終わると、前のトークンは「ローテーション済み → 再利用 → 連鎖失効」の通常経路に戻ります。0 を渡すと完全無効化(厳密な single-use)にできます。代償は、モバイル回線で稀に偽陽性の連鎖失効が起きることです。

既定は 60 秒

WithRefreshGracePeriod を渡さない場合の猶予期間は 60 秒refresh.GraceTTLDefault)です。op.WithRefreshGracePeriod(0) で猶予期間を完全無効化(厳密な single-use)、正の値で猶予期間を明示設定できます。負値は構築時に拒否されます。

OFCS のリフレッシュトークン回帰テストはローテーションとリトライの間に約 32 秒待つため、それ以下の猶予期間に縮めると適合性が後退します。

profile.FAPI2Baselineprofile.FAPI2MessageSigning では、明示設定された非ゼロの猶予窓は op.New で拒否されます。どちらかの profile を有効にする前に、WithRefreshGracePeriod を外すか 0 にしてください。profile.FAPICIBA にはこのリフレッシュ猶予の検査は適用されません。

go
// NG: FAPI Baseline では非ゼロの猶予窓を明示できない
op.New(
  op.WithProfile(profile.FAPI2Baseline),
  op.WithRefreshGracePeriod(30*time.Second),
)

// OK: FAPI Baseline では未指定、または 0 で厳密 single-use
op.New(
  op.WithProfile(profile.FAPI2Baseline),
  op.WithRefreshGracePeriod(0),
)

TTL バケット

Optionデフォルト適用範囲
op.WithRefreshTokenTTL(d)30 日通常のリフレッシュトークン。
op.WithRefreshTokenOfflineTTL(d)WithRefreshTokenTTL を継承offline_access scope で発行されたリフレッシュトークン。

バケットを分けることで、offline_access(ログイン状態の維持)には長寿命を持たせつつ、通常のリフレッシュトークンは短いローテーション間隔を維持できます。

発行の判定

既定では、リフレッシュトークンが発行されるのは次の 2 つ が成り立つときだけです。

  1. クライアントの GrantTypesrefresh_token が含まれている。
  2. 付与された scope に openid が含まれている(本ライブラリでリフレッシュトークンは OIDC の構成要素として扱う)。

どちらか一方でも欠けると、トークンエンドポイント (/token) は access_token + id_token を返して成功扱いとなり、refresh_token フィールドは付きません —「クライアントが refresh_token grant を持っていない」場合と同じ振る舞いです。アクセストークンが切れたら、RP は再度ユーザに認証を求めることになります。

OIDC Core 1.0 §11 の既定(緩やかな)解釈では、offline_access発行の判定 ではありません。同意画面の内容とリフレッシュトークンの寿命バケット(WithRefreshTokenTTLWithRefreshTokenOfflineTTL)を切り替えるだけです。offline_access を発行条件にしたい場合は op.WithStrictOfflineAccess() を明示してください — 次のセクションを参照。

op.WithStrictOfflineAccess — OIDC Core §11 の厳格解釈

op.WithStrictOfflineAccess() を渡すと、発行とリフレッシュ交換の両方が §11 の厳格解釈に切り替わります — リフレッシュトークンは、付与された scope に offline_access が含まれているときに限り発行 / 受理されます。同意画面の内容と発行判定をビット単位で揃えたいときに選んでください。代償として、ログイン状態を維持したい RP はすべて明示的に offline_access を要求する必要があります。

このオプションは op.WithOpenIDScopeOptional と排他です(openid 自体が任意な構成では §11 に意味がないため、両方を同時指定すると op.New が拒否します)。

認証コンテキストはローテーションをまたいで保持される

リフレッシュトークンは、subject と scope だけでなく、元のログインの認証コンテキストを保持します。リフレッシュ交換が新しい id_token や JWT アクセストークンを発行するとき、OP はリフレッシュ時点ではなく、ユーザが実際に認証したときのコンテキスト — auth_timeacramr、付与された authorization_details — を再現します。そのため、ログイン時に acr_values=aal2 を要求した RP は、1 週間バックグラウンドでリフレッシュを続けたあとでも acr がその強度を反映したままになり、ステップアップの鮮度シグナルがローテーションのたびに知らぬ間にリセットされることもありません。リフレッシュレコードはこれらのフィールド(トークンの origin も含む)を永続化するので、保存された連鎖がそれらを忠実に再現します。

保存形式: ハッシュ化・定数時間

リフレッシュトークンのハンドルは opaque な bearer secret であり、所持しているだけで行使できます。OP は提示された値そのものを保存しません — ハッシュを保存し、Find / Consume では提示された値をハッシュ化して digest を引き、定数時間で比較します。これにより公開ストアの参照はハッシュのみ・タイミング非依存の形に保たれ、ストア漏洩とタイミングサイドチャネルの双方に対して堅牢になります。内部の再利用検出では、公開参照をハッシュのみに保つため、別の RefreshChainResolver 経路で保存ハンドルを解決します。ストアを自前実装する場合は、契約を満たすためにハッシュ化した id を永続化する必要があります。

監査ログ

token endpoint は op.WithAuditLogger 経由で 2 種類の slog 監査イベントを発行します。

イベント発火タイミング
op.AuditTokenIssuedauthorization_code 交換時にリフレッシュトークンを発行したとき。
op.AuditTokenRefreshedrefresh_token grant でリフレッシュトークンをローテーションしたとき。

すでにローテーション済みのトークンが再提示された(再利用検出)ときは、連鎖失効を試みる前に refresh.replay_detected イベントが発行されます。

両方とも extrasoffline_access(boolean)と ttl_bucket"offline" または "default")を持つので、SOC ダッシュボードは scope を再読することなく「ログイン状態の維持」の連鎖と通常のローテーションを区別できます。

続きはこちら