アクセストークンの寿命を15分に縮めた途端、15分ごとにログイン画面へ飛ばされる。この矛盾を解くために置かれるのがリフレッシュトークンです。RFC 6749が定めるこの資格情報は、リソースサーバへは一切送らず、トークンエンドポイントに対してだけ提示して新しいアクセストークンを受け取ります。そして、その更新のたびにリフレッシュトークン自体も新しい値へ差し替え、古い値を使えなくする方式がトークンローテーションです。本記事では、grant_type=refresh_tokenによる更新リクエストの組み立て方、トークンローテーションの世代交代と再利用検知の実装、画面初期化時の並行リクエストで壊れる箇所、RFC 9700(BCP 240)が公開クライアントへ課す要件、RFC 7009にもとづく失効設計、そして寿命の決め方までを実装の順序で整理します。
まとめ:リフレッシュトークンの適用条件と寿命・失効設計の結論
結論から置きます。リフレッシュトークンは「アクセストークンを短命にしても再ログインを強いない」ための仕組みであって、認証状態そのものを表すものではありません。導入すべきなのは、外部クライアントへ権限を渡す場合、クッキーが使えないネイティブアプリやCLIの場合、複数APIをまたいで同じ認可を使い回す場合の3つに限られます。フロントとAPIが同一オリジンで完結するWebアプリなら、セッションクッキーのほうが失効管理は簡潔に済みます。
トークンローテーションとは、更新のたびに新しいリフレッシュトークンを発行して直前の値を無効にするという方式です。狙いは「盗まれた値が使われた事実を検知する」ことにあり、無効化済みの値が再提示されたら同じ系列をまとめて失効させる再利用検知とセットで初めて効きます。SPAやモバイルアプリなどの公開クライアントでは、RFC 9700が送信者制約かローテーションのどちらかを必須にしています。
実装で手が止まるのは発行処理ではありません。ローテーションを入れると、画面初期化で複数のAPIが同時に401を受けた瞬間、片方が無効になった古い値を掴んで落ちる。ここはクライアント側で更新処理を1本に集約し、認可サーバ側に数十秒の再利用猶予を持たせて解きます。
もう一点、失効テーブルを持たない構成は事故になります。退職者や退会ユーザーのリフレッシュトークンを止める手段がなくなるためです。発行済みの系列をDBで管理し、RFC 7009のrevocationエンドポイントとログアウト処理を接続するところまでが最小構成になります。
リフレッシュトークンの定義とアクセストークンを短命に保つ仕組み
まず、何を持ち、どこへ送る資格情報なのかを他のトークンと切り分けます。
RFC 6749が定める発行主体と提示先を限定する原則の実装意図
リフレッシュトークンは、認可サーバが発行してクライアントへ渡す資格情報です。RFC 6749(2012年10月発行)の1.5節は、その用途を「アクセストークンが無効になった、あるいは期限切れになったときに、新しいアクセストークンを取得するための資格情報」と定義しています。アクセストークンと決定的に違うのは提示先で、リソースサーバへ送ることはありません。送り先は認可サーバのトークンエンドポイントだけです。そのトークンを最初に受け取るまでの手順は認可コードフローの実装手順と検証設計で扱っています。
この限定が効きます。APIリクエストのたびにネットワークへ流れるのは寿命15分のアクセストークンだけになり、数週間有効な資格情報はトークン更新時にしか露出しない。仮にプロキシのログにアクセストークンが残っても、被害は残り寿命の範囲で頭打ちになります。OAuth 2.0のグラント種別全体の位置づけはOAuth 2.0の認可フローと認証・認可の違いの整理を先に読むと把握しやすくなります。
アクセストークン・IDトークンとの寿命と失効の効き方の実務的な違い
3種のトークンは名前が似ているだけで、提示先も止め方も別物です。設計時に取り違えると、失効させたはずのトークンが動き続けます。
| 種別 | 提示先 | 代表的な寿命 | 失効の効き方 |
|---|---|---|---|
| アクセストークン | リソースサーバ | 5分から60分 | JWT型は即時に止まらない |
| IDトークン | クライアント本体 | 数分から1時間 | 再認証で置き換える |
| リフレッシュトークン | トークンエンドポイント | 数日から数か月 | revocationで即時に停止 |
注意したいのは1行目です。自己完結型のJWTをアクセストークンに使う構成では、サーバ側に状態を持たないぶん、発行後に取り消しても期限が来るまで検証は通ってしまいます。だからこそ寿命を5分から15分へ縮め、その短さをリフレッシュトークンで埋め合わせる。トークン形式そのものの判断材料はJWTの構造と署名検証の仕組みにまとめています。
offline_accessスコープとリフレッシュトークンが返らない構成
「レスポンスにrefresh_tokenが入っていない」という相談の大半は、不具合ではなく設定どおりの挙動です。OpenID Connect Core 1.0は、ユーザーが画面を離れた後もアクセストークンを取り直す権限を要求するスコープとしてoffline_accessを定めており、認可サーバによっては同意画面の明示的な再表示を条件に付けます。要求スコープに入れ忘れたまま更新処理だけ実装しても、返ってくるのはアクセストークンだけです。
クライアント種別の設定も確認してください。SPAやモバイルアプリとして登録した公開クライアントに対し、リフレッシュトークンの発行を既定で無効にしている認可サーバは珍しくありません。切り分けの順序は、要求スコープ、クライアント種別、認可サーバ側の発行ポリシーの3点です。この順で見れば、たいてい2手目までに原因へ届きます。
トークンローテーションとは何か:更新ごとに旧値を失効させる仕組みと対象
ここで、本記事の後半を通して使う「ローテーション」という語の範囲を固めておきます。
トークンローテーションの定義と署名鍵・APIキーのローテーションとの違い
OAuthの文脈でトークンローテーションと言えば、リフレッシュトークンのローテーションを指します。RFC 9700の4.14.2節は、認可サーバが更新要求のたびに新しいリフレッシュトークンを発行し、直前の値を無効にする方式として説明しています。攻撃者と正規のクライアントのどちらかが無効化済みの値を提示した時点で認可サーバはコピーの存在に気づけるため、その系列に属する有効なリフレッシュトークンを失効させる、という流れです。どちらが攻撃者かを判定できなくても、系列ごと止めれば被害は止まります。
同じ「ローテーション」という言葉でも、次の2つは対象も周期も別物です。1つはJWTの署名鍵のローテーションで、認可サーバがJWKSで公開する鍵を数か月単位で入れ替える運用を指します。もう1つはAPIキーやデータベースのパスワードといった静的なシークレットの定期交換です。こちらは利用者ごとの発行と失効の設計が主題になり、APIキー管理の発行・保管・失効とローテーションの実装や、自動交換の仕組みを持つAWS Secrets Managerのローテーション設定が扱う領域です。リフレッシュトークンのローテーションは、更新のたびに1回ずつ起きる点と、再利用検知という侵害検知の役割を持つ点でこの2つと区別できます。Slackのボットトークンでこの方式を有効にしたときの更新処理は、Slack Bot Tokenの取得と確認で具体例として扱っています。
1回の更新で起きる世代交代をリクエスト例とSQLの4ステップで追う手順
動きを具体の値で追います。クライアントは手持ちの第1世代の値でトークンエンドポイントへPOSTし、応答で第2世代の値を受け取る。認可サーバ側は第1世代に「回転済み」の印を付け、猶予時間を過ぎた再提示を侵害として扱います。更新リクエストと応答は次の形です(公開クライアントの例。値は説明用)。
POST /oauth2/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=rt_gen1_EXAMPLE&client_id=spa-client
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{"access_token":"at_EXAMPLE","token_type":"Bearer","expires_in":900,"refresh_token":"rt_gen2_EXAMPLE"}
認可サーバ側の保存と判定は、PostgreSQLなら次の4ステップで書けます。トークン値は平文で持たず、SHA-256のハッシュで照合します。
-- (1) 系列IDと世代番号で保存する
CREATE TABLE refresh_tokens (
token_hash CHAR(64) PRIMARY KEY,
family_id UUID NOT NULL,
generation INT NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
rotated_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL
);
-- (2) 提示された値を引く($1 = 提示値のハッシュ)
SELECT family_id, generation, revoked, rotated_at, expires_at
FROM refresh_tokens WHERE token_hash = $1;
-- (3) 未回転なら回転済みにして次世代を発行(期限は元の値を引き継ぐ)
UPDATE refresh_tokens SET rotated_at = now() WHERE token_hash = $1;
INSERT INTO refresh_tokens (token_hash, family_id, generation, expires_at)
VALUES ($2, $3, $4 + 1, $5);
-- (4) 回転済みかつ猶予30秒を過ぎた再提示なら系列ごと失効
UPDATE refresh_tokens SET revoked = TRUE WHERE family_id = $3;
ステップ(3)で期限を引き継ぐのは、ローテーションを繰り返しても認可の起点から測る絶対上限を超えさせないためです。(2)から(3)は同一トランザクション内でSELECT ... FOR UPDATEを使って行をロックし、同じ値による2本の更新が両方とも成功する競合を防いでください。
Auth0・Okta・Cognitoで異なる再利用猶予と期限の扱いの比較
自前で実装せず認可サーバ製品に任せる場合も、猶予と期限の既定値は製品ごとに違います。2026年10月時点の各社ドキュメントで確認した内容を並べます。
| 製品 | 再利用猶予の範囲 | 再利用検知と期限の扱い |
|---|---|---|
| Auth0 | 既定は無効 | 系列全体を失効させる |
| Okta | 0〜60秒(既定30秒) | 最新の値とアクセストークンを失効 |
| Amazon Cognito | 最大60秒 | 回転しても期限は延びない |
Auth0のRefresh Token Rotationは、無効化済みの値が使われると元のトークンから派生した系列をすべて失効させ、利用者に再認証を求めます。猶予は設定画面のRotation Overlap Period(Management APIではleeway)で、既定では無効です。Oktaのガイドは「Grace period for token rotation」を0〜60秒で指定でき、既定は30秒としています。
Amazon Cognitoはアプリクライアント単位でローテーションを有効にし、RefreshTokenRotationのRetryGracePeriodSecondsで最大60秒の猶予を置けます。新しい値の期限は元の値の残り期間に固定されるため、30日設定なら毎日回転しても初回サインインから30日で切れます。有効にするとREFRESH_TOKEN_AUTHフローが使えなくなり、更新はGetTokensFromRefreshTokenかトークンエンドポイントへ寄せる必要がある点に注意してください。既存アプリへ後から入れるときは、ここが改修範囲になります。
refresh_tokenグラントの実装手順と並行リクエストで壊れる箇所
更新処理そのものは数十行で書けます。落ちるのは、同じ処理が同時に2回走ったときです。
更新リクエストの組み立てと応答で確認すべき4項目および判定手順
RFC 6749の6節が定める更新の手順は次のとおりです。
- トークンエンドポイントへPOSTし、
grant_type=refresh_tokenと保持しているリフレッシュトークンを送る - 機密クライアントはあわせてクライアント認証を行う(公開クライアントは認証情報を持たないため省く)
- スコープを指定する場合、元の認可で与えられた範囲を超えない値にする(拡大は仕様上できない)
- 応答の
expires_inと、新しいリフレッシュトークンが同梱されているかを確認する
4項目めの確認を落とすと後段で詰まります。ローテーションを有効にした認可サーバは応答に新しい値を含めて返し、その瞬間に古い値を無効化するためです。受け取った新しい値で保存領域を上書きし、旧値は即座に破棄してください。
並行リクエストによる二重更新を再利用猶予で回避する実装の手順
実運用でまず踏むのがここです。ダッシュボード画面の初期化で5本のAPIを同時に呼ぶと、5本が同時に401を受け、5本が同時に更新リクエストを投げる。ローテーションが有効なら、最初の1本だけが成功し、残り4本は無効化済みの値を提示したことになります。認可サーバが再利用検知を実装していれば、正規の利用者が侵害とみなされて全トークンを失効させられます。
クライアント側の対処は単純で、更新処理を1つの非同期処理として共有し、更新中に発生した要求はその完了を待たせるキューへ入れます。サーバ側は同一トークンの再提示を10秒から30秒程度だけ許容する猶予を設けておく。この猶予を数分まで広げると盗まれたトークンの検知窓が消えるため、数十秒で止めるのが現実的な落としどころです。Oktaの既定30秒、CognitoとOktaの上限60秒という製品側の値も、この考え方と同じ帯に収まっています。
invalid_grantと401の切り分けおよび再ログイン誘導の設計
更新に失敗したとき、リトライしてよい失敗と、してはいけない失敗があります。リソースサーバが返す401はアクセストークンの期限切れで、更新すれば回復します。トークンエンドポイントが返す400のinvalid_grantは、リフレッシュトークンの期限切れ、ローテーション済み、失効済みのいずれかで、何度投げても結果は変わりません。ここでリトライループを組むと、再利用検知に引っかかって被害を広げます。
403は権限不足であり、トークンの鮮度とは無関係です。切り分けの実装は、401なら1回だけ更新して元のリクエストを再送、invalid_grantなら保存済みトークンを消してログイン画面へ、403はそのままエラー表示、という3分岐で足ります。監視のために、失効理由を認可サーバ側のログへ残す設定も入れておきましょう。
RFC 9700が公開クライアントへ課すローテーションと送信者制約
2025年に発行されたセキュリティBCPで、公開クライアントの扱いは選択肢から要件へ変わりました。
公開クライアントに課される2択とRFC 9700が示すその根拠
RFC 9700「Best Current Practice for OAuth 2.0 Security」(BCP 240・2025年1月発行)は2.2.2節で、公開クライアント向けのリフレッシュトークンについて、送信者制約とするか、ローテーションを用いるかのいずれかをMUSTとして規定しています。SPA、モバイルアプリ、デスクトップアプリはいずれも公開クライアントに該当し、この2択から逃れられません。
機密クライアントが対象外なのは、同文書が示すとおり、リフレッシュトークンを使えるのが発行先のクライアントに限られるためです。クライアント認証を通らなければトークンエンドポイントで弾かれる。サーバサイドで秘密情報を保持できる構成なら、まずクライアント認証を確実に効かせることが先決になります。
再利用検知でトークン系列ごと失効させるデータ設計と実務判定条件
ローテーションの効果を生むのは無効化そのものではなく、無効化済みの値が再び提示されたときの扱いにあります。正規のクライアントは新しい値へ差し替え済みなので、古い値の提示はコピーが存在する証拠になる。ここで同じ認可グラントから派生した系列を丸ごと失効させます。
DBの持ち方は、系列を識別するIDと世代番号、失効フラグ、有効期限の4列を最小構成にしてください。猶予時間を設けるなら、前章のSQL例のように回転した時刻の列を足します。トークン値そのものは平文で保存せず、ハッシュ化して照合します。系列単位で持てば、再利用を検知した際のクエリが「同一系列の全レコードを失効」の1文で済み、取りこぼしが起きません。
mTLSとDPoPのどちらで送信者制約を実現するかの判断基準
送信者制約は、トークンを特定の送信者に紐づけて、盗んだだけでは使えなくする手法です。選択肢は2つあります。RFC 8705(2020年2月発行)はクライアント証明書とトークンを結びつけ、RFC 9449のDPoP(2023年9月発行)はクライアントが生成した公開鍵にトークンを結びつけます。
判断基準は証明書の配布と失効を運用できるかどうか。サーバ間連携でクライアント数が限られ、証明書のライフサイクル管理をすでに回している組織ならmTLSが素直です。ブラウザやモバイルのように配布先を制御できない場合はDPoPか、実装コストを抑えるならローテーションを選びます。3つを同時に入れる必要はありません。
保管先の選定とRFC 7009にもとづく失効処理の実装範囲の線引き
寿命が長い資格情報である以上、どこに置き、どう止めるかが被害範囲を決めます。
クライアント種別ごとの保管先と漏えい時に広がる被害範囲の違い
置き場所は種別で決まります。迷う余地があるのはSPAだけです。
| クライアント種別 | 保管先 | 漏えい時の被害範囲 |
|---|---|---|
| サーバサイドWeb | サーバ側のストア | サーバ侵害時に限られる |
| ネイティブアプリ | OS標準の資格情報領域 | 端末のroot化が前提 |
| SPA | メモリのみ(永続化しない) | XSS一発で持ち出される |
SPAでリフレッシュトークンをlocalStorageへ入れる構成は避けてください。XSSが1つ通れば攻撃者の環境から任意のタイミングでアクセストークンを再発行でき、寿命を短くした意味が消えます。ローテーションを入れていても、攻撃者が正規クライアントより先に更新すれば、検知が働くのは正規側が古い値を出した後のタイミングです。長寿命のリフレッシュトークンをどうしても必要とするなら、サーバ側へ閉じ込めるBFF構成へ切り替える判断になります。切替の条件はPKCEとBFFへ切り替える判断ラインで3つに分解して整理しました。
revocationエンドポイントと連鎖失効でカバーできる範囲
RFC 7009「OAuth 2.0 Token Revocation」(2013年8月発行)は、クライアントが自らトークンを失効させるためのエンドポイントを定めています。送るのは失効対象の値と、種別を伝えるtoken_type_hintの2つ。ヒントの初期値はアクセストークンとリフレッシュトークンの2種です。失効させたトークンをリソースサーバ側でどう検知するかはトークンイントロスペクションによる検証の実装で扱っています。
連鎖の範囲も規定されています。同文書の2.1節は、リフレッシュトークンを失効させたとき、認可サーバは同じ認可グラントにもとづいて発行したアクセストークンもあわせて無効化すべき(SHOULD)としています。ただし自己完結型JWTのアクセストークンは、この無効化がリソースサーバ側の検証に届きません。ログアウト直後の数分間はAPIが通り続ける前提で、アクセストークンの寿命を5分から15分に抑えて辻褄を合わせます。失効を呼ぶべき契機は、ログアウト、パスワード変更、権限の剥奪、端末紛失の申告の4つ。セッションIDを併用する構成での止め方はセッションIDの発行から失効までの実装と突き合わせて設計してください。
寿命設計を決める3入力とリフレッシュトークンを使わない条件の整理
ここからは仕様が答えを持たない領域です。条件を置いて言い切ります。
寿命を決める3つの入力と業務種別ごとに現実的な期限設定の目安
期限の議論は「数字をいくつにするか」から入ると決まりません。入力は3つです。扱うデータの機微度、再ログインを求めてよい頻度、そして監査要件が定めるセッション最大時間。この3つが決まれば数字は自動的に絞られます。
目安を置きます。社内の参照系ツールならアクセストークン60分・リフレッシュトークン30日で運用に耐えます。一般的な業務SaaSはアクセストークン15分・リフレッシュトークン14日あたり。決済や権限変更を含む画面はアクセストークン5分・リフレッシュトークン数時間まで詰め、操作の直前に再認証を挟みます。あわせて、更新のたびに期限を延ばすスライディング方式を採る場合でも、認可の起点から測る絶対上限を別に置いてください。上限がないと、1度ログインしたセッションが永久に延命されます。Cognitoがローテーション後も元の期限を引き継ぐ設計にしているのは、この絶対上限を製品側で担保するためと読めます。
リフレッシュトークンを導入すべきでない構成と代替に置く方式の条件
ここは言い切ります。フロントエンドとAPIが同一オリジンで完結し、外部クライアントへ権限を配らないWebアプリに、リフレッシュトークンは要りません。HttpOnly・Secure・SameSiteを付けたセッションクッキーで同じ体験が作れて、失効はサーバ側のセッションストアを消すだけで完結します。そこへトークン方式を重ねると、セッションとトークンで失効経路が二重になり、ログアウト時にどちらかが残る事故を自分で作り込むことになります。
導入する条件は3つに絞れます。第一に、サードパーティを含む外部クライアントへ権限を委譲する場合。第二に、ネイティブアプリやCLIのようにクッキーを前提にできない場合。第三に、複数のAPIやマイクロサービスをまたいで同一の認可を使い回す場合です。いずれにも当てはまらないなら、セッションクッキーのまま踏みとどまってください。
事故になる失敗パターンは無期限発行と失効テーブル未実装の2つ
実装レビューで見つかると差し戻す構成が2つあります。1つは無期限のリフレッシュトークンを発行し、発行済み一覧をDBに持たない構成。退職者や退会ユーザーのトークンが生き続け、止める手段が物理的に存在しません。もう1つは、ローテーションだけ入れて再利用検知を省いた構成です。古い値の再提示を黙って拒否するだけでは、コピーが出回っている事実に気づけないままになります。
どちらも、系列単位の失効テーブルと再利用時のアラートを入れれば塞げます。会員基盤を自前で持ち、認証と権限管理の実装まで含めて相談したい場合は会員管理システム開発で、要件整理から設計・実装まで対応しています。
よくある質問
リフレッシュトークンとトークンローテーションの設計・実装で相談の多い5点をまとめます。
リフレッシュトークンの有効期限はどれくらいが妥当ですか?
扱うデータで変わります。社内の参照系なら30日、一般的な業務SaaSで14日、決済や権限変更を含む画面では数時間まで詰めるのが目安です。あわせて、更新のたびに期限を延ばすスライディング方式でも、認可の起点から測る絶対上限を必ず別に設けてください。上限がないと1回のログインが無期限に延命されます。監査要件でセッション最大時間が定められている場合は、その値が上限の天井になります。
リフレッシュトークンはlocalStorageに保存してもよいですか?
SPAでは避けてください。localStorageはJavaScriptから読めるため、XSSが1つ通った時点で攻撃者が持ち出し、自分の環境から任意のタイミングでアクセストークンを再発行できます。メモリ上に保持してタブを閉じたら失う方式が最小限の防御で、それでも寿命を長く取りたいならBFF構成へ移してサーバ側に置きます。ネイティブアプリはOS標準の資格情報保管領域を使い、平文ファイルや設定値には書きません。Flutterで実装する場合の保管領域の扱いはflutter_secure_storageとは?11.0系の暗号方式とトークン保存の判断で整理しています。
トークンローテーションとは何ですか?有効にすると同時リクエストで失敗するのはなぜですか?
トークンローテーションは、リフレッシュトークンで更新するたびに新しいリフレッシュトークンを発行し、使った値を無効にする方式です。同時リクエストで失敗するのは、複数の更新が同じ旧値を出し、2本目以降が無効化済みの値を提示した扱いになるためです。クライアントは更新処理を1つの非同期処理として共有し、更新中のAPI要求はその完了を待つキューへ入れます。サーバ側は同一トークンの再提示を10秒から30秒だけ許容する猶予を設定してください。猶予を数分に広げると、盗まれたトークンを検知する窓が実質的に消えます。
リフレッシュトークンとセッションIDはどちらを使うべきですか?
フロントとAPIが同一オリジンで完結し、外部クライアントへ権限を配らないなら、セッションクッキーで足ります。失効はサーバ側のセッションを消すだけで済み、経路が1本にまとまるためです。リフレッシュトークンを選ぶのは、外部クライアントへ権限を委譲する場合、クッキーを使えないネイティブアプリやCLIの場合、複数APIをまたいで同一の認可を使い回す場合の3つです。両方を併用するときは、ログアウト時に双方を失効させる処理を必ず接続します。
リフレッシュトークンが発行されないのはなぜですか?
設定どおりの挙動である場合がほとんどです。確認は3点の順で行います。第一に要求スコープへoffline_accessを入れているか。第二にクライアント種別が公開クライアントとして登録され、認可サーバ側で発行が既定で無効になっていないか。第三に、認可サーバのポリシーが同意画面の明示的な再表示を条件にしていないかです。この順で追えば、多くは2手目までに原因へ到達します。
関連記事
- Bearer Token(ベアラートークン)とは?読み方・Authorizationヘッダーの書き方・JWTとの違いを解説:更新後のアクセストークンをAPIへ載せる書式を確認するときに。
- APIセキュリティとは?認証・認可からレート制限まで実装者目線で解説:トークン以外の防御層とあわせて設計するときの全体像。
- IDaaSとは?SSO・IdPとの違いとSAML・OIDC・SCIM連携を実装視点で解説:認可サーバを外部サービスへ寄せる判断に。
- CIAMとは?顧客ID基盤の設計と会員DB移行の同意管理を実装視点で解説:顧客向けログイン基盤ごと設計し直す場合の入口。
- SAMLとは?認証フロー・IdP/SPの仕組みとOAuth・OIDCとの使い分けを実装視点で解説:既存のSAML基盤と併存させる構成を検討するときに。