はじめに
最近、Amazon CognitoとAPI Gatewayを少し触る機会があり、そこでOpenID ConnectのIDトークンを使ったAPI Gatewayでのアクセスコントロールができることを知りました。Cognitoの 開発者ガイド >> Amazon Cognitoユーザープール >> ユーザープールのトークンの使用には下記のように書かれています。
ID トークンを使用する
ID トークンは JSON Web キートークン (JWT) として表されます。トークンには、認証ユーザーの ID に関するクレームが含まれます。たとえば、name、family_name、phone_number などのトークンが含まれます。標準クレームに関する詳細については、OpenID Connect specification を参照してください。クライアントアプリは、アプリケーション内でこの ID 情報を使用できます。**ID トークンは、リソースサーバーまたはサーバーアプリケーションに対するユーザーの認証にも使用できます。**ID トークンはウェブ API に対してアプリケーション外で使用する場合、ID トークン内でクレームを信頼する前に、ID トークンの署名を確認する必要があります。
ここで湧いた疑問
リソースサーバのAPIの保護(アクセスコントロール)といえば、OAuth 2.0のアクセストークンで行うのでは?
なんでAmazon API GatewayはIDトークンでやっちゃっているんだろう?
結論を先にいうと、Amazon Cognito/API Gatewayの仕様は問題ない と最終的には思いました。ただし、少し混乱を招く仕様&ドキュメントだなぁと個人的には思いました(実際混乱しました)。また、IdToken vs AccessToken sent to Resource Server のように、同じようにリソースサーバの保護にIDトークンとアクセストークンのどっち使えばいいんだろう? と混乱している人が世の中にそれなりにいそうです。よい機会なので本記事でアクセストークン・IDトークンの2つのトークンの使いみちについて少し整理したいと思います。
忙しい人向けに先に結論
- Web APIを認証/認可サーバで保護して他のアプリケーションに公開する場合 アクセストークン使いましょう
- Web APIを自分のアプリケーションの一部(バックエンドサービス)として作成して保護する場合 IDトークン使いましょう
Amazon Cognito/API GatewayのIDトークンを使ったアクセスコントロールは、後者での使いみちなのでおかしな話ではない、というのが自分の中での結論です。AmazonがIDトークンによるアクセスコントロールの機能を提供してくれているんだから、Web APIのアクセスコントロールって全部IDトークンでやっておけばOKだよね! とならないように注意していただきたいです。
IDトークンとアクセストークンのユースケース
各トークンのユースケースについておさらいしつつ、各トークンの使われ方について説明したいと思います。
IDトークン
最も代表的なユースケースはSAMLを使って実施していたようなWebアプリケーションのIDフェデレーション(ID連携、SSO)でしょう。例えば、IDトークンを使ったよくあるWebアプリケーションへのID連携は下図シーケンスのようになります。
上記はサーバサイドで動作するWebアプリケーションでのID連携フローとなっていますが、OpenID ConnectではSAMLと異なりJSONベースのプロトコルであり、NativeアプリやJavaScriptクライアントアプリでも扱いやすいようになっています。そのようなアプリケーションの場合、クライアント単体で完結せずにバックエンドサーバを持つ構成をとることが多いかと思います。そのような構成では下記のように、クライアントアプリからバックエンドサービスにIDトークンを送信するという使い方が説明されているドキュメントをいくつか発見しました。
If you use Google Sign-In with an app or site that communicates with a backend server, you might need to identify the currently signed-in user on the server.
You use the GoogleAuthUtil class, available through Google Play services, to retrieve a string called an “ID Token”. You send the token to your back end and your back end can use it to quickly and cheaply verify which app sent it and who was using the app.
If your Firebase client app communicates with a custom backend server, you might need to identify the currently signed-in user on that server. To do so securely, after a successful sign-in, send the user's ID token to your server using HTTPS.
We are in a world where we need to authenticate a user of many clients to a set of back-end services.
バックエンドにIDトークンを送りつける場合は以下のようなシーケンスになるでしょう。JavaScriptなどのクライアントアプリケーションを想定してImplicit flowをここでは使います。
注意点として、バックエンドサービスはクライアントであるRPに付属するものであり、OP(認証/認可サーバ)側が提供するリソースサーバではない という点。ここでのバックエンドサービスはRPの一部となります。バックエンドサービスをRPの一部であると考えると、IDトークンを受け取って認証処理するのはごく自然ですね。
ちなみに、IDトークンの中身に aud
(audience) という項目がOpenID Connectの仕様上ありますが、ここにはIDトークンの受け取り側であるRPのクライアントIDが格納されます。また、IDトークンの検証でも、aud
の値がクライアントIDとなっていることをチェックすることがMUSTとなっています。
Client は aud (audience) Claim が iss (issuer) Claim で示される Issuer にて登録された, 自身の client_id をオーディエンスとして含むことを確認しなければならない (MUST).
この点から、API公開したリソースサーバにIDトークンを送るというのはおかしいということが分かります。広く一般にAPI公開されるリソースサーバ側は、サードパーティが開発したクライアントIDのことは知らないため、IDトークンが自分(リソースサーバ)に対する物かどうかを検証することができません。
アクセストークン
次にアクセストークンですが、OAuth 2.0認証 と呼ばれてIDフェデレーションのユースケースでも使われてしまっていますが、本来的にはリソースサーバのAPIの保護目的で使うことが想定されています。こちらも代表的なフローのシーケンス図を書いてみました。
アクセストークンの場合、現在は標準化されたRFC 7662: OAuth 2.0 Token Introspectionをリソースサーバ側で使うことで、アクセストークンの検証ができるところが多いです。また、レスポンスにはOPTIONAL
ではありますが、client_id
とaud
が定義されています。
{
"active": true,
"client_id": "l238j323ds-23ij4",
"username": "jdoe",
"scope": "read write dolphin",
"sub": "Z5O3upPC88QrAjx00dis",
"aud": "https://protected.example.net/resource",
"iss": "https://server.example.com/",
"exp": 1419356238,
"iat": 1419350238,
"extension_field": "twenty-seven"
}
上記例のように、IDトークンと異なり、aud
には受け取り側であるリソースサーバを表す文字列が入ります。client_id
にはOAuth 2.0クライアントIDが入りますので、どのOAuth 2.0クライアントに、どのリソースサーバへのアクセス許可を与えたかを厳密にチェックすることができます。また、scope
と合わせることで、許可する範囲をさらに限定ことができます。
これにより、たとえ別のクライアント向けや別のリソースサーバ向けのトークンで置換されたとしてもブロックすることができるわけですね。IDトークンの場合は「認証したのは誰か」はわかりますが「エンドユーザが同意した内容(どのクライアントにどのリソースへどのスコープでアクセス許可を与えたか)」についてはトークンからは分かりません。この点から、リソースサーバのAPI保護にはIDトークンではなくアクセストークンが適切であると分かります。
ちょっと脱線) OAuth 2.0を認証目的に使うと危険という話
2012年とかなり前ですが、単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる という話が話題になりました。Implicit grant flowの場合は攻撃者が容易にアクセストークンの置換ができるため、OAuth 2.0を認証目的で使うと大きなセキュリティホールができるよ、という話です。
問題の原因は、上記記事で以下のように説明されています。
問題の原因は、access_token の audience は resource endpoint であるのに対して、認証に使うトークンの audience は client でなければいけないというところにあります。だから、OpenID Connect では、client を audience にした id_token という、access_token とは別のトークンを発行しているのです。Facebook の signed_request も同じです。
本記事のテーマ(API保護にIDトークンを使っていいんだっけ?)とはまた違う話ですが、aud
の違いが引き起こした問題であり、IDトークンとアクセストークンの異なるポイントとして認識しておくとよいかと思います。
Amazonでの使い方は結局どうなの?
ドキュメントには、 ID トークンは、リソースサーバーまたはサーバーアプリケーションに対するユーザーの認証にも使用できます。 と書いてありましたが、英語版を見ると、your resource serversと書かれています。
Using the ID Token
The ID token is represented as a JSON Web Key Token (JWT). The token contains claims about the identity of the authenticated user. For example, it includes claims such as name, family_name, phone_number, etc. For more information about standard claims, see the OpenID Connect specification. A client app can use this identity information inside the application. The ID token can also be used to authenticate users against your resource servers or server applications. When an ID token is used outside of the application against your web APIs, you must verify the signature of the ID token before you can trust any claims inside the ID token.
Amazon Cognito/API Gatewayでアプリケーションを開発する場合、通常はユーザープール、クライアントアプリケーション、API Gatwayの作り手は同じになるかと思います。ここでいうリソースサーバはOAuth 2.0でいうリソースサーバと異なり、クライアントアプリケーションのバックエンドサービスに過ぎません。そう考えると、API GatewayにIDトークンを送りつけてアクセスコントロールするのも違和感ありません。
しかし、Amazon API Gatewayで作成したサービスをOAuth 2.0のリソースサーバとして公開(つまり、自分以外のアプリケーションにアクセスさせる)、をやろうとすると、IDトークンでは認証ユーザは特定できても、OAuth 2.0による認可制御は正しくできません。
結局のところ、Amazon Cognito/API Gatewayでは、
- 自分のアプリケーションの一部としてAPI Gatewayを使うなら、IDトークンによるアクセスコントロールは普通な話であり、Amazonはその機能を提供してくれている。
- ユーザープールを認証/認可サーバとして利用し、API Gatewayで外部公開用のWeb APIを作成したい場合は、カスタムオーソライザーでアクセストークンによるアクセスコントロールを行う必要がある(Lambdaで実装する必要がある)。
という話であり、Amazon独自の変わった仕様というわけではない、というのが自分の中での結論です。
今回誤読してしまった理由
今回、どこが自分の中で混乱のもととなったのか、反省のために振り返ってみました。
- OAuth 2.0の仕様をある程度知っている自分としては、Cognitoのドキュメントでリソースサーバという言葉が使われていたため、OAuth 2.0でいうところのリソースサーバに関するアクセスコントロールの話である、と思ってしまった。
- ID トークンは、リソースサーバーまたはサーバーアプリケーションに対するユーザーの認証にも使用できます と書かれていたが、よくよく見ると認可に関しては言及がなく、そもそもスコープ外っぽい。
- Amazon API Gatewayのドキュメント、Amazon Cognito ユーザープールを使用 - ユーザープールに統合された API を呼び出すに [Authorization] ヘッダー (または、認証作成時に指定した別のヘッダー) に ID トークンを含めます とあるのですが、実はAuthorizationヘッダー=Bearer Tokenと勘違いし、OAuth 2.0のリソースサーバに対するアクセスコントロールと思ってしまった。
あたりでしょうか。ちゃんとドキュメント読めってことですね
でも、Amazonさんももうちょっと分かりやすく書いて欲しいと思う...
まとめ
再掲ですが、
- Web APIを認証/認可サーバで保護して他のアプリケーションに公開する場合 アクセストークン使いましょう
- Web APIを自分のアプリケーションの一部(バックエンドサービス)として作成して保護する場合 IDトークン使いましょう
というお話でした!