Skip to content

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 のままで、act claim に {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 向けに絞る場合など)。

このページで触れる仕様
  • RFC 8693 — OAuth 2.0 Token Exchange
  • RFC 8707 — Resource Indicators(audience / resource で使用)
  • RFC 7800 — Confirmation(cnf)claim — 発行トークンを呼び出し actor の DPoP / mTLS 鍵に再バインド
用語の補足
  • subject_token — 新トークンの sub を決めるトークン。通常は上流から転送されてきたユーザのアクセストークン。アクセストークン(JWT / opaque)、JWT、ID トークンを受け付け、種別は subject_token_type で示す。
  • actor_token — actor を示す任意の 2 つ目のトークン。付いていれば、新トークンの act.sub はこの検証済みトークンの sub になる。subject_token と同じ subject に解決されるものは使えない。
  • act claim(RFC 8693 §4.1) — sub の代わりに行動している主体を記録する入れ子オブジェクト。入れ子にでき(act.act.act…)、複数ホップの呼び出しなら中継 service をすべて記録できる。本ライブラリでは最大 5 段まで。
  • cnf の結び付け — リクエストが検証済みの DPoP proof または mTLS 証明書を持っていれば、発行トークンの cnf(RFC 7800 confirmation)はそこから設定される。subject_token 自身が送信者制約付きなら、同じ鍵を提示したリクエストだけが受理される。

転送、交換、actor 付きの交換を並べて見る ​

audience を変えられるのは交換だけで、転送は失敗する
中継サービスがユーザの代理で下流サービスを呼ぶ 3 通りの方法。元のトークンをそのまま転送すると audience が合わず失敗します。actor_token なしの交換では、ユーザを名乗りつつ呼び出し元の client を act claim に記録したトークンが得られます。actor_token ありの交換では、act claim に actor_token の subject が記録されます。転送actor_token なしの交換actor_token ありの交換Service A中継OP認可サーバService BリソースAuthorization: Bearer <token>sub=alice · aud=service-a401 · aud が service-a で service-b ではないgrant_type=…:token-exchangesubject_token=<alice>sub=alice · aud=service-bact={ sub: service-a }Bearer sub=alice · act={ sub: service-a }act.sub は呼び出し元の client_idsubject_token=<alice>actor_token=<service-a>sub=alice · aud=service-bact={ sub: service-a }Bearer sub=alice · act={ sub: service-a }act.sub は actor_token の sub
転送は audience が合わず失敗します。どちらの交換でも「service A を経由した alice」を表すトークンが得られ、違いは actor の名前の出どころだけです。認証された呼び出し元の client か、検証済みの 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 が登録されています。交換にはこの登録が必要です。

sh
(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 で自己検証)。

続きはこちら ​