Cognito Userpool から送信される認証メール・SMSの送信基盤が選べるようになりました

AWS の認証基盤サービスである Cognito Userpool では、メールアドレスの実在性を確認したり、パスワードリセットを行うため、確認コードをメールやSMSで送信する機能が提供されています。

これまで、それらのメッセージの内容をカスタマイズするためには、以下の方法が提供されていました。

- コンソールからメッセージを編集する
- カスタムメッセージLambdaトリガーを利用する

これらのメッセージは、Amazon SESやAmazon SNSを利用して送信されていましたが、2020年11月のサイレントアップデートにより、サードパーティーの配信基盤（SendGrid等）からメッセージを送信する手段が提供されました！

また、SESを利用する場合でも、プレーンテキストやマルチパート形式でメールを送信したり、SESのConfiguration Setを適用することが可能になります。
一部のキャリア・受信設定によってはHTMLパートのみのメールが正しく受け取れずに文字化けしたりすることがありますが、これも解消することが可能です！

それでは早速、これを適用するための新しいトリガー（以下「カスタムメッセージ**送信**Lambdaトリガー」の設定手順を紹介します。

# ポイント

- カスタムメッセージLambdaトリガーでは「確認コードを配置するためのプレースホルダー」が渡されるが、カスタムメッセージ送信Lambdaトリガーでは「KMSキーで暗号化された確認コード」が渡される
- Lambda内で復号処理を実施し、メッセージを送信するコードを書く
- コンソールやCloudFormationからは設定できないので、CLIを利用する（2021年1月現在）

# 設定手順

以下のドキュメントを参考に進めていきます（この記事ではドキュメントと異なるステップで設定を進めます）。
https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-email-sender.html

なお、ここではEメールの送信について設定していきますが、SMSについても同様の手順で設定できます。

## 1. KMSでキーを作成する

KMSで、確認コードの暗号化に使用する対称キーを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43540/02f505cf-63a8-e0d8-fb90-11c57b67a9ac.png)
キーのARNと、エイリアスのARNを控えておきます。

## 2. Lambdaを作成する

ユーザー情報と確認コードをCognitoから受け取り、メールを送信するLambdaを作成します。
以下、上記ドキュメントに記載のサンプルコード（Node.js）を整形したものを記載します。

```javascript

const AWS = require('aws-sdk');
const b64 = require('base64-js');
const encryptionSdk = require('@aws-crypto/client-node');

//Configure the encryption SDK client with the KMS key from the environment variables.
const { encrypt, decrypt } = encryptionSdk.buildClient(encryptionSdk.CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT);
const generatorKeyId = process.env.KEY_ALIAS;
const keyIds = [process.env.KEY_ID];
const keyring = new encryptionSdk.KmsKeyringNode({ generatorKeyId, keyIds })

exports.handler = async (event) => {
  //Decrypt the secret code using encryption SDK.
  let plainTextCode;
  if (event.request.code) {
    const { plaintext, messageHeader } = await decrypt(keyring, b64.toByteArray(event.request.code));
    plainTextCode = plaintext
  }

  //PlainTextCode now has the decrypted secret.

  if (event.triggerSource == 'CustomEmailSender_SignUp') {

    //Send email to end-user using custom or 3rd party provider.
    //Include temporary password in the email.

  } else if (event.triggerSource == 'CustomEmailSender_ResendCode') {

  } else if (event.triggerSource == 'CustomEmailSender_ForgotPassword') {

  } else if (event.triggerSource == 'CustomEmailSender_UpdateUserAttribute') {

  } else if (event.triggerSource == 'CustomEmailSender_VerifyUserAttribute') {

  } else if (event.triggerSource == 'CustomEmailSender_AdminCreateUser') {

  } else if (event.triggerSource == 'CustomEmailSender_AccountTakeOverNotification') {

  }

  return;
};
```

### サンプルLambdaについての補足

サンプルLambdaではaws-sdkに加えて[@aws-crypto/client-node](https://www.npmjs.com/package/@aws-crypto/client-node)と[base64-js](https://www.npmjs.com/package/base64-js)というライブラリを使用しています。
npm等を使ったインストールや、Lambdaデプロイ時にアップロードが必要となります。

このサンプルLambdaを動作させるには、環境変数を2つ設定する必要があります。
それぞれ変数名から想起される内容と設定すべき値が異なるのでご注意ください。

- `KEY_ALIAS`: KMS対称キーのエイリアスの**ARN**
- `KEY_ID`: KMS対称キーの**ARN**

Lambdaの実行ロールには、通常のLambda実行権限のほかに `kms:Decrypt` アクションを許可する必要があります（リソースに上記キーを設定するなど、適切に権限付与を行ってください）。

また、サンプルLambdaでは送信処理の記述は割愛されていますので、お使いの配信基盤に合わせて送信処理を実装してください。

### トリガーLambdaの仕様について

`event.triggerSource` で渡される値については上記ドキュメントに記載があり、また従来のカスタムメッセージトリガーとほとんど変わりません（加えて、新しいトリガーでは「アドバンスドセキュリティを有効にした場合の高リスク通知メール」についてもカスタマイズできるようになっています）。

暗号化された確認コードが格納されている `event.request.code` のほかは、[カスタムトリガーの共通パラメータ](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html#cognito-user-pools-lambda-trigger-sample-event-parameter-shared)に記載の内容が渡されます。
例えばメールアドレスは `event.request.userAttributes.email` に、電話番号は `event.request.userAttributes.phone_number` に格納されていますので、これらを使ってメッセージの送信を行うことができます。

Lambdaを作成したら、LambdaのARNを控えておきます。

## 3. Cognito Userpool から Lambda を呼び出すための実行権限を設定する

以下のコマンドを実行し、Cognito Userpoolから該当のLambdaを呼び出せるようにします。
`<lambda_arn>` には、先ほど作成したLambdaのARNを指定します。

```shell
aws lambda add-permission \
  --function-name <lambda_arn> \
  --statement-id "CognitoLambdaInvokeAccess" \
  --action lambda:InvokeFunction \
  --principal cognito-idp.amazonaws.com
```

## 4. Userpool にトリガーを設定する

Userpoolでメッセージを送信するタイミングで、作成したLambdaが呼ばれるようにトリガーを設定します。

- Eメールの送信で設定するトリガー: `CustomEmailSender`
- SMSの送信で設定するトリガー: `CustomSMSSender`

しかし、2021年1月現在ではこのトリガーをコンソールやCloudFormationで設定することができません。AWS CLIを使用した手順がドキュメントに記載されています。

```shell
# 既存の設定の確認
aws cognito-idp describe-user-pool --userpool-id <userpool_id>

# トリガーの反映
aws cognito-idp update-user-pool \
  --userpool-id <userpool_id> \
  --lambda-config "CustomEmailSender={LambdaVersion=V1_0,LambdaArn= <lambda_arn> },KMSKeyID= <key_id>"
```

トリガーを設定するには[update-user-pool](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cognito-idp/update-user-pool.html)コマンドを使用します。[^1]

- `<userpool_id>`: UserpoolのプールID
- `<lambda_arn>`: 先ほど作成したLambdaのARN
- `<key_id>`: KMS対称キーの**ARN** [^2]

このコマンドに関してはいくつか注意点があります。

- これらのトリガーは比較的最近のバージョンのAWS CLIでのみ実行可能です。バリデーションエラーが発生した場合はバージョンをご確認ください。[^3]
- ほかのトリガーを設定している場合（確認完了後、認証チャレンジ、ユーザー移行等）は、それらも一緒に `--lambda-config` に設定する必要があります。 `CustomEmailSender` のみを指定して上記コマンドを実行すると、それ以外のトリガーがすべて外れます。
- Lambda ARNを直接指定するほかのトリガーと異なり、今回増えた `CustomEmailSender` と `CustomSMSSender` の場合は `LambdaVersion` と `LambdaArn`によるオブジェクトをセットする必要があります。
- `LambdaVersion` は `V1_0` 固定です。Lambda自身のバージョンを指定できるパラメータではありません。

以上で設定は完了です。

# まとめ

AWS What's Newで見つけることができず発見が遅くなってしまったのですが[^4]、待っていた方も多いアップデートなのではないでしょうか。
これまでのカスタムメッセージLambdaでは不満だった方は、ぜひこのアップデートを試してみてください！

[^1]: ドキュメントではなぜかコマンド部分が割愛されています。なぜ……
[^2]: ARNを指定するのにパラメータ名は`KmsKeyID`なので注意。なぜ……
[^3]: このコマンドを実行するために CloudShell を使っていたのですが、 CloudShell 上のデフォルトのAWS CLIのバージョンでは古くて実行できませんでした（CloudShell上のAWS CLIも、[ドキュメント](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html#cliv2-linux-upgrade)に記載の手順でバージョンアップが可能です）。
[^4]: 改めて探してみたのですが、2020年11月にはそれらしきアップデートが見つからず。今回はたまたま英語版のドキュメント更新履歴を見て発見しました。それにしてもなぜこのアップデートだけ更新日が「November 2020」なんでしょう……?