Event WebhookのOAuth2.0をAWSで設定してみる
イベントデータを任意のサーバにPOSTするEvent Webhook機能には、サーバ側のセキュリティ強化のためのオプションが2つ用意されています。一つがSigned Event Webhook Requests、もう一つがOAuth 2.0です。
「Signed Event Webhook Requests」は認証(Authentication)のための機能で、データが確かにSendGridから送られたことを確認できます。一方「OAuth 2.0」は認可(Authorization)のためのプロセスを表し、信頼できる情報源にのみアクセス権限を与えます。
本記事ではOAuth 2.0にフォーカスします。認可された情報源からのみアクセスできるようにすることで、攻撃者によるアクセスを防ぐことができ、セキュリティの大幅な向上が見込めます。Amazon Web Services (AWS) の各種サービス(Amazon Cognito、Amazon API Gateway、 AWS Lambda)を用いた設定例を紹介するので、ぜひ実装の参考にしてください。
OAuth 2.0 Client Credentialsグラントの概要
SendGridのEvent Webhookは、OAuth 2.0で定義されている4つの認可フローのうち、Client Credentialsグラントを用いた認可に対応しています。最初にこの概要を説明します。以後、記事中の「OAuth」はOAuth 2.0のClient Credentialsグラントのことを指すと考えてください(他のグラントとはフローが異なります)。
OAuthによる認可を行わない場合、クライアントがサーバにリクエストし、サーバがレスポンスを返すだけです(下図左)。OAuthの定義に合わせて、以後サーバは「リソースサーバ」と表記します。Event Webhookの文脈では、クライアントがSendGrid、リソースサーバはイベントデータが送られるサーバに対応します。
OAuthでは新たに「認可サーバ」が登場し、以下のようなフローをたどります。
- クライアントは、リソースサーバへのリクエストに必要となるアクセストークンを認可サーバに要求します。このとき、クライアントIDとクライアントシークレットをリクエストに含める必要があります。
- 認可サーバは、正しいクライアントIDとクライアントシークレットが確認でき、クライアントが認証できたら、アクセストークンを返します。
- クライアントは、アクセストークンを持ってリソースサーバにリクエストします。
- リソースサーバは、アクセストークンが有効なものかどうか検証します。
- 検証結果に応じて、リソースサーバはクライアントにレスポンスを返します。
AWSを用いた実装例
以上のフローをAWSを用いて実装してみます。ここではAmazon Cognitoが認可サーバ、Amazon API Gatewayがリソースサーバに対応します。
4. までは上述のOAuthのフロー通りですが、その後アクセストークンが有効な場合にLambda関数を動作させます。この関数は、送られてきたリクエストデータをログに出力させるだけの単純なものです。
早速設定していきます。
1. API GatewayとLambdaの設定(リソースサーバの準備)
① Lambda関数の作成
まず、POSTされたイベントデータを処理するLambda関数を作成します。マネージメントコンソールでLambdaを選び、「関数の作成」をします。適当な関数名をつけ、ランタイムとしてPython 3.8を選択しましょう。コードソース(lambda_function.py)を以下のようにして「Deploy」します。
import json def handler(event, content): try: body = event.get("body") print(body) status_code = 200 except Exception as e: status_code = 500 body = {"description": str(e)} return { "statusCode": status_code, "headers": { "Content-Type": "application/json" }, "body": json.dumps(body) }
このLambda関数は送信されたリクエストデータをオウム返ししています。また、print関数でAmazon CloudWatchにログが出力されるので、これで動作確認します。
② API Gatewayの設定
こちらも適当な名前をつけて、REST APIを作成します。作成したAPIの画面で、「アクション」の中の「リソースの作成」から/activityというリソースを作成し、このリソースにPOSTメソッドを追加します。このとき、統合タイプをLambda 関数とし、先ほど作った関数を指定します。リソースは必ずしも作る必要はないですが、今回はActivityの情報を受け取るという意味でactivityと名前をつけています。
2. Cognitoの設定(認可サーバの準備)
Cognitoには、認証を管理する枠組みとして「ユーザープール」という機能があります。はじめにこれを作成し、その中でOAuthに必要な各種設定を行っていきます。
① ユーザープールの作成
「ユーザープールの管理」>「ユーザープールの作成」から適当な名前をつけて、デフォルトの設定でユーザープールを作成します。
② アプリクライアントの作成
左側メニューの「アプリクライアント」を選択し、適当な名前をつけて、デフォルトの設定でアプリクライアントを作成します。クライアントIDとクライアントシークレットを記録しておきます。
③ リソースサーバーの作成
リソースサーバーの識別子とスコープを定義します。今回、リソースサーバーはイベントデータPOST先のサーバ1つだけで、スコープ(権限)もサーバにアクセスできるかどうかの一種類だけなので、適当な名前でOKです。ここではリソースサーバーの識別子をactivity(Activityの情報を受けるサーバーなので)とし、スコープをフルアクセスの意味を込めて「*」としておきます。
④ アプリクライアントの設定
OAuth 2.0の「許可されている OAuth フロー」でClient credentialsを選び、「許可されているカスタムスコープ」のactivity/*にチェックを入れて保存します。
⑤ ドメイン名の設定
メニューの「ドメイン名」から認可サーバのドメインのプレフィックスを決めます。ここではevent-webhook-testとします。他で使われていないユニークなドメインである必要があります。
3. API Gatewayのオーソライザーの設定とAPIのデプロイ
上で設定したユーザープールをAPI Gatewayのオーソライザーに指定してAPIへのアクセスを制御します。
まず1で作成したAPIのページに移動し、左側の「オーソライザー」を選択します。新しいオーソライザーを作成し、図のように設定して保存します。「Cognitoユーザープール」には2で作成したユーザープールを選択します。
オーソライザーを作成できたら、左側の「リソース」に戻り、POSTの「メソッドリクエスト」を選択します。
「認可」には先ほど作ったオーソライザー(my_authorizer)を選択し、「OAuth スコープ」に「activity/*」を入力します。
「アクション」から「APIのデプロイ」を行います。「デプロイされるステージ」はprodとしておきます。デプロイすると、エンドポイントのURL(https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod)が表示され、設定は一通り完了です。
4. 認可フローの確認
次のステップに進む前に、OAuthのフローがちゃんと実装できたか確認してみましょう。まず認可サーバに対して認可リクエストを送り、アクセストークンを取得します。
$ curl --url 'https://event-webhook-test.auth.ap-northeast-1.amazoncognito.com/oauth2/token' \ --header 'Authorization: Basic username:password' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' {"access_token":"eyJrrrrr.eyJzzzzz.yyyyy","expires_in":3600,"token_type":"Bearer"}
Authorizationヘッダの「username:password」の部分は、クライアントIDとクライアントシークレットをコロンでつないでBase64エンコードした文字列にしてください。リクエスト方法の詳細はAWS公式ドキュメントに記載されています。アクセストークン (access_token) が返却されたら、それをAuthorizationヘッダに指定してAPIエンドポイントにアクセスしましょう。
$ curl --url 'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/activity' \ --header "Authorization: Bearer eyJrrrrr.eyJzzzzz.yyyyy" \ --data '{"message":"Hello"}' {\"message\":\"Hello\"}
リクエストデータがそのまま返ってくれば成功です。CloudWatchにも同じデータが記録されるはずです。ちなみに、アクセストークンを指定しなかったり期限切れのアクセストークンを指定したりすると、リクエストは失敗します。
5. SendGridの設定(クライアント側の準備)
ここまでくれば、もう一息です。SendGridダッシュボードのSettingsからMail Settings > Event Webhookを選択し、以下の通り入力します。
- Authorization Method: OAuth 2.0
- Client ID: アプリクライアントのクライアントID
- Client Secret: アプリクライアントのクライアントシークレット
- Token URL: Cognitoで設定したドメイン(https://event-webhook-test.auth.ap-northeast-1.amazoncognito.com/oauth2/token)
- HTTP Post URL: API GatewayのエンドポイントURL(https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/activity)
「Test Your Integration」ボタンを押下して、CloudWatchを確認してみましょう。「ロググループ」から該当のAPIのログを開き、テストデータが確認できたら成功です!
おわりに
少し手数は多いですが、わかってしまえば難しいところはありません。特に、既にAWS Lambdaを用いてイベントデータを受け取っているという方は、ぜひOAuth 2.0の導入もご検討ください。また、AWS CDKのPythonのコードサンプルと手順を以下にまとめました。AWSのマネージメントコンソールではなく、コードベースで同様の設定を行いたい場合はご参照ください。
https://github.com/yken2257/apigateway-client-credentials-cdk
OAuth 2.0と対をなす認証の機能として、「Signed Event Webhook Requests」があります。こちらの実装例はこのブログ記事で紹介しています。また、2つのセキュリティ強化オプションの概要説明は、こちらの記事をご覧ください。