Token Exchange(RFC 8693)
現代のマイクロサービス構成では、ユーザの代理として他のサービスを呼ぶ 場面がほぼ必ずあります。frontend がトークンを取り、service A がそれを受け取り、service A が service B を呼ぶ。service A はユーザのトークンをそのまま使うべきか、新しく取り直すべきか、両方を組み合わせるべきか? RFC 8693 はこのトークン交換の通信路上の形式を定義します。
仕様は 2 つの意図を区別します。
- Impersonation(なりすまし) — service A はユーザだけを名乗るトークンを得て、ユーザ本人として 使う。service B からは
sub=aliceに見え、下流の監査ログには Alice 単独の操作として記録される。 - Delegation(委譲) — service A は
subが Alice のままで、actclaim に{sub: service-a}が入ったトークンを得る。Alice の権限が service A 経由で行使されていることが記録される。service B からはsub=alice, act={sub: service-a}に見えて、連鎖に依存するポリシー判定(「service A は Alice の振込を実行してよいが、リクエスト元が Alice 自身からの場合に限る」)が可能になる。
近年の脅威モデルでは委譲のほうが望ましいとされています。監査の連鎖が途切れず、失効を中継 service 単位で当てられ、最小権限の境界が subject 全体に広がらず actor 単位で絞り込めるためです。本ライブラリは、交換を実行した主体を必ず記録します。 act の付かないトークンができるのは自己交換だけです。自己交換とは、呼び出し元の client が subject_token の発行先 client と同じ場合です(自分のトークンを別の audience 向けに絞る場合など)。
このページで触れる仕様
用語の補足
- subject_token — 新トークンの
subを決めるトークン。通常は上流から転送されてきたユーザのアクセストークン。アクセストークン(JWT / opaque)、JWT、ID トークンを受け付け、種別はsubject_token_typeで示す。 - actor_token — actor を示す任意の 2 つ目のトークン。付いていれば、新トークンの
act.subはこの検証済みトークンのsubになる。subject_tokenと同じ subject に解決されるものは使えない。 actclaim(RFC 8693 §4.1) —subの代わりに行動している主体を記録する入れ子オブジェクト。入れ子にでき(act.act.act…)、複数ホップの呼び出しなら中継 service をすべて記録できる。本ライブラリでは最大 5 段まで。cnfの結び付け — リクエストが検証済みの DPoP proof または mTLS 証明書を持っていれば、発行トークンのcnf(RFC 7800 confirmation)はそこから設定される。subject_token自身が送信者制約付きなら、同じ鍵を提示したリクエストだけが受理される。
転送、交換、actor 付きの交換を並べて見る
actor_token かです。service B は監査ログで呼び出しの起点を把握でき、センシティブな操作では特定の service からの act を要求できます。
OP が強制すること
仕様がデプロイ側のポリシーに委ねている項目のうち、本ライブラリの RFC 8693 ハンドラはいくつかについて独自の方針を採っています。
actの連鎖は OP 側で構築する。呼び出し側が手で渡すのではない。何が入るかは下の表のとおり。呼び出し側にactを捏造させず、ポリシーのExtraClaimsでも上書きできない。- audience は正規化して許可リストで制限する。RFC 8707 の
audience/resourceパラメータは policy の評価前に正規化します。両方を省略した場合はsubject_tokenの audience を既定値として使います。得られた audience はすべて、呼び出し client に登録されたResourcesに含まれていなければなりません。Resourcesが空の client にはどの audience も発行できません(invalid_target)。 - scope は縮める方向にしか動かない。
scopeを省略すると、subject_tokenの scope を呼び出し client の登録Scopesに絞った値になります。明示したscopeは 2 つの上限の内側に収まる必要があります。client に登録されていない値やsubject_tokenの scope を超える値は、黙って落とされずinvalid_scopeで拒否されます(前者はtoken_exchange.scope_inflation_blockedも記録します)。結果が空の場合もinvalid_scopeです。 - TTL は上限で頭打ちにする。ポリシーの
GrantedTTL(未指定なら OP のアクセストークン上限)と、subject_tokenの残り寿命のうち小さいほうまで切り詰められる。長寿命トークンをさらに長いトークンへ伸ばすロンダリングはできない。subject_tokenの期限判定には OP の時計ずれ許容(leeway)が適用される。 - 返るのはアクセストークンだけ。
requested_token_typeは省略するかアクセストークンの URN のみ。リフレッシュトークンは、ポリシーがIssueRefreshToken: op.PtrBool(true)でオプトインしたときだけ発行される。ID トークンは、subject_tokenが ID トークンだった場合(またはポリシーが強制した場合)に限る。 cnfはリクエストの proof に従う。service A が交換リクエストに DPoP proof を提示すれば、発行トークンのcnf.jktは service A の鍵になる。subject_tokenがすでに結び付けられている場合は同じ鍵の提示が必要なので、盗まれた結び付き済みトークンは交換できない。
これらは組み込み側が提供する TokenExchangePolicy が呼ばれる 前 に強制されます。ポリシー側はさらに絞り込むこと(特定 audience を拒否、特定の actor 組み合わせを拒否など)はできますが、広げることはできません。要求を超える scope や audience を許可する判定結果は拒否されます。
| リクエスト | 発行トークンの act |
|---|---|
actor_token あり | { sub: <actor_token の sub>, act: <subject_token にあった以前の連鎖> } |
actor_token なし、呼び出し client が subject_token の発行先 client と異なる | { sub: <呼び出し client の client_id>, act: <以前の連鎖> } |
actor_token なし、同じ client(自己交換) | 以前の連鎖をそのまま引き継ぐ(無ければ act なし) |
5 段を超える連鎖は、ポリシーの実行前に invalid_grant で拒否されます。交換で発行されたトークンは subject_token の発行元 grant に属するため、その grant を失効させると(RevokeSubject、grant management、/end_session の後始末)交換トークンも失効します。交換で得たリフレッシュトークンも、subject が削除または停止された時点でローテーションできなくなります。client_credentials のトークンのように grant を持たない subject_token には、この系譜はありません。pairwise client に発行された ID トークンは交換できません。
resource と audience の正規化
OP は、token exchange とその他の resource indicator を扱う箇所で、URI 形式の resource / audience を同じ比較形へ正規化します。URI 形式の値は scheme と host を持つ絶対 URI でなければなりません。OP は scheme と host を小文字化し、既定ポート(http の 80、https の 443)を取り除き、path 末尾のスラッシュを取り除きます。path の大文字・小文字は保持し、query と既定でないポートは入力どおり保持します。fragment と userinfo は拒否するため、https://[email protected]/api や https://api.example.com/api#v1 は許可リストの値と一致しません。
たとえば https://API.Example.com:443/v1/?tenant=acme は https://api.example.com/v1?tenant=acme として比較されます。リクエストが audience と resource の両方を省略した場合は、subject token の audience を同じ規則で正規化して引き継ぎます。URI 形式ではない論理的な audience ラベル(URN を含みます)はバイト単位の完全一致で比較し、OP は小文字化などの書き換えを行いません。
実際に必要になるとき
Token exchange が 不要 な場合:
- 両 service を同じチームが所有し暗黙に信頼している。ユーザのトークンを複数の
audを持たせてそのまま使う。 - 下流 service が上流 actor を知る必要がない。ユーザのトークンを適切な
audで受け渡す。
Token exchange が 必要 な場合:
- 下流 service が 連鎖に誰がいるか に応じてポリシーを適用する — 「振込 service はモバイルアプリを信頼するが SMS ボットは信頼しない、Alice として呼ばれていても」。
- 連鎖にサードパーティ service が含まれていて、組織をまたぐ actor の記録が監査上の義務として必要。
- Sender-binding(DPoP / mTLS)を、ユーザではなく 呼び出し service に追従させたい。
動かしてみる
examples/33-token-exchange-delegation は frontend → service-a → service-b の連鎖を実演します。frontend が認可コードフローでユーザトークンを取得し、service-a が actor_token なしでそれを act={sub: service-a}・aud=service-b のトークンに交換し、service-b のリソースサーバ側 verifier が act.sub をたどって、act を持つトークンのみを受理します。service-a には GrantTypes に token-exchange の URN、Resources に両方の audience が登録されています。交換にはこの登録が必要です。
(cd examples/33-token-exchange-delegation && GOWORK=off go run -tags example .)example はロール別ファイルに分割されています(op.go で OP の組み立て + TokenExchangePolicy、service_a.go で中継、service_b.go でリソースサーバ、probe.go で自己検証)。
続きはこちら
- ユースケース: トークン交換の組み込み —
op.WithTokenExchangeGrant、TokenExchangePolicyの契約、audience の構成、op.PtrBool(true)による refresh 発行のオプトイン。 - カスタム grant の組み込み — token exchange は OP がルーティングする「カスタム grant_type」の同梱例。独自 URN を書く組み込み側は
op.WithCustomGrant経由で同じ形をたどる。