リフレッシュトークン
Refresh Token ・ りふれっしゅとーくん
期限切れのアクセストークンを、再ログインなしで発行し直してもらうための長寿命の資格情報。
✦ 30秒で試す — 航海スマホアプリが、何週間もログインしたままでいられる理由この後、本物のアプリが手元の券を使うと?予想してみる概要
リフレッシュトークンは、OAuth で使われる「アクセストークンを発行し直してもらうための引換券」です。API を呼ぶたびに見せるのがアクセストークン、それが期限切れになったときに認可サーバへ差し出して「新しいアクセストークンをください」と頼むのがリフレッシュトークン、という分担になっています。アクセストークンは数分〜1時間で切れる短命な鍵、リフレッシュトークンは数日〜数か月もつ長寿命の鍵です。
この二段構えのおかげで、アクセストークンの寿命を大胆に短くしても、ユーザーは何度も認証(ログイン)し直さずに済みます。スマホアプリを数週間ぶりに開いてもログインしたままなのは、アプリが裏でリフレッシュトークンを使い、アクセストークンを静かに取り直しているからです。
一方で、リフレッシュトークンは「ログインし続けられる権利」そのものなので、盗まれたときの被害も大きくなります。そのため現在の設計では、使うたびに新しいものと取り替える「ローテーション」や、持ち主の鍵に縛りつける「送信者制約」と組み合わせて使うのが前提になっています。
なぜ生まれたか
OAuth 1.0 の時代には、アクセストークンに期限を設けないサービスも多く、一度発行されたトークンはユーザーが連携を解除するまで使えました。便利ですが、漏れれば攻撃者も無期限に使えます。しかも、リソースサーバがリクエストのたびに「このトークンはまだ有効か」を中央のデータベースに問い合わせる必要があり、API の規模が大きくなるほど負担になりました。
ここにはジレンマがあります。アクセストークンを長寿命にすれば快適ですが、漏えい時の被害が長く続きます。とくに JWT のように署名だけで検証できる自己完結型のトークンは、リソースサーバが問い合わせをしないぶん速い代わりに、発行後に取り消すのが困難です。かといって短寿命にすれば、ユーザーは数十分ごとに認証をやり直すことになります。
この板挟みを「役割の分離」で解いたのが、OAuth 2.0 の前身の OAuth WRAP(2009〜2010年)で取り入れられ、2012年の OAuth 2.0(RFC 6749)で標準になったリフレッシュトークンです。頻繁に飛び交うアクセストークンは短命にして、被害を時間で区切ります。そのうえで、長寿命のリフレッシュトークンは認可サーバにだけ見せ、認可サーバはそれを使うたびに有効かを確かめます。問い合わせは「アクセストークンが切れたときに1回」だけになり、取り消したいときは認可サーバで止めればよくなります。サーバ側で状態を持つセッションの「いつでも無効にできる」性質を、トークン方式の世界に部分的に持ち込んだ設計と言えます。
詳細
更新フローを順に追う
リフレッシュトークンが動き出すきっかけは、アクセストークンの期限切れです。
(1)クライアントが期限切れのアクセストークンで API を呼ぶと、リソースサーバは HTTP の 401 を返します。(2)クライアントは再ログインを求める代わりに、認可サーバのトークンエンドポイントへ grant_type=refresh_token と手元のリフレッシュトークンを送ります。サーバで動くアプリのようにシークレットを持てる機密クライアントは、このときもクライアント認証(シークレットや秘密鍵による署名)を添えます。リフレッシュトークンを盗まれても、それだけでは交換できないようにするためです。(3)認可サーバは、そのトークンがこのクライアントに発行されたものか、失効していないか、期限内か、(ローテーション時は)使用済みでないかを確かめ、新しいアクセストークンを返します。要求する scope は元の許可の範囲内に限られ、広げたいときはもう一度ユーザーの同意を取ります。実装では、401 を受けてから更新するほかに、期限の少し前に先回りして更新する方式もよく使われます。
アクセストークンとの分担
両者の違いは「どこに見せるか」と「どれだけ生きるか」です。アクセストークンは多くのリソースサーバに頻繁に見せるので、漏れる機会が多い代わりに短命です。リフレッシュトークンは認可サーバ1か所にだけ、たまにしか見せないので、長命にしても露出は小さく、しかも認可サーバで取り消せます。露出の多い経路には失っても被害の小さいものを流し、価値の高いものは露出を最小にする、という OAuth の認可コードフローと同じ考え方を、時間の方向に当てはめたものです。
保管場所も分担に合わせます。リフレッシュトークンは長期間のログイン権そのものなので、スマホアプリなら OS のセキュアストレージ(iOS の Keychain や Android の Keystore)に保存します。SPA で JavaScript から読める場所(localStorage など)に置くと、XSS が1つあれば丸ごと盗まれます。そこで、HttpOnly の Cookie に載せてスクリプトから読めなくする、あるいはブラウザにはトークンを一切持たせず、同じドメインのバックエンド(BFF: Backend for Frontend。APIゲートウェイの一種として置かれることもあります)にトークン管理を寄せる構成が推奨されています。
ローテーションと盗難検知
長寿命の鍵である以上、盗まれたときの備えが設計の中心になります。代表的な対策が「リフレッシュトークンローテーション」です。更新のたびに新しいリフレッシュトークンを発行し、使った古いものは使用済みにします。リフレッシュトークンは毎回使い捨てになり、同じログインから派生したトークンの列は「トークンファミリー」として台帳で追跡されます。
ローテーションの本当の価値は、盗難を検知できることにあります。攻撃者がリフレッシュトークンの写しを盗んだとしましょう。正規クライアントと攻撃者の手元には同じトークンがあります。どちらかが先に更新すると、そのトークンは使用済みになり、もう一方の手元に残るのは「旧世代」です。やがてもう一方がそれで更新しようとすると、認可サーバは「使用済みのトークンが再び出された」ことに気づきます。このときサーバには、どちらが攻撃者かは分かりません。先に更新したのが攻撃者で、いま旧世代を出したのが正規ユーザーかもしれないからです。そこで、ファミリー全体(現役の最新世代も含めて)を一括で失効させます。正規ユーザーは再ログインするだけで済み、攻撃者は更新の手段を失います。「どちらが悪いか判定できなくても、全部止めれば安全側に倒せる」という割り切りです。
下の部品で確かめてください。攻撃者の手元には、正規アプリのリフレッシュトークンの写しがあります。どちらのトークンを先に認可サーバへ運んでも、2回目で台帳のファミリーが一斉に失効します。ローテーションを切って早送りすると、攻撃者が毎晩静かに更新し続けても何も起きないことが分かります。
攻撃者が XSS などで、正規アプリのリフレッシュトークン(RT)第1世代の写しを盗みました。どちらかの RT のチップを認可サーバへドラッグして、アクセストークンを更新してみてください(タップでも運べます)。
送信者制約: 盗まれても使えなくする
ローテーションは「盗まれた後に気づく」仕組みで、検知されるまでの間は攻撃者も使えます。もう一歩進めて、盗まれても使えなくするのが「送信者制約(sender-constrained)」です。トークンを発行するときに、クライアントが持つ鍵に結びつけておきます。mTLS でクライアント証明書に束縛する方式(RFC 8705)と、クライアントが公開鍵暗号の鍵ペアを作り、リクエストのたびに秘密鍵で署名した証明を添える DPoP(RFC 9449)があります。トークンの写しだけを盗んでも、秘密鍵がなければ使えません。2025年のセキュリティのベストプラクティス(RFC 9700)は、シークレットを持てない公開クライアント(SPA やスマホアプリ)に発行するリフレッシュトークンについて、送信者制約かローテーションのどちらかを使うよう求めています。
期限と保存の設計
リフレッシュトークン自体にも期限を設けるのが普通です。「最後に使ってから14日」のように使い続ける限り延びる無操作期限と、「ログインから90日で必ず再認証」のような絶対期限を組み合わせ、利便性と「永久にログインしたまま」の防止を両立させます。絶対期限がないと、ローテーションなしで盗まれた写しは永遠に使えてしまいます。金融系など要件の厳しい領域では、両方を短く設定します。
リフレッシュトークンをどう実装するかは自由です。多くの実装は、認可サーバのデータベースに記録を持つ不透明な文字列にしますが、暗号化・署名した自己完結型の値にすることもできます。ただし、ローテーションの再利用検知や即時の失効には、どの世代が使用済みかを覚えておくサーバ側の状態が必要です。ユーザーがログアウトしたときや連携を解除したときは、トークン失効エンドポイント(RFC 7009)でリフレッシュトークンを取り消すと、その系譜からは新しいアクセストークンが出なくなります。
ログインのための OIDC では、ユーザーがその場にいなくても(オフラインで)アクセスし続けるためのリフレッシュトークンを、offline_access という scope で明示的に要求します。「メールを毎晩バックアップする」のような、ユーザー不在の処理に必要な権限だからです。同意画面でも、その旨がユーザーに示されます。
実務の落とし穴
よく知られているのが、同時更新の競合です。複数のタブや並列のリクエストがほぼ同時に同じリフレッシュトークンで更新すると、2つ目が「使用済みの再利用」とみなされ、正規ユーザーなのにファミリーごと失効してしまいます。クライアント側で更新処理を排他制御で1本にまとめる(更新中のリクエストは結果を待たせる)か、サーバ側で数秒の猶予期間だけ直前の世代を受け付けるのが定番の対処です。
もう一つは、失効の効き目の範囲を取り違えることです。ファミリーを失効させて止まるのはリフレッシュトークンの系譜で、すでに発行済みのアクセストークンが JWT のような自己完結型なら、リソースサーバは失効を知らないまま寿命まで受け付けます。アクセストークンを短命にしておくのは、この「取り消せない時間」を短くするためでもあります。また、ネットワークエラーで新しいリフレッシュトークンを受け取り損ねると、ローテーション下ではクライアントの手元に使用済みの世代しか残らず、再ログインが必要になります。受け取った新しいトークンは、使う前に確実に保存します。
試問
正規アプリの手元と、XSS などで写しを盗んだ攻撃者の手元に、同じリフレッシュトークン(RT。アクセストークンを取り直すための長寿命の引換券)があります。RT のチップを認可サーバへ運ぶと更新でき、上の台帳にその系譜(1回のログインから続く RT の世代)が並びます。ローテーション(使うたびに RT を新しい世代と取り替える)の有無を切り替えられ、いまは「なし」です。
攻撃者が XSS などで、正規アプリのリフレッシュトークン(RT)第1世代の写しを盗みました。どちらかの RT のチップを認可サーバへドラッグして、アクセストークンを更新してみてください(タップでも運べます)。 いまはローテーションなしの設定です。
