はじめに
このチュートリアルでは、『証明書に紐付いたアクセストークン』を活用して Amazon API Gateway 上に構築した API をこれまで以上に安全に保護する方法を紹介します。 OAuth アクセストークンが一度漏洩すると、攻撃者はそのアクセストークンをもって API にアクセスできます。従来のアクセストークンは電車の切符のようなもので、一度盗まれたら誰でも使えてしまいます。 この脆弱性はアクセストークンと同時にアクセストークンの正当な保有者である証拠も併せて提示することを API 呼出者に要求することで軽減することができます。その証拠は proof of possession と呼ばれ、よく PoP と短縮されます。使用時に PoP を必要とするアクセストークンは、搭乗時にパスポートの提示も併せて要求される国際線の航空券に似ています。 RFC 8705 (OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens) は PoP の仕組みを標準化しました。OAuth コミュニティーではその仕組みは MTLS と呼ばれていますが、混乱を避けるため個人的には『証明書バインディング』と呼んでいます。いずれにしても、簡潔に述べますと、その仕組みは、トークンエンドポイントからアクセストークンの発行を受ける際に使用した **X.509 証明書**と同じ証明書をアクセストークンと併せて提示することを API 呼出者に要求します。次の図は証明書バインディングの概念を示しています。 金融グレード API (英語名称 Financial-grade API)、通称 FAPI (ファピ) をご存知でしょうか。FAPI は、より高度な API セキュリティのために OAuth 2.0 と OpenID Connect の上に作られた技術仕様です。英国オープンバンキングはオープンバンキングプロファイルの基盤として FAPI を採用し、今では他国も追随しつつあります。ここで注目していただきたいのは、証明書バインディングが FAPI の必須技術要素となっている点です。 証明書バイディングの大前提は、API とクライアントアプリケーション間の接続が相互 TLS (Mutual TLS) を用いて確立されることです。相互 TLS では、TLS ハンドシェイク中にクライアントアプリケーション側も X.509 クライアント証明書の提示を要求されます。2020 年 9 月 17 日、AWS は “Introducing mutual TLS authentication for Amazon API Gateway” と題したブログで Amazon API Gateway が相互 TLS に対応したことを発表しました。Amazon API Gateway は金融グレード API セキュリティへの扉を開けました。オーソライザーの役割
Amazon API Gateway は、API 保護の独自ロジックを開発者自ら実装する手段として **Lambda オーソライザー**という仕組みを提供しています。OAuth ベースの API 保護を提供する Lambda オーソライザーは、API コールからアクセストークンとクライアント証明書を取り出し、次の事項を確認します。- 提示されたアクセストークンが存在し、
- 有効期限が切れておらず、
- リソースアクセスに要求されるスコープを持ち、
- 無効化されておらず、
- 提示されたクライアント証明書に紐付いている。 (証明書バインディング)
- リソースアクセスを許可する IAM ポリシーを返す。
- リソースアクセスを拒否する IAM ポリシーを返す。
- HTTP ステータスコード “401 Unauthorized” を用いてリソースアクセスを拒否するように Amazon API Gateway に指示するため、エラーメッセージ
'Unauthorized'を持つ例外を投げる。 - HTTP ステータスコード “500 Internal Server Error” を用いてリソースアクセスを拒否するように Amazon API Gateway に指示するため、他のエラーメッセージを持つ例外を投げる。
Lambda オーソライザーの応答フォーマットに 2.0 を選択し、IAM ポリシーよりも簡素な応答を返すこともできます。詳細は『Lambda オーソライザーのレスポンス形式』を参照してください。
Amazon API Gateway に “401 Unauthorized” 応答をさせるのに、例外を投げる以外の方法はあるのでしょうか?ご存知であれば教えてください。RFC 6750 (The OAuth 2.0 Authorization Framework: Bearer Token Usage) に準拠するためには、Lambda オーソライザーで
WWW-Authenticate ヘッダーの値を指定して Amazon API Gateway に使わせることができなければなりません。オーソライザーの実装
この後すぐにお見せすることになる Lambda オーソライザーの実装では、アクセストークンの検証処理を Authlete (オースリート) のイントロスペクション API (/api/auth/introspection) に委譲します。そのため、そのオーソライザーの実装は複雑なロジックを含みません。次の図は Amazon API Gateway、Lambda オーソライザー、Authlete の関係を示しています。
これに加え、Authlete の Python ライブラリ内にある Authorizer クラスが必要な作業のほとんどを行うため、実装は非常に小さくできます。実際に、次のコードは証明書バインディングをサポートする Lambda オーソライザーの完全な実装例です。
リソースアクセスに要求されるスコープ
しかしながら、実際には、どのリソースがどのスコープを要求するかを設定する必要が出てくるでしょう。これは、(1)Authorizer の handle() メソッドに関数を渡す、もしくは(2)Authorizer のサブクラスを作り determine_scopes() メソッドをオーバーライドする、という方法で実現することができます。
関数とメソッドの双方とも、下表に挙げられている 4 つの引数を取り、リソースアクセスに必要となるスコープの名前のリストを返すことが求められます。
次の 2 つの例はどちらも同じ効果を持ち、
time リソースに HTTP GET メソッドでアクセスする際には time:query スコープが必要であると言っています。
ポリシーまたは例外
OAuth 2.0 関連の仕様に準拠するためには、場合によっては (例えば提示されたアクセストークンの有効期限が切れているとき) “401 Unauthorized” を API 呼出者に返すよう、Lambda オーソライザーから Amazon API Gateway に伝えなければなりません。AWS の技術文書やサンプルプログラムによれば、これを行うためにオーソライザーは'Unauthorized' というメッセージを持つ例外を投げなければなりません。しかし、このような簡素な例外は “Unauthorized” レスポンスに関する全ての貴重な情報を捨ててしまい、デバッグ作業をとても難しくしてしまいます。
そこで、デフォルトでは、別の言い方をすると policy プロパティーが True (デフォルト) の場合、Authorizer の handle() メソッドは、'Unauthorized' 例外を投げるべきである場合を含め、常に許可 (“Allow”) もしくは拒否 (“Deny”) を示す IAM ポリシーを返します。
もしも ”Unauthorized” や “Internal Server Error” の際に Authorizer に例外を投げさせたければ、policy プロパティーに False を設定してください。Authorizer のコンストラクターに policy=False を渡すことで実現できます。
ポリシーのコンテキスト
Authorizer の handle() メソッドは IAM ポリシーを表す dict インスタンスを返します。その辞書内には context があり、その値も辞書となっています。Authorizer はそこに幾つかの情報を埋め込みます。下表は、context 辞書に含まれる可能性のあるプロパティーの一覧です。
Authorizer のサブクラスで update_policy_context() をオーバーライドすることにより、context にエントリーを追加することができます。ただし、値として JSON オブジェクトや配列は使えないので注意してください。これは AWS の技術的制限事項です。
context に含まれるエントリー群は、後ほど他の場所で使うことができます。詳細は『Amazon API Gateway Lambda オーソライザーからの出力』を参照してください。
フック
Authorizer クラスはサブクラスの実装のために幾つかのフックを提供します。必要に応じてオーバーライドしてください。
オーソライザーの設定
Lambda イベントペイロード
Lambda オーソライザーを作成するにあたり最も重要な点は、Lambda イベントペイロードで『リクエスト』を選ぶことです。さもないと、オーソライザーはクライアント証明書の情報にアクセスできません。それはアクセストークンがクライアント証明書と紐付いているかどうかをオーソライザーがチェックできなくなることを意味します。 Lambda イベントペイロードの選択によりオーソライザーへの入力がどのように変化するかの詳細については『Amazon API Gateway Lambda オーソライザーへの入力』を参照してください。環境変数
AuthleteApi のインスタンスが渡されない場合、Authorizer のコンストラクターは内部で AuthleteApiImpl(AuthleteEnvConfiguration()) を実行してインスタンスを生成し、それを Authlete API へのアクセス時に使用します。そこで使用されている AuthleteEnvConfiguration が Authlete の設定情報を環境変数経由で取得できることを想定しているので、オーソライザーの実装として用いる Lambda 関数に次の環境変数群を設定しておく必要があります。
タイムアウト
Lambda 関数のタイムアウトをデフォルト値よりも大きくしておくことをお勧めします。様々な条件が重なることにより、Authlete のイントロスペクション API の応答に時間がかかることがありうるためです。オーソライザーのパッケージング
Lambda 関数の ZIP パッケージの生成とアップロードの方法は『Python の AWS Lambda デプロイパッケージ』の『追加の依存関係を使用して関数を更新する』で説明されています。 下記は、authlete パッケージを含む Lambda オーソライザーの ZIP ファイルを生成してアップロードする例です。テスト
このチュートリアルにおいて、テストが一番大変な箇所かもしれません。というのは、下図に示すテスト環境を構築するために多くの手順が必要だからです。 次の手順を一つずつ一緒に踏んでいきましょう。- 証明書バインディングをサポートする認可サーバーを用意する。
- 認可サーバー用のサーバー証明書を用意する。
- 認可サーバーのトークンエンドポイントの相互 TLS のためにリバースプロキシを設定する。
- 証明書バインディング用に設定されたクライアントアプリケーションを用意する。
- クライアントアプリケーション用のクライアント証明書を用意する。
- Amazon API Gateway 用のカスタムドメインを用意する。
- 証明書に紐付くアクセストークンを取得する。
- リソースアクセス (API コール) をおこなう。
認可サーバー
認可サーバーとして java-oauth-server を使うことにしましょう。これは、Authlete をバックエンドサービスとして用いる Java で書かれた認可サーバーです。http://localhost:8080 で認可サーバー (java-oauth-server) が起動します。
それから、管理者コンソールにログインし、認可サーバーに対応するサービスの設定を証明書バインディングをサポートするように変更します。
サーバー証明書
認可サーバー用の秘密鍵および自己署名証明書を作成します。openssl コマンドは OpenSSL のものであることに注意してください。High Sierra 以降、macOS にインストールされている openssl は LibraSSL のものなので、このチュートリアルのコマンドラインをそのまま試すためには OpenSSL の openssl をインストールする必要があります。
リバースプロキシ
java-oauth-server 自身は自分のエンドポイント群を TLS で保護していないので、TLS 接続を受けるためには java-oauth-server の前にリバースプロキシを置く必要があります。次のものは Nginx をリバースプロキシとして動かすための設定ファイルの例です。https://localhost:8443 で起動し、/token で受け付けたリクエストを http://localhost:8080/api/token に転送します。
この設定ファイルの名前が nginx.conf の場合、次のコマンドにより Nginx を起動することができます。
クライアントアプリケーション
Authlete 管理コンソールにログインし、テストで使用する予定のクライアントアプリケーションの証明書バインディングの設定を有効にしてください。クライアント証明書
クライアントアプリケーション用の秘密鍵および自己署名証明書を作成します。/CN=) の値には別の値を指定するようにしてください。というのは、次のセクションで説明しますが、Amazon API Gateway のカスタムドメインの設定が同じサブジェクトの証明書を複数含むトラストストアを拒否するからです。
カスタムドメイン
本記事執筆時点では、Amazon API Gateway で相互 TLS を有効にするためには API にカスタムドメイン (例api.example.com) を割り当てなければなりません。
Amazon API Gateway のコンソールには『カスタムドメイン名』というメニューがあります。そこでのカスタムドメイン設定をスムーズにおこなうため、事前に次のものを用意しておいたほうがよいでしょう。
- カスタムドメイン用のサーバー証明書
- トラストストア (信頼するクライアント証明書群を含むファイル)
カスタムドメイン用のサーバー証明書
Amazon API Gateway のカスタムドメイン用のサーバ証明書は AWS Certificate Manager (ACM) の管理下に置かなければなりません。ACM コンソールでは、既存の証明書をインポートしたり新しく証明書を作成したりすることができます。しかし、相互 TLS が有効になっている場合、インポートした証明書は Amazon API Gateway のカスタムドメイン用としては使えないので、新しく作成してください。トラストストア
相互 TLS を設定する際、信頼するクライアント証明書群を含むファイルの場所を入力するよう求められます。そのファイルはトラストストアと呼ばれます。Amazon API Gateway の相互 TLS の実装は、TLS ハンドシェイク中に提示されたクライアント証明書がトラストストア内に含まれているかチェックします。もしも含まれていなければ、Amazon API Gateway は Lambda オーソライザーを呼ぶことなく、その接続を拒否します。 トラストファイルは、PEM フォーマットのクライアント証明書群を下記のように列挙するテキストファイルです。PEM フォーマットの詳細については『図解 X.509 証明書』を参照してください。
カスタムドメイン設定
Amazon API Gateway にカスタムドメインを登録するための準備が整いました。 『ドメインの詳細』で相互 TLS 認証を有効にしてください。そうすると、トラストストア URI を入力するフィールドが表示されます。あなたのトラストストアの S3 URI をそのフィールドに入力してください。 それから、『エンドポイント設定』でカスタムドメイン用のサーバ証明書を選択してください。 カスタムドメイン設定後、APIとステージの組をカスタムドメイン下のパスにどのようにマッピングするかを設定することができます。次のスクリーンショットでは、“Example” API のdev ステージをカスタムドメインのパス dev にマッピングしています。
ルーティング
カスタムドメイン設定の最後の手順は、カスタムドメインが『API Gateway ドメイン名』にルーティングされるよう、DNS サーバーの CNAME レコードを追加することです。証明書に紐付くアクセストークン
全ての準備が済みました。認可コードフローを用いて証明書に紐付くアクセストークンを発行してみましょう。認可リクエスト
認可コードフローの最初のステップは、認可サーバーの**認可エンドポイントに Web ブラウザ経由で認可リクエスト**を送ることです。このチュートリアルでは、認可エンドポイントは java-oauth-server が提供するhttp://localhost:8080/api/authorization です。認可リクエストを表す下記の URL の ${クライアントID} をあなたのクライアントアプリケーションの実際のクライアント ID で置き換え、Web ブラウザを使ってその URL にアクセスしてください。
john と john を入力し Authorize ボタンを押してください。Web ブラウザはあなたのクライアントアプリケーションのリダクレクトエンドポイントにリダイレクトされます。
john と john は java-oauth-server の擬似データベースにハードコードされている値です。認可エンドポイント再訪時に入力フィールドを再表示させたいときは、認可リクエストの末尾に
&prompt=login を追加してください。prompt パラメーターについては OpenID Connect Core 1.0 の 3.1.2.1. Authentication Request を参照してください。https://{Authleteサーバー}//api/mock/redirection/{サービスAPIキー} です。
ブラウザのアドレスバーに表示されているリダイレクトエンドポイントの URL には、次のように code レスポンスパラメーターが含まれています。
code レスポンスパラメーターの値は、あなたのクライアントアプリケーションに対して認可サーバーから発行された認可コードです。この認可コードはクライアントアプリケーションがトークンリクエストを投げる際に必要となります。
トークンリクエスト
認可コード取得後、クライアントアプリケーションは認可サーバーの**トークンエンドポイントにトークンリクエスト**を投げます。このチュートリアルでは、トークンエンドポイントは Nginx が提供するhttps://localhost:8443/token です。
トークンリクエストはシェル端末で curl コマンドを用いて投げることができます。下記はトークンリクエストの例です。タイプする前に、${クライアントID} と ${認可コード} を実際のクライアント ID と認可コードに忘れずに置き換えてください。
ここでポイントとなるのは、
--key オプションと --cert オプションを用いてクライアントの秘密鍵と証明書を curl コマンドに渡すことです。トークンエンドポイントが相互 TLS を要求するため、すなわち TLS ハンドシェイク中にクライアント証明書を要求するためです。トークンエンドポイントから発行されるアクセストークンは、トークンリクエストで使用されたクライアント証明書に紐付けられます。
トークンリクエストが成功すると、トークンエンドポイントは access_token を含む JSON を返します。
access_token プロパティーの値が発行されたアクセストークンです。クライアントアプリケーションは API コール時にこのアクセストークンを使います。
リソースアクセス (API コール)
ついに、証明書に紐付くアクセストークンで保護される Amazon API Gateway 上のリソース (API) にアクセスする準備が整いました。 最初に、アクセストークンと正しいクライアント証明書でリソースにアクセスしてください。下記の例の${アクセストークン}、${カスタムドメイン}、${リソース} は実際の値で置き換えてください。全てが全て正しく設定されていれば、ブロックされることなくリソースの取得に成功するでしょう。
client_certificate_2.pem) でリソースにアクセスしてください。