OAuth トークンの設計(アクセス/リフレッシュ/イントロスペクション)
トークンが漏れても被害を最小化できる。不透明トークンと JWT の使い分け、短命アクセス+リフレッシュローテーション、DPoP による盗難耐性、失効とイントロスペクションを原理から設計できる。
- 1.アクセストークンは不透明(参照型)か JWT(自己完結型)かを選ぶ。JWT は検証が分散できる反面ステートレスゆえ即時失効が難しく、不透明トークンはイントロスペクションで毎回検証する代わりに即時失効できる。寿命は数分〜十数分に抑える。
- 2.リフレッシュトークンは長命なので、使うたびに新しいものへ差し替えるローテーションと、旧トークン再利用を検知したら系列全体を失効させる reuse detection を組み合わせる。これにより盗難されても窃取者と正規ユーザーのどちらかが必ず弾かれる。
- 3.Bearer トークンは持っているだけで使える。DPoP や mTLS で送信者バインディングを加えると、トークンを盗んでも対応する秘密鍵がなければ使えなくなる。失効は RFC 7009、検証は RFC 7662 のイントロスペクションが標準。
トークン設計は「漏れた後」を前提に決める
OAuth 2.0 のフロー(誰がどの経路でトークンを受け取るか)は OAuth 2.0 の認可フロー内部 で扱いました。本稿はその先、発行されたトークンそのものをどう設計するかを扱います。トークンは必ず漏れうる前提で、形式・寿命・失効・送信者バインディングを組み合わせ、漏洩時の被害時間と被害範囲を最小化するのが設計の目的です。
OAuth には役割の異なる3種のトークンがあります。
- アクセストークン:API(リソースサーバー)に提示し、限定された権限(スコープ)を行使する。短命。
- リフレッシュトークン:アクセストークンが期限切れになったとき、再ログインなしで新しいアクセストークンを得るための長命トークン。認可サーバーにのみ提示する。
- ID トークン:OIDC でユーザーの認証結果を伝える JWT。API のアクセス制御には使わない。詳細は OpenID Connect の信頼モデル を参照。
混同しやすいのが「ID トークンを API 認可に使う」誤りです。ID トークンは**クライアントが受信者(aud)**で、API 宛てではありません。API に渡すのはアクセストークンだけです。
不透明トークン vs JWT
アクセストークンの形式は2系統あります。どちらを選ぶかで、検証方法・失効可能性・漏洩耐性が根本から変わります。
| 観点 | 不透明トークン(参照型) | JWT(自己完結型) |
|---|---|---|
| 中身 | 意味のないランダム文字列。実体は認可サーバーが保持 | クレームを Base64URL で詰めた署名付き文字列 |
| 検証方法 | イントロスペクション(認可サーバーへ問い合わせ) | 署名・exp・aud をローカル検証 |
| 検証の依存 | 毎回ネットワーク往復・認可サーバーが単一障害点 | オフラインで完結・分散しやすい |
| 即時失効 | 可能(サーバー側で消せば次の照会で無効) | 困難(exp が来るまで有効) |
| 情報漏洩 | 中身が無いので漏れても読めない | payload は誰でも読める(暗号化ではない) |
JWT の構造と署名検証の急所は JWT の構造と署名検証 で詳述しています。要点は、JWT は改ざんを検知するが秘匿はしないこと、検証ではアルゴリズムと鍵をサーバー側で固定することです。
選択の指針は明快です。即時失効の要件が強いなら不透明トークン+イントロスペクション、多数のマイクロサービスが独立に高速検証したいなら JWT。両者を折衷し、外向きには不透明トークンを配り、ゲートウェイで JWT に変換して内部に流す Phantom Token パターンも実務でよく使われます。これは「失効可能性」と「分散検証」の両取りを狙う設計です。
アクセストークンの寿命
アクセストークンの寿命は短いほど安全です。漏洩しても、exp(有効期限)が来れば自動的に使えなくなり、被害時間が有限になるからです。
寿命の典型値:
アクセストークン : 5〜15分 ← 漏洩の影響時間を最小化
リフレッシュトークン: 数日〜数週間 ← ローテーションで都度差し替え
ID トークン : 数分 ← 認証直後の一回限りの確認用
ここで「短くすると再認証が頻発して UX が悪化するのでは」という疑問が出ますが、それを吸収するのがリフレッシュトークンです。ユーザーは長命のリフレッシュトークンを使い、裏でアクセストークンを静かに更新するため、再ログインは発生しません。短いアクセストークン寿命のコストは、リフレッシュの仕組みが肩代わりするのがこの設計の肝です。
JWT 形式のアクセストークンは即時失効ができないため、寿命を短く保つことが事実上唯一の失効手段になります。exp を 5〜15 分にしておけば、漏洩しても影響はその窓に限定されます。逆に exp を数時間に延ばすと、漏洩トークンがその間ずっと有効になり、ステートレスゆえ止める手段がありません。
リフレッシュトークンのローテーション
リフレッシュトークンは長命なので、漏洩したときの被害が大きい。これを補うのがローテーション(rotation)です。原理は単純で、リフレッシュトークンを使うたびに新しいものへ差し替え、古いものを失効させる——つまり一度使ったリフレッシュトークンは二度と使えません。
1. クライアントが RT1 でアクセストークンを更新
2. 認可サーバーは AT_new と RT2 を返し、RT1 を失効済みにする
3. 次回はクライアントが RT2 を使う → RT3 が返り、RT2 が失効
…以降、リフレッシュのたびに番号が進む
ローテーション単体では盗難を防げませんが、**reuse detection(再利用検知)**と組み合わせると強力です。仕組みはこうです。ローテーション済みの古いトークン(RT1)が再び提示されたら、それは「正規ユーザーと窃取者の両方が同じ系列を使っている」証拠とみなし、その系列(token family)全体を即座に失効させます。
リフレッシュトークンが盗まれた場合、その後リフレッシュするのは正規ユーザーか窃取者のどちらかです。先に使った側が新トークンを得て、後から使った側は「失効済みの古いトークン」を提示することになります。サーバーはこの再利用を検知して系列全体を失効させるため、どちらが先でもいずれ必ず一方が締め出され、窃取が露見します。盗難を完全には防げなくても、長く居座らせないのが狙いです。
なお SPA など public client(client_secret を秘匿できない)でリフレッシュトークンを扱う場合、ローテーションと再利用検知は必須とされます(OAuth 2.0 Security BCP)。トークンの保管は localStorage を避け、HttpOnly Cookie やメモリ内に留めるなど、セッション管理の原理 と同じ Cookie 属性の配慮が要ります。
トークンバインディング(DPoP / mTLS)
ここまでの対策をすべて施しても、標準の Bearer トークンは「持っている者が使える」性質が残ります。盗まれたアクセストークンは、窃取者がそのまま提示するだけで通ってしまう。これを断つのが送信者制約(sender-constrained)トークンで、トークンを特定の鍵に縛り付けます。
| 方式 | 縛り方 | 適する状況 |
|---|---|---|
| Bearer(無制約) | 縛りなし。持っていれば使える | 短命トークンで許容できる一般用途 |
| DPoP(RFC 9449) | クライアントの公開鍵にトークンを紐付け、リクエストごとに秘密鍵で署名した proof を付ける | SPA・モバイル等のアプリ層バインディング |
| mTLS(RFC 8705) | クライアント証明書のハッシュをトークンに埋め、TLS 層で証明書を提示 | サーバー間・証明書管理ができる環境 |
DPoP(Demonstrating Proof-of-Possession)の原理は、アクセストークンに「このトークンはこの公開鍵の持ち主専用」という束縛(cnf クレーム=鍵のサムプリント)を埋めることです。クライアントは API 呼び出しごとに、対象 URL・HTTP メソッド・時刻を含む小さな JWT(DPoP proof)を自分の秘密鍵で署名して添えます。
リクエスト:
Authorization: DPoP <access_token> ← cnf に公開鍵のサムプリント
DPoP: <proof_jwt> ← htu/htm/iat を秘密鍵で署名
リソースサーバーの検証:
1. proof の署名を proof 内の公開鍵で検証
2. その公開鍵のサムプリントが access_token の cnf と一致するか
3. htu(URL)・htm(メソッド)が今のリクエストと一致するか、iat が新鮮か
アクセストークンを盗んでも、対応する秘密鍵はクライアント内にしか無いため、窃取者は正しい proof を作れず弾かれます。htu/htm を縛ることで、別の宛先への使い回しも防げます。mTLS は同じ目的を TLS 層で達成し、証明書のハッシュをトークンに埋めて TLS ハンドシェイクで所持を証明します。
失効とイントロスペクション
不透明トークンの真価は即時失効にあります。ログアウト・権限剥奪・漏洩検知の際、認可サーバー側で実体を消せば、次の照会で無効になります。
- 失効(RFC 7009):クライアントが
/revokeエンドポイントにトークンを送り、認可サーバーが無効化する。リフレッシュトークンを失効させると、紐づくアクセストークンも失効するのが推奨動作。 - イントロスペクション(RFC 7662):リソースサーバーが
/introspectにトークンを送り、認可サーバーがactive(有効/無効)とスコープ・有効期限・sub などのメタデータを返す。
POST /introspect
token=<opaque_access_token>
レスポンス:
{ "active": true, "scope": "read:orders", "sub": "user-123", "exp": 1750000000 }
active が false なら、失効済みか期限切れ → 即拒否
JWT アクセストークンを使いつつ即時失効も欲しい場合、純粋なステートレスは諦めます。jti(トークン ID)のブラックリストを失効リストとして持つ、ユーザーごとのトークンバージョンを上げて既発行を一括無効化する、といった最小限のサーバー側状態を併用します。詳細は JWT の失効設計 を参照。即時失効が要件なら、最初から不透明トークン+イントロスペクションの方が素直です。
イントロスペクションは毎回ネットワーク往復が発生するため、認可サーバーがボトルネックになりがちです。短い TTL のキャッシュで緩和できますが、キャッシュ期間中は失効が遅延するトレードオフがあります。「失効の即時性」と「検証の性能」は対立する——この緊張関係こそトークン設計の本質です。
スコープ設計
スコープはトークンが行使できる権限の範囲で、最小権限の原則を貫くのが基本です。
- 粒度:
read:orderswrite:ordersのように動詞+リソースで分け、過大なadmin一括スコープを避ける。 - audience(aud):トークンの宛先 API を
aud/resourceで限定し、ある API 向けトークンが別 API で使い回せないようにする(Resource Indicators, RFC 8707)。 - 同意:ユーザーが何を許可したか分かる単位で設計し、不要なスコープを既定で要求しない。
スコープは「クライアントがこのトークンで何を求めてよいか」の上限を示すだけで、**「このユーザーが実際にそのデータにアクセスしてよいか」**の判定はリソースサーバー側の認可で別途行います。スコープが read:orders でも、他人の注文を読めてよいわけではありません。スコープ(委譲の範囲)とユーザー認可(本人の権限)を混同すると、横断的なアクセス制御の穴になります。
まとめ
OAuth トークン設計は「漏れる前提」で被害の時間と範囲を絞る作業です。形式は即時失効が要るなら不透明トークン+イントロスペクション(RFC 7662)、分散高速検証なら JWT を選び、JWT は寿命を 5〜15 分に抑えることが事実上の失効手段になります。リフレッシュトークンはローテーションと再利用検知を組み合わせ、盗難されても系列ごと失効させて窃取を露見させます。Bearer の弱点は DPoP(RFC 9449)や mTLS(RFC 8705)の送信者バインディングで塞ぎ、盗んでも秘密鍵がなければ使えなくします。失効は RFC 7009、スコープは最小権限と aud 限定で範囲を絞ります。形式・寿命・失効・バインディング・スコープを「失効の即時性 対 検証の性能」というトレードオフの中で組み合わせるのが、堅牢なトークン設計の勘所です。
セキュリティ Article
OAuth トークンの設計(アクセス/リフレッシュ/イントロスペクション)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
OAuth
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 6
導入後に効く点
リフレッシュトークンは長命なので、使うたびに新しいものへ差し替えるローテーションと、旧トークン再利用を検知したら系列全体を失効させる reuse detection を組み合わせる。これにより盗難されても窃取者と正規ユーザーのどちらかが必ず弾かれる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「OAuth / アクセストークン」に近いか確認する。
- 強みである「アクセストークンは不透明(参照型)か JWT(自己完結型)かを選ぶ。JWT は検証が分散できる反面ステートレスゆえ即時失効が難しく、不透明トークンはイントロスペクションで毎回検証する代わりに即時失効できる。寿命は数分〜十数分に抑える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。