リフレッシュトークン
リフレッシュトークン は、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_accessscope — OIDC が定める「ユーザがその場にいなくてもアプリに動き続けてほしい」を表明する scope。既定では offline TTL バケットの選択に使われ、op.WithStrictOfflineAccess()を渡すと発行条件にもなります。
ローテーションのしくみ
grant_type=refresh_token が成功するたびに、リフレッシュトークンは ローテーション します — 古いトークンは無効化され、新しいトークンが返されます。
再利用検知は連鎖全体を無効化
すでにローテーション済みのリフレッシュトークンが再提示されると、OP は「資格情報が盗まれた」シグナルとして扱い、連鎖全体を失効させます — 盗まれたトークンも、それを起点に発行された正規のトークンも、両方とも無効になります。両者とも再認証が必要です。
これは意図的な挙動です — OP が出せる「何かがおかしい」という最強のシグナルだからです。
ローテーション / 再利用検知 / 連鎖失効とは
ブログ記事ではしばしば同じ意味で使われる 3 つの用語ですが、本ライブラリでは別物です。
- ローテーション — 正常系 の成功経路。
grant_type=refresh_tokenが成功するたびに新しいリフレッシュトークンを返し、前のトークンを無効化します。既定は single-use。 - 再利用検知 — すでにローテーション済みのリフレッシュトークンが再度送られてきた状態。漏洩、マルウェア、混乱したクライアントによる複製のいずれかでしか起こり得ません。本ライブラリは盗難として扱います。
- 連鎖失効(family revocation とも) — 再利用検知への応答。問題のトークンと同じ系統に属するリフレッシュトークン全部が無効化されます — 正規クライアントが今使っている子孫トークンも含めて。次の正規リフレッシュは失敗し、ユーザは再認証することになり、攻撃者の盗難トークンも死にます。
RFC 9700 §4.14.2 は、public クライアントのリフレッシュトークンについて、送信者制約またはローテーションによる再利用検知を要求します。ローテーションで再利用を検知した場合は、現在有効なリフレッシュトークンを失効させると説明しています。本ライブラリは、クライアント種別を問わず同じ系列全体を無効にします。
猶予期間
正規クライアントが競合状態(同じリフレッシュトークンを 2 回フェッチしてしまった、など)に陥ったとき、本来なら再利用検知に引っかかってしまいます。op.WithRefreshGracePeriod(d) でローテーション後の猶予期間を調整できます。
op.WithRefreshGracePeriod(2 * time.Second)ローテーション成功から d 秒以内なら、正当な再試行で同じ後継リフレッシュトークンを回収できます。送信者制約付きのリクエストでは、OP が再試行時に提示された鍵または証明書へ結び付けたアクセストークンを新たに発行することがあります。d 秒を過ぎた再提示は再利用として扱います。
猶予期間中の再試行
OP は猶予期間中にリフレッシュトークンの系列を再度ローテーションせず、保存済みの後継リフレッシュトークンを返します。Bearer の応答はそのまま再送できます。DPoP または mTLS を使う場合は、同じ後継リフレッシュトークンと ID トークンを保ち、再試行で提示された送信者制約に合わせてアクセストークンを再発行します。保存済みの再試行用応答がない場合は、後継トークンを失効させずに invalid_grant を返します。猶予期間後に古いトークンを提示すると、再利用検知と系列の失効が起きます。期間を 0 にすると再試行を受け付けず、応答の取りこぼしを再利用と判定する可能性が高まります。
既定は 60 秒
WithRefreshGracePeriod を渡さない場合の猶予期間は 60 秒(refresh.GraceTTLDefault)です。op.WithRefreshGracePeriod(0) で猶予期間を完全無効化(厳密な single-use)し、0 以上の値で猶予期間を明示設定できます。負値は構築時に拒否されます。
OFCS のリフレッシュトークン回帰テストはローテーションとリトライの間に約 32 秒待つため、それ以下の猶予期間に縮めると適合性が後退します。
profile.FAPI2Baseline と profile.FAPI2MessageSigning では、0 秒から既定の 60 秒まで(60 秒を含む) の明示設定が許容されます。60 秒を超える猶予期間だけが op.New で拒否され、オプションを省略した場合は既定値が使われます。この上限はライブラリの設定であり、現行 FAPI 仕様への適合条件ではありません。Security Profile §5.3.2.1(9) は、例外的な状況を除く通常のリフレッシュトークンローテーションを禁止しています。profile.FAPICIBA にはこのリフレッシュ猶予の上限検査は適用されず、より長い猶予期間も設定できます。
// NG: FAPI Baseline では 60 秒を超える猶予窓は指定できない
op.New(
op.WithProfile(profile.FAPI2Baseline),
op.WithRefreshGracePeriod(61*time.Second),
)
// OK: FAPI Baseline では 60 秒(既定値)も許容される
op.New(
op.WithProfile(profile.FAPI2Baseline),
op.WithRefreshGracePeriod(60*time.Second),
)ローテーション時に現在のクライアント登録 scope を再検証する
リフレッシュトークンは連鎖の開始時に付与された scope を記録しますが、ローテーションが後から変更されたクライアント登録を迂回することはありません。提示されたトークンを消費する前に、OP は要求された scope とトークンに束縛された scope の積集合を実効 scope として求めます(要求を省略した場合は束縛済み scope を再利用します)。その実効 scope と現在のクライアントの Scopes 登録との積集合が実効 scope と一致するかを再検証し、scope ごとの AllowedClients ポリシーも検証します。発行対象の scope が現在の登録に含まれなくなっていれば /token は invalid_scope を返し、提示されたトークンは未消費のまま残ります。これにより、クライアント登録を狭めても、既存の連鎖が削除済み scope を発行し続けることはありません。
TTL バケット
| Option | デフォルト | 適用範囲 |
|---|---|---|
op.WithRefreshTokenTTL(d) | 30 日 | 通常のリフレッシュトークン。 |
op.WithRefreshTokenOfflineTTL(d) | WithRefreshTokenTTL を継承 | offline_access scope で発行されたリフレッシュトークン。 |
バケットを分けることで、offline_access(ログイン状態の維持)には長寿命を持たせつつ、通常のリフレッシュトークンは短いローテーション間隔を維持できます。
発行の判定
既定では、リフレッシュトークンの発行には次の 3 条件が必要です。
- OP が
refresh_tokenグラントを有効にしている。 - クライアントの
GrantTypesにrefresh_tokenが含まれている。 - 付与された scope に
openidが含まれている。
条件を満たさない場合でも、認可コードの交換はアクセストークンを返して成功できますが、リフレッシュトークンは付きません。ID トークンが返るのは、付与された scope に openid が含まれる場合だけです。op.WithOpenIDScopeOptional() を使う OAuth 専用の構成では openid 条件が外れ、op.WithStrictOfflineAccess() を使う構成では offline_access 条件が加わります。この 2 つのオプションは併用できません。
OIDC Core 1.0 §11 の既定(緩やかな)解釈では、offline_access は 発行の判定 ではありません。同意画面の内容とリフレッシュトークンの寿命バケット(WithRefreshTokenTTL か WithRefreshTokenOfflineTTL)を切り替えるだけです。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_time、acr、amr、付与された 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.AuditTokenIssued | authorization_code 交換時にリフレッシュトークンを発行したとき。 |
op.AuditTokenRefreshed | refresh_token grant でリフレッシュトークンをローテーションしたとき。 |
すでにローテーション済みのトークンが再提示された(再利用検出)ときは、連鎖失効を試みる前に refresh.replay_detected イベントが発行されます。
両方とも extras に offline_access(boolean)と ttl_bucket("offline" または "default")を持つので、SOC ダッシュボードは scope を再読することなく「ログイン状態の維持」の連鎖と通常のローテーションを区別できます。
続きはこちら
- ID トークン / アクセストークン / userinfo — それぞれのトークンが実際に持つ中身。
- 送信者制約 — アクセストークン(とリフレッシュトークン)をクライアント保有の鍵に結び付ける。