🐴

Cognitoへのサインアップをトリガに動くLambdaをGoで書く

2023/10/11に公開

そもそも何がやりたかったか

Cognitoへのユーザの追加時に、ユーザが所属するテナントを判断して該当のCognitoグループに所属させるといったことを実現したかったです。

これを実現するため、以下のような設計を行いました。
前提として、ユーザが所属するテナントは予めアプリケーションのDBに保存されているものとします。(管理者ユーザはメールアドレスや所属テナントなど入力してDBに保存し、同時にユーザを招待するようなイメージ)

やったこと

Serverless Framework + Goを使って実装しました。
また、前提として、Cognitoはすでに作成済みのものを使いました。

1.serverless.ymlの記述

ベースとして、Serverless Frameworkのaws-go-modを使用しました。
(以下コマンドでテンプレートを生成できます。)

sls create --template aws-go-mod --path cognito-sign-up

こちらで生成されたベースに対して、以下のようにserverless.ymlを変更します。
(本記事と関係ない部分の記述は省略しています。)

provider:
  iam:
    role:
      statements:
        - Effect: "Allow"
          Action:
            - "cognito-idp:*"
          Resource:
            - "*"
functions:
  sign-up:
    handler: bootstrap
    events:
      - cognitoUserPool:
          pool: ${self:custom.cognito.${self:provider.stage}.poolName}
          trigger: PostConfirmation # サインアップ後
          existing: true
    vpc:
      securityGroupIds:
        - !Ref CognitoSignInLambdaSecurityGroup
      subnetIds: ${self:custom.vpc.${self:provider.stage}.subnetIds}

以下、補足です。

  • 関数のトリガとなるイベントとして、cognitoUserPoolPostConfirmationを設定します。
  • 既存のCognitoを使う時は、existing: trueを設定する必要があります。
  • VPC内のDBにアクセスするために、vpcの項目を設定していますが、DBへのアクセスが不要であれば設定も不要です。

2.Goの関数の記述

以下、長々とコードを貼り付けますがポイントは以下2点です。
ちなみに、コードの箇所は本記事とは関係ない部分は省略しているので、そのまま貼り付けただけでは動かないかもしれません。

引数と戻り値の型

  • 引数

    • aws-lambda-go/eventsCognitoEventUserPoolsPostConfirmationを受け取れます。
    • コードでも記載しているように、このリクエストから、Cognitoのユーザ名やemailを取得できます。
  • 戻り値

    • 引数で渡されたrequestと、errorがあればそれを返却します。

公式ドキュメントの該当箇所は以下です。

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/user-pool-lambda-post-confirmation.html

レスポンスは5秒以内に返却する必要がある

以下の公式ドキュメントの重要な考慮事項に記載の通り、レスポンスは5秒以内に返却する必要があり、しかもこの値は変更することができません。
仮に5秒以上かかる処理を実行した場合、ログインに失敗する形となります。

https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html#important-lambda-considerations

import (
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
)

func Handler(request events.CognitoEventUserPoolsPostConfirmation) (events.CognitoEventUserPoolsPostConfirmation, error) {

	// サインインしたユーザの情報を取得
	userName := request.UserName
	email := request.Request.UserAttributes["email"]

	// DB接続
	db, err := sql.Open("postgres", "host="+dbHost+" dbname="+dbName+" user="+dbUser+" password="+dbPass)
	if err != nil {
		log.Panicln("DB Connection Error: ", err)
	}
	defer db.Close()

	// ユーザが所属しているテナントを取得
	query := `SELECT "company"."companyCode" FROM "user" INNER JOIN "company" ON "user"."companyId" = "company"."id" WHERE "user"."email" = $1`
	rows, err := db.Query(query, email)
	if err != nil {
		log.Panicln("DB Query Error: ", err)
	}
	defer rows.Close()

	var companyCodes []string
	for rows.Next() {
		var code string
		err := rows.Scan(&code)
		if err != nil {
			log.Panicln("Row Scan Error: ", err)
		}
		companyCodes = append(companyCodes, code)
	}

	// ユーザを各テナントのCognitoグループに追加
	cfg, _ := config.LoadDefaultConfig(context.TODO()) // 外部リソース(~/.aws/config など)からコンフィグ情報 (aws.Config) を生成
	client := cognitoidentityprovider.NewFromConfig(cfg)

	for i := 0; i < len(companyCodes); i++ {
		_, err := client.AdminAddUserToGroup(context.TODO(), &cognitoidentityprovider.AdminAddUserToGroupInput{
			GroupName:  aws.String(companyCodes[i]),
			Username:   aws.String(userName),
			UserPoolId: aws.String(cognitoUserPoolId),
		})
		if err != nil {
			log.Panicln("AdminAddUserToGroup Error: ", err)
		}
	}
	return request, nil
}

Discussion