Event WebhookのOAuth2.0をAWSで設定してみる

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では新たに「認可サーバ」が登場し、以下のようなフローをたどります。

OAuthによる認可

  1. クライアントは、リソースサーバへのリクエストに必要となるアクセストークンを認可サーバに要求します。このとき、クライアントIDとクライアントシークレットをリクエストに含める必要があります。
  2. 認可サーバは、正しいクライアントIDとクライアントシークレットが確認でき、クライアントが認証できたら、アクセストークンを返します。
  3. クライアントは、アクセストークンを持ってリソースサーバにリクエストします。
  4. リソースサーバは、アクセストークンが有効なものかどうか検証します。
  5. 検証結果に応じて、リソースサーバはクライアントにレスポンスを返します。

AWSを用いた実装例

以上のフローをAWSを用いて実装してみます。ここではAmazon Cognitoが認可サーバ、Amazon API Gatewayがリソースサーバに対応します。

AWSを用いた実装例

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で作成したユーザープールを選択します。

API Gatewayのオーソライザーの設定

オーソライザーを作成できたら、左側の「リソース」に戻り、POSTの「メソッドリクエスト」を選択します。

APIのデプロイ

「認可」には先ほど作ったオーソライザー(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のログを開き、テストデータが確認できたら成功です!

CloudWatchを確認

おわりに

少し手数は多いですが、わかってしまえば難しいところはありません。特に、既に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つのセキュリティ強化オプションの概要説明は、こちらの記事をご覧ください。

メールを成功の原動力に

開発者にもマーケターにも信頼されているメールサービスを利用して、
時間の節約、スケーラビリティ、メール配信に関する専門知識を手に入れましょう。