受け取ったトークンをそのまま渡してはいけない理由
一言でいうと
サービスが下流のサービスを呼ぶときのトークンは2系統あります。ユーザーと無関係な処理なら、サービス自身の名前で受け取ったclient credentialsトークンを使い、ユーザーの代わりに行う処理なら、受け取ったユーザートークンをSTSに出して、この下流専用で、誰が代わりに呼んでいるかが記された新しいトークンに交換して使います(トークン交換)。受け取ったトークンをそのまま流すのは、どちらでもありません。
なぜ必要なのか
注文APIがユーザートークン(aud=lab-api)を受け取り、そのユーザーのために在庫・決済サービスを呼ぶとします。いちばん簡単な道は、受け取ったトークンをそのままAuthorizationに載せて渡すことです。この近道には問題が3つあります。
1つ目に、そのトークンのaudは下流ではありません。下流が受け付けるにはaudの検査を切る必要があり、audを見ないサービスは他のサービス向けのトークンまで受け付けてしまいます。1か所から漏れたトークンが、チェーン全体の鍵になります。2つ目に、下流はユーザーが直接呼んだのか、サービスが代わりに呼んだのかを区別できません。監査ログに行為者が残りません。3つ目に、権限が減りません。ユーザートークンの権限が、チェーンの最後までそのまま行きます。
逆に、サービス自身のトークンだけを使うと、下流は「api-svcが呼んだ」ことしかわからず、誰のための要求なのかがわかりません。ユーザーごとの権限判断ができません。この2つの間を埋めるのが、RFC 8693 OAuth 2.0 Token Exchangeです。
どう動くのか
client credentials。RFC 6749 §4.4は、このグラントを、クライアントが自分の管理下にあるリソースにアクセスするときに使うものとし、機密クライアントだけが使えると明記しています。レスポンスにはrefresh tokenを入れないことが推奨されています(§4.4.3)。秘密を持つクライアントは、いつでも取得し直せるからです。このラボのKeycloakでapi-svcで受け取ると、ユーザーの代わりにservice-account-api-svcというサービスアカウントが主体になります。
トークン交換。リクエストはトークンエンドポイントに次のように送ります。
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
subject_token=<사용자 토큰> subject_token_type=...:token-type:access_token (필수)
actor_token=<서비스 토큰> actor_token_type=...:token-type:access_token (선택)
audience=orders-api (선택)
RFCは2つの意味を区別します。impersonationは、AがBと区別できない形でBになることで、delegationは、Aが自分の身元を保ったままBを代理することです。委任なら、新しいトークンにactクレームで現在の行為者を残します。入れ子のactは以前の行為者で、RFCは、アクセス制御には最上位のクレームと最も外側のactだけを使い、残りは情報としてだけ見るよう述べています。レスポンスにはissued_token_typeが必須です。subject_tokenが無効ならinvalid_requestを、要求されたaudienceでは発行できないならinvalid_targetを返します。
責任は2つに分かれます。STSは、subject_tokenを署名・iss・exp・audまで検証し、audienceを許可リストと照合し、権限を絞り、寿命を短くして渡します。検証を省いたSTSは、偽造トークンを自分の本物の署名でロンダリングしてしまう装置になります。下流は、audが自分か、actが許可された呼び出し元かを見ます。
現場での姿
「Keycloakでトークン交換すればいい」は、バージョンを先に確認すべき言葉です。Keycloakは26.2で標準のトークン交換を正式にサポートし始めました。token-exchangeのドキュメントによると、標準の交換(V2)はサーバーを起動すればデフォルトで有効になり、レガシーの交換(V1)はpreviewで廃止予定のため、--features=token-exchangeのようなフラグとfine-grained admin permissions v1を一緒に有効にする必要があります。V2も、同じレルム内のクライアント同士で交換する場合だけをサポートし、ユーザーのimpersonationと外部トークンの交換はサポートしません。委任(delegation)はドキュメントで実験的機能に分類されています。
このラボのPodのKeycloakは26.0.7です。つまりV2がありません。実際に、discoveryドキュメントのgrant_types_supportedにtoken-exchangeがなく、そのグラントでリクエストするとunsupported_grant_typeが返ります。そのためこのラボは、サービス間はKeycloakのclient credentialsで実測し、交換はSTSを自分で作って再現します。現場でも同じ判断をすることになります。アップグレードするのか、previewの機能に本番を賭けるのか、交換の窓口を別に置くのか、です。
次のラボですること
Keycloakでapi-svcのサービストークンを受け取って署名とクレームを確認し、Keycloak 26.0.7がトークン交換を拒否するのを自分で見ます。続いて8308にローカルSTSを立て、dev1のトークンをorders-api専用のトークン(actにapi-svcを記録)に交換し、8309の下流にaudとactを検査させます。最後に、偽造・改ざん・期限切れのsubject_tokenと、知らないaudienceがすべて拒否されることを、実際のリクエストで証明します。