Token Exchange Grantについて

本記事は Digital Identity技術勉強会 #iddance Advent Calendar 2021 の11日目の記事です。

最近、[OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)にが自分の中で流行っているので、それについて書こうと思います。

## **どんな仕様なのか？**

まず、[RFC8693](https://datatracker.ietf.org/doc/html/rfc8693)に書かれていることをざっとまとめます

この仕様は、認可サーバにSecurity Token Service(STS)の機能をもたせるものと説明されています。

STSとは、Security Tokenを取得・検証して、異なる用途のSecurity Tokenを発行するサービスです。簡単に言ってしまえば、トークンを受け取って、別のトークン作るやつです。

この仕様の特徴的なところは、「あるトークンから、異なるsubject・異なる権限を持ったトークンの発行を許す」ことだと思います。

流れはすごくシンプルなので、別に面白みはありませんが、シーケンス図をかいておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12268/802493d3-c803-bb1f-9e45-4bb683413be5.png)

リクエストは、client_credentials grantと同様に、Clientが認可サーバにtoken requestを送って、トークンを取得する形です。異なる部分は以下の点になります。

- grant_typeには、新しく定義される urn:ietf:params:oauth:grant-type:token-exchange が使われる。
- subject_token, subject_token_typeで、トークン発行の根拠となるトークンとその種類を提示する。
- actor_token, actor_token_typeで、誰が利用するかを提示する
- 発行するトークンの種類を表すために、requested_token_typeというパラメータが定義されている。(access_token, refresh_token, id_token, jwt等)

その他のパラメータ、scope, audience, resourceの用途は、他のものと変わりません。（でもなんかちゃんとそれぞれがどういうものか書いてあって、包括的な理解の助けになりました）

レスポンスは、通常のtoken endpointのレスポンスとほぼ同じものです。ただし、レスポンスの`access_token`には、requested_token_typeで指定されたトークンが入ります。そのため、必ずしもアクセストークンが入るわけではなく、ID Tokenやその他Security Tokenが入る可能性もあります。

## **どんなユースケースがあるのか？**

次にどのようなユースケースがあるのか、見ていきます。

### **Resource serverにおける利用**

この例は、[RFC8693](https://datatracker.ietf.org/doc/html/rfc8693)にも例としてでているもので、Resource Serverが、更にBackendに居るサーバに対してリクエストを投げるときに、Resource ServerがClientから受け取ったアクセストークンを使うのではなく、新しくアクセストークンを取得し直して、Backend Serverのサーバにアクセスするというものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12268/8f017420-6e73-c0dc-28e5-9deef8fba0a7.png)


この状況は、Resource Server とBackend Serverがセキュリティ的に切り離された状況において利用されます。同じ状況は、[Microservices Security IN ACTION](https://www.manning.com/books/microservices-security-in-action?gclid=Cj0KCQjw4eaJBhDMARIsANhrQAAOyFvRIn-oUZPCGebD922UR7SM3sI4HsNtvxYRfjLIMMuHOho-V9caAqy-EALw_wcB) という本のなかでは、「あるTrust domainに属するmicroserviceが異なるTrust domainに対してアクセスする」という表現がされてします。

そのような状況において、Resource Serverが受け取ったアクセストークン(AT1)を使って、Backend Serverにアクセスすることは、以下のような理由で推奨されないと思われます。

- Backend Serverは、Resource Serverとは異なる権限を要求している。（そうでなかったら切り離されている意味ないし）
- Clientが必要とする権限は、Resource Serverにつなぐ権限であって、Backend Serverにつなぐ権限ではない。
- Clientは、Resource Serverが提供するAPIの作りに関しては無知であるべき。Resource ServerがBackend Serverに使うために必要だからという理由でAT1にBackend Serverにつなぐ権限を付与すべきではない。
- もし、AT1がsender-constrained Tokenになったら、仕組み的に、Resource ServerがAT1を再利用することはできなくなる。

そこで、token exchangeがでてきます。Resource Serverが受け取るのは、「ClientがResource Serverにアクセスする権限」を持ったトークンで、そのトークンを、subjectは同じだけど「Resource ServerがBackendサービスにアクセスする権限」を持ったトークンに交換することになります。このように、「同一のsubjectで、別の権限を持ったトークン」を発行するユースケースになります。

### **Clientにおける利用**

Resource Server側だけでなく、Client側でもこの機能を利用するuse caseがあります。その一つが、トークンの権限をDowngrade するようなときです。以下の図では、Clientがもともと持っているアクセストークン(AT1)を、より権限が絞られたアクセストークン(AT2)に交換しています。Googleの[Downscope with Credential Access Boundaries](https://cloud.google.com/iam/docs/downscoping-short-lived-credentials)に書いてあるのと同じ用途です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12268/bd840481-6274-acd3-8d6b-fc2c74c16fae.png)

もうちょっと具体的な例として、以下のようなClient applicationを考えてみます。

- Native Applicationで、Backend ServerのAPIと通信する。
- コンテンツの一部はサーバ側からHTMLを配信し、Webviewで表示している。
- Webivewで表示するコンテンツからは、JavascriptでBackendのAPIと通信する。
- BackendのAPIは、NativeからでもWebviewからでも、同様の認証認可を要求する。

Webview側でAPIを実行するときに認証を通す方法は、ぱっと以下3パターンくらいかな。

1. Native側からWebview側にアクセストークンを共有する
2. Webview側を一つの独立したClientとして、アクセストークンを取得する。
3. 権限を絞ったアクセストークンを作り、Webview側で使わせる。

1は基本的に良い感じはしませんね...。アクセストークンを管理しているNative Application側は、Webview上で行われるJavascriptの挙動を把握しているわけでもないのでどんな使われ方するのかわかりません。

とはいえ、2も少しやりすぎかもと思います。この例のように、同じアプリの一部でそのコンテンツの表示方法が異なるだけという位置づけでWebview利用しているような場合、Native application側とWebview側で異なるユーザのコンテンツが表示される状況はありませんし、原理的にそういう状況が発生し得ない形にするほうが無難に見えます。

そこででてくるのがtoken exchangeを使った方法です。Native Application側は、Webviewに共有するために、Webviewが使う必要最低限の権限にだけ絞ったトークンを発行して、Webview側に共有します。これも、Resource Serverのときと同じで、「同一のsubjectで、別の権限を持ったトークン」を発行するユースケースになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12268/26e6f8e5-039a-091e-b370-a6d1ff8ba796.png)

### **CIにおける利用**

今までは、同一subjectのトークンを発行する例でしたが、「異なるsubject・異なる権限を持ったトークン」を発行する例もあるでしょう。その一つがCIにおけるアクセストークンの取得です。[Github ActionsのOpenID Connect対応](https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/)とか、[GCP workload identity federation](https://cloud.google.com/iam/docs/configuring-workload-identity-federation)とか、まさにこの例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12268/15c1d464-6700-d8d6-e7a0-37226c53bff5.png)

CI上の環境変数にクレデンシャルをもたせてConfidential Clientとして振る舞わせるのはよくやられることだと思いますが、その方法にはいくつか懸念すべきことが挙げられます。

- CIでいろんなことし始めると、その権限は往々にして大きくなりがち
- CI上で強い権限を持ったクレデンシャルを管理するのは実際難しい

特に自分の周りではCodecovを狙ったサプライチェーン攻撃以降、基本的にその方向性は、少なくともbest practiceではないよねとなってきたように思います。

token exchangeを使った方法では、「認可サーバ側で作られるクレデンシャルをCIに持たせる」ではなく、「CI側で持っているクレデンシャルをもとに、認可サーバ側で定義されているユーザ・サービスアカウントにImpersonateして、アクセストークンを払い出す」方向を取ります。そしてそのアクセストークンは、CIが必要なオペレーションをするために必要最小限の権限だけ持ち、有効期間が非常に短いものとなります。

> 〜 心の声です 気にしないでください 〜
> 基本的にPublic ClientよりConfidential Clientのほうがセキュアであると認識されるけど、それは、クレデンシャルを使って認可サーバ側でClient認証ができるからだよな、アクセストークンを発行するときにtoken endpointとかで。でも、今のCIの例のように、現実問題、Confidential Clientとして扱ってみたものの、実はクレデンシャルをセキュアに管理できていなかったということは往々にして起こりうると。それならもう、認可サーバがクレデンシャルを払い出し共有するようなclient_secret_post、client_secret_basic、client_secret_jwtを使ったConfidential Clientは安全ではない!、private_key_jwtを使ったConfidential Clientもちゃんと秘密鍵管理できない人がいるから安全ではない!、Public clientをちゃんと実装することこそ最も安全だ〜とかいう事になったりするんだろうか....、ないか流石に。。そういえば、このCIの例みたいに、Client側のPlatformで発行されたらクレデンシャルを認可サーバ側と紐付けるパターンも、OAuth2.1のcredentialed clientに分類されるんだろうか...。

### **Employees / Business Partnersにおける利用**

同じく「異なるsubject・異なる権限を持ったトークン」を発行する例として、社内のEmployeeやBusiness partner向けのアクセストークンを発行するときにも利用可能なんじゃないかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12268/40845996-04ef-1744-a002-404fafbfdab5.png)

例えばこんな会社があったとします。

- 会社としてGoogle Workspaceを利用している。
- 自社のシステムのカスタマーサポートツールは、Google認証で利用できるようになっている。
- 顧客に対して提供しているアプリ用には、自分たちのIdPでアクセストークン発行している。
- Backendのサービスは、カスタマーサポートツールや顧客が利用するアプリに対して、APIを提供している。

この状況において、Backendのサービスがリクエストを認証する方法は、

1. Google認証で取得できるID Tokenを、Backend側に渡してEmployeeだと認識する。
2. Google認証で取得できるID Tokenをもとに、アクセストークンを発行して、それをBackend側に渡してEmployeeだと認識する。

基本的に、Backendが提供するAPIは、『機能毎にAPIを作って、必要な権限管理をする』のが一般的だと思います。全く同じ機能を提供するに当たって、End user向け、社内のEmployee向け、Business partner向けとAPIを分けたり、Backendに対してアクセスする方法をClientの種類や認証主体毎に準備していくと、認可の方法・アクセス制限方法もそれら毎に準備することになって汎用性はなくなるし、セキュリティの問題が生まれやすくなるように思えます。

また、ID Tokenの用途はあくまで、「IdPで認証した情報をClient側に伝える」ものなので、アクセス制御のための仕組みがあるわけではありません。

それらを踏まえて考えると、カスタマーサポートツールが受け取ったGoogle ID Tokenを使って、Backendのサービスまでアクセスさせるよりも、Google ID Tokenをもとに、Backend サービスにアクセスするトークンを払い出す形のほうが、Resource側で持つ認可の方法は標準的なものに統一でき、アクセス制御の方法もトークンの種類毎に別々に考えるとかする必要がなくて、筋が良いように思えます。


## **認可サーバにToken Exchangeの導入するときの諸々**

ここまで色々ユースケースを見てきましたが、実際に認可サーバにこの仕様を導入するに当たっては更に細かい話を考えていく必要があります。

この話はユースケースごとに話が変わってくると思うので、上げたユースケースをベースとして、何を考えていかなきゃいけなそうかを考えてみます。

### **subject_token_typeをどう使い分けるか？**

[3. Token Type Identifiers](https://datatracker.ietf.org/doc/html/rfc8693#section-3)をみてみると、このsubject_token_typeにはいくつかの種類が定義されています。若干、その使い方についてわかりにくいところがありますが、以下のような想定だと読みました。

|subject_token_type|利用方法|
|---|---|
|urn:ietf:params:oauth:token-type:access_token|「issued by the given authorization server」とあり、その下の説明にも「受け取るのは認可サーバが発行したアクセストークン」と言っているところから、同認可サーバが払い出したアクセストークンのみ受け付けるものと思われます。Resource ServerやClientのユースケースでは、このsubject_token_type利用されると想定されます。|
|urn:ietf:params:oauth:token-type:id_token|access_tokenとrefresh_tokenに対してはある「issued by the given authorization」 serverが、id_tokenに対してはないので、自身に限らず他の認可サーバが払い出したID Tokenも受け付けることを想定していると思われます。例えば、Github ActionsのID Tokenとか、Google Server Accountをもとに取得したID Tokenとか。CIやEmployeeのユースケースで利用されると想定されます。|
|urn:ietf:params:oauth:token-type:jwt|id_tokenの方と用途の違いにかなりなやみますが、client authentication private_key_jwt等で、subjectに紐づく公開鍵がすでに登録されている前提において、client assertionを受け付けるのかなと思いました。でもそれなら、jwt-bearerでよくない？|

あと、samlとrefresh_tokenは、上記ユースケースには合わないので割愛。refresh_tokenは正直ユースケースがよくわかってません。。

### **誰にImpersonateすることを想定するか？**

仕様上明確に言及されてはいませんが、発行されるアクセストークンのsubjectがどのように決められるかは、このsubject_token_typeにどんなトークンが使われるかに依存していると思います。

|subject_token_type|impersonate対象|
|---|---|
|urn:ietf:params:oauth:token-type:access_token|impersonateの対象は、subject_tokenのsubjectに準ずる。access_tokenのsubjectがuserを示すなら、発行されるアクセストークンのsubjectもuser。|
|urn:ietf:params:oauth:token-type:id_token|impersonateの対象は、事前にID Tokenの情報と紐付けて保存しておく。例えば、Google workload identity federationでは、ID Tokenのsubjectと特定のserviceアカウントを紐付ける形の設定をする。|
|urn:ietf:params:oauth:token-type:jwt|impersonateの対象は、jwt発行に利用された非対称鍵とbindしているsubject。例えば、private_key_jwtで登録された鍵であれば、Clientに対してimpersonateすることになる。|

### **Token Exchangeの利用自体を、どのように制限するか？**

もちろん誰でも適当にとった、アクセストークン・ID Tokenで、異なる権限を持ったアクセストークンを発行させるわけにはいきません。何かしらの基準を持って、利用を制限する必要はあるでしょう。ここではsubject-token-typeがaccess_tokenとid_tokenの場合において、どのような制限の選択肢があるかを検討してみます。

まず、subject_token_typeにurn:ietf:params:oauth:token-type:access_tokenを使った場合の制限についてです。Resource ServerやClientでのユースケースを想定しています。

|制限対象|制限内容|
|---|---|
|ActorとしてのClient|とりあえず、どのClientがtoken exchangeを利用可能かは、そのClientが利用可能なgrant_typeで制限されることになります。どちらのユースケースでもこれは入ることになると思う。|
|Subject_tokenのClient|どのClientのATをToken exchangeに利用可能かを制限する方向性です。Clientのユースケースでは、利用者はそのActor側のClient自身になるのでこの形で利用可能でしょう。<br><br>しかし、Resoure Serverのユースケースでは、リソースサーバとしては構造的に別のBackend Serverに接続する必要がありかつ、不特定多数のClientからリクエスト受け付けるわけだから、特定のClientのみ許可するというのは難しそう。|
|Subject_tokenのmay_act|[4.4. "may_act" (Authorized Actor) Claim](https://datatracker.ietf.org/doc/html/rfc8693#section-4.4)にmay_act claimというのが定められています。これは、subject_tokenの中に含まれていて、そこで誰がこのアクセストークンをsubject_tokenとして利用可能かを書いておくものです。Resource Serverのユースケースでは、この形がマッチしそう。 <br><br> でも、どうやって入れるのかは特にさだめられてません。想像するに、subject_tokenを取得するときの認可リクエストで、resource/audienceをもとにして、そこにつなぐならmay_act claimsをつけるとかするのかな...。しかし、しかしこの構造、認可サーバ側で特定のリソースサーバがtoken exchangeするってことを知らなきゃいけなくて微妙じゃね？|

次に、subject_token_typeにurn:ietf:params:oauth:token-type:id_tokenを使った場合の制限についてです。CIでの利用や、Employeeに対する利用をユースケースとして想定しています。

|制限対象|制限内容|
|---|---|
|ActorとしてのClient|とりあえず、どのClientがtoken exchangeを利用可能かは、そのClientが利用可能なgrant_typeで制限されることになります。どちらのユースケースでもこれは入ることになると思う。|
|ID Token - Issuer|どちらのユースケースにおいても、外部のIDTokenを使う以上、流石にIssuerで絞るのは必須でしょう。|
|ID Token - subject|CIにおける利用において、subject_tokenとなるID Tokenのsubjectは、用途によって固定されているでしょう。例えば、Github Actionでは、subjectにレポジトリやbranchが入っています。そのため、issuerとともに特定のsubjectなら利用可能と制限するのが良さそうに思う。|
|ID Token - email|Employeeの利用において、subject_tokenとなるID Tokenのsubjectは社員毎に異なります。全社員のsubjectを登録するとか厳しいものがあるので、emailのdomainで利用可否を定めるもしくは、他のサービスを参照して権限を確認するとかになるかな。|


### **発行するトークンの権限を、どのように制限するか？**

発行するアクセストークンの権限は、token endpointに対して渡される、scopeやaudience、resourceパラメータによって指定されるとされていますが、そのActorとしてのClientやそのsubject_tokenにおいてどこまでの権限を許すのか、は特に言及されていません。

しかし、このToken exchangeでは、「異なるsubject・異なる権限を持ったトークンの発行を許す」性質上、特に発行する権限が拡大するようなユースケースでは、認可サーバ側で何かしらの制限を持っていても自然だと思います。

|ユースケース|制限内容|
|---|---|
|Resource Server , Clientのユースケース|例えば、Resource Server, Clientのユースケースでは、Resource Server側のユースケースでは権限が拡大する形になりますが、Clientのユースケースでは権限が縮小される使い方のみの想定です。そのため例えば、『権限が拡大するような交換は、confidential clientでしかできない、public clientでは権限を落とす交換しかできない』と言った制限があってもいいよな思いました。|
|CIのユースケース|CIのケースでは、CIに一つのOAuth Clientを発行して、CI側で発行されたID Tokenを持って、Resourceにアクセスする権限を持ったアクセストークンを発行します。で、おそらく、発行されたID Tokenのsubject毎に何を実行できるかを変更したくなるでしょう。 <br><br>Github Actionsを例に取れば、ID Tokenのsubjectには、repositoryやbranchが含まれます。そのため、そのCIからGCPのProject上で何かしらの操作をするときに、「このrepositoryへのpushをきっかけに、GCPのProject Aのデプロイを実行できるServiece Accountにimpersonateしてデプロイする」とかするでしょう。そのとき、別のrepositoryをpushしたときに作られるID TokendでProject Aのデプロイを実行できては行けないので、特定のsubjcetとそのATが実行する権限を結びつける形になっています。<br><br>そのあたりを考えると、ID Token - subjectの制限と合わせて、許可されるaudience/scopeを設定できるようになっていてもいいのかもしれません。|
|Employeeのユースケース|Employeeのケースでは、Clientだけの制限ではなく、employee毎・subject毎にアクセス制限がしたいケースが多いと思います。ただ、全employeeの細かい権限を認可サーバで管理するよりも、Employeeの権限を管理するサービスが別にいて、そのサービスに問い合わせて権限を設定、もしくは認可サーバではClient単位の基本的な制限のみ行い、各employeeが持つべき細かい権限は、内部的に行う方向もあると思います。|


### **リフレッシュトークンを発行するか否か？また発行するアクセストークンの有効期限を伝播させるか否か？**

[2.2.1. Successful Response](https://datatracker.ietf.org/doc/html/rfc8693#section-2.2.1)をみると、リフレッシュトークンを発行することも許されています。発行したトークンの利用が一時的なものであれば、RefreshTokenは使う必要ない。subject_tokenが無効になった後でも、発行したトークンを利用したいユースケースがあるなら、RefreshTokenを使っても良いとされています。ただ、自分で使いたいケースがあるなら明確にしておけ、と言っています。

上記のユースケースのなかで、Resource, Client, CIにおける利用では、おそらくsubjet_tokenが切れた後に、発行されたアクセストークンを利用したいケースはないと思います。ClientやCIの利用では、その利用自体が一時的な利用なので、アクセストークンの有効期限自体、すごく短くて良いでしょう。Resourceでの利用の場合、Resourceサーバ側で取得したアクセストークンをキャッシュしておく必要はあると思いますが、もとのsubject tokenが切れているなら、前のアクセストークンを使わずに新しくリクエストされてきたアクセストークンをsubject tokenとして再度アクセスしてアクセストークンを取ることになると思います。

ただ、Employeeのケースでは、RefreshTokenが必要になると思います。なぜなら、CSToolがResourceにアクセスしたい期間はID Tokenの期間ではなく、ID TokenをベースにCSToolが自分で作るSessionの有効期限によるからです。EmployeeがGoogleで認証して、CSToolを利用できる間は、CSToolがResourceを触れるようにしておきたいのです。そのため、RefreshTokenも発行して、必要なときに再取得する形になると思います。

また、似たような話で、発行したアクセストークンの有効期限をどうするか？という話があります。もし、subject_tokenがアクセストークンで無効になった後に利用するユースケースが存在しないなら、発行するアクセストークンの有効期限は、subject_tokenの有効期限と同じにしておいたらいいんじゃないかなと思いました。subject_tokenの残りが10分なら、発行されるアクセストークンの有効期間は10分というように。

### **そういえば、Jwt-bearerとはどう使い分けるか？**

[JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants](https://datatracker.ietf.org/doc/html/rfc7523) というのがあります。ぱっとみ、「JWT(assertion)をtoken endpointに投げつけて、アクセストークンを発行する」という、token exchangeとかなりにている用に思えます。token exchange grantとjwt-bearer grantはどのように使い分けるべきか？という疑問がわきます。

これに対しては、jwt-bearerでは「根拠となるトークンは同じplatform内で発行されるもので、subjectの変更を伴わない」、token exchangeは「根拠となるトークンは異なるplatformでの発行されてよく、subjectの変更も許す」と理解しました。

Googleの例でいえば、**jwt-bearer**として利用されるのは、[Service Accountからアクセストークンを取得するケース](https://developers.google.com/identity/protocols/oauth2/service-account#httprest_1)。この場合、すでにGoogle Service Accountが発行されて、credentialが共有されている状態において、そこからassertionを作って、jwt-bearerで当該Service Accountのアクセストークンを取ります。**token-exchage**として利用されるのは、[workload identity federationのケース](https://cloud.google.com/iam/docs/workload-identity-federation)。この場合、subject_tokenとして利用するJWT (ID Token)は、他のplatformで作られたもので、内部のService AccountにimpersonateしてATを発行するというように理解しています。

### **終わりに**

Token Exchangeの仕様とユースケース、認可サーバに導入するときの詳細について考えていきました。

ユースケースの幅が広くて面白いなと思う一方で、トークンを発行する際の制限をちゃんと考えないと、とんでもないセキュリティホールが生まれてしまいそうだなと思いました。