every Tech Blog

株式会社エブリーのTech Blogです。

Amazon Cognitoのユーザーの移行

はじめに

エブリーでソフトウェアエンジニアをしている本丸です。
最近、Amazon Cognitoのユーザープールから別のユーザープールにユーザーを移行する方法について調査する機会がありました。
Amazon Cognitoに関しては色々な箇所で使われていると思いますが、ユーザーの移行について触れる機会はそれほど多くないかと思いますので紹介しようかと思います。

Amazon Cognitoとは

Amazon Cognito(以降Cognitoと表記します)は、AWSが提供するウェブアプリとモバイルアプリ用のアイデンティティプラットフォームです。ユーザーの認証・承認を行うユーザープールとユーザーにAWSリソースへのアクセスを許可するアイデンティティプールを持っています。

DELISH KITCHENでは、ユーザーのメールアドレスの管理とメールアドレスを用いたサインインにCognitoのユーザープールを利用しています。

Lambdaトリガー

CognitoにはLambdaトリガーという機能があり、ユーザープールに対してサインインなどのイベントが発生した時に、それをトリガーとしてLambdaを呼び出すことができます。

公式ドキュメントからの引用ですが、Lambdaトリガーとして設定できるイベントには下記のようなものがあります。

トリガーの種類 説明
認証前の Lambda トリガー サインインリクエストを承認または拒否するカスタム検証
サインアップ前の Lambda トリガー サインアップリクエストを承認または拒否するカスタム検証を実行する
ユーザー移行の Lambda トリガー 既存のユーザーディレクトリからユーザープールにユーザーを移行する
カスタムメッセージの Lambda トリガー メッセージの高度なカスタマイズとローカライズを実行する

Cognitoのユーザープールへのインポート

ユーザープールへのインポート・移行方法は2つ用意されています。CSVを用いた方法とLambdaトリガーを用いた方法です。

CSVを用いたインポート

CSVを用いたインポートでは、指定されたフォーマットのCSVファイルを使用して一括でユーザーのインポートを行います。公式ドキュメントでは低労力で低コストなオプションとして紹介されていました。
こちらの方法では、セキュリティの観点からパスワードのインポートができないようになっています。そのため、移行の際にユーザー側でパスワードの変更が必要になります。

Lambdaトリガーを用いたインポート

Lambdaトリガーを利用したインポートでは、前述したLambdaトリガーを起点にユーザーの移行を行います(上述の表のユーザー移行の Lambda トリガーが今回説明するトリガーです)。このトリガーは、ユーザーがサインインする時とパスワードのリセットを行うときに発火します。
こちらの方法では、パスワードも連携されるのですが認証フローにUSER_PASSWORD_AUTHまたはADMIN_USER_PASSWORD_AUTHを指定し、ユーザー名とパスワードによる認証を行わなければならない点に注意です。 少しイメージしにくいかと思うので、次でもう少し詳細に説明します。

Lambdaトリガーを用いたユーザーの移行の実装

Lambdaトリガーを用いたユーザー移行の流れはおおよそ図のようになります。

ユーザーを移行したい先のアプリケーションでサインインもしくはパスワードリセットが呼び出されたのをトリガーにしてユーザー移行のLambda(図のuser migration lambda)が呼び出されます。
ユーザー移行のLambdaの実装は次のようになります。

import { 
  CognitoIdentityProviderClient,
  AdminInitiateAuthCommand,
  AdminGetUserCommand,
  CognitoIdentityProviderClientConfig,
  UserNotConfirmedException
} from "@aws-sdk/client-cognito-identity-provider";
import { UserMigrationTriggerHandler } from "aws-lambda";

const userMigration: UserMigrationTriggerHandler = async (event) => {
    const config: CognitoIdentityProviderClientConfig = {
        region: 'ap-northeast-1',
    };
    const client = new CognitoIdentityProviderClient(config);

    if(event.triggerSource == "UserMigration_Authentication") {
        const adminInitiateAuthCommand = new AdminInitiateAuthCommand({
            UserPoolId: ${USER_POOL_ID}, 
            ClientId: ${CLIENT_ID},
            AuthFlow: "ADMIN_USER_PASSWORD_AUTH", 
            AuthParameters: {
                "USERNAME": event.userName,
                "PASSWORD": event.request.password,
            },
        });
        // 認証できるかチェック
        try {
            await client.send(adminInitiateAuthCommand)
        } catch (e) {
            console.log(`user auth failed: ${e.message}`);
            throw e;
        }

        // cognitoに登録するユーザー情報構築
        const adminGetUserCommand = new AdminGetUserCommand({
            UserPoolId: process.env.DK_USER_POOL_ID,
            Username: event.userName,
        });

        try {
            const response = await client.send(adminGetUserCommand);
            // 移行先のユーザーに持たせたい情報を詰め込む
            event.response.userAttributes = {
                "email": response.UserAttributes.find((attr) => attr.Name === "email")?.Value ?? "",
                "email_verified": response.UserAttributes.find((attr) => attr.Name === "email_verified")?.Value ?? "false",
            };
            // 検証メールを送信しないため、下記を指定する
            event.response.messageAction = "SUPPRESS";
            event.response.finalUserStatus = "CONFIRMED";
            return event;
        } catch (e) {
            console.log(`get user failed: ${e.message}`);
            throw e;
        }
    }
    return event;
};

このユーザー移行のLambdaの中で、移行元となるCognitoでユーザーの認証を行い、認証が成功した場合にユーザーの情報を取得します。その情報を移行先のCognitoに返すことでユーザーの移行を行います。 event.responseに渡すデータを変更することで移行先のユーザーに持たせたい情報を変更したり、ユーザーがそのメールアドレスの正当な所有者であるかを確認するメールを送信するかなどを操作することができます。

まとめ

Cognitoのユーザーの移行方法を調査して、ユーザーの移行を行うためにAWS公式で便利な機能が用意されていることを知ることができました。
2つの方法にそれぞれメリット・デメリットがあるかと思うので適切に使うようにしていきたいと思います。

参考資料