EY-Office ブログ

Amazon Cognitoのユーザー移行Lambdaトリガーは便利

以前、AWS Cognitoを試してみたが今回は見送ったという記事を書きましたが、最近も別のシステムでCognitoの検討を行いました。
今回のシステムはすでにRDBにユーザー管理テーブルを持ちユーザー管理を行っているので、ユーザー管理テーブルからCognitoへの移行手段も考えなければいけません。

Amazon Cognito User Migration Lambda Triggers Are Convenient DeepAIが生成した画像です

ユーザー管理の移行は簡単ではない

ユーザー管理テーブルからCognitoへの移行ですが、現行のデータベースからユーザー管理テーブルをエクスポートしCognitoにインポートすれば良いわけではありません。
ユーザー管理テーブルには認証(ログイン)用のパスワードが書かれていますが、デーブルに書かれているのはパスワードから作ったハッシュ値(ダイジェスト値)です。現行の認証処理とCognitoの認証処理が全く同じなら良いですが、そうではありません。

移行を考えると従来のデーブルと認証処理もシステム内に残し、

  • すでにCognitoに登録されているユーザーの認証はCognitoで行う
  • まだ移行してないユーザーの認証は
    1. 画面から送られて来たメールアドレスとパスワードを従来の認証処理で認証を行なう
    2. 上で認証成功した場合は、送られて来たメールアドレスとパスワードでCognitoにユーザー登録を行う
    3. 以降のユーザーの認証はCognitoに移る

のような処理を作り、利用しているユーザーの移行が終わるのを待つ必要があります。面倒ですね。

CognitoのLambdaトリガー

Cognitoは高機能なユーザー管理のバックエンドですが、個々のシステム/サービスをすべての要望を満たせるわけではありません。そこでLambda トリガーを使用したユーザープールワークフローのカスタマイズに書かれているように、Lambdaを使って独自処理を組み込めるようになっています。

この中にユーザー移行のLambda トリガーというLambdaトリガーがあります。これを使うと上に書いた移行処理が簡単になる事がわかります。

Cognito側で

  • すでにCognitoに登録されているかの判断
    • 未登録な場合のみ、ユーザー移行Lambdaトリガーが呼び出されます
  • ユーザー移行Lambdaトリガーから認証OKが戻ってくればユーザー登録は自動的に行われます
    • その際にCognito側に移行したいデータがあればLambda内の処理で行えます

実際に試してみた

本当に簡単に移行処理が行えるのか簡単なサンプルを書いて試してみました(今回はRDS、Cognito、Lambda等の設定手順は省きました)。

もとの認証処理

もとの認証処理はLambdaで実装されていて、RDS上にあるPostgreSQLにユーザー管理用のusersテーブルがあり、email/passwordで認証を行っています。またLambdaからはRDS Proxyを通してRDS(PostgreSQL)接続されています。

  • ① パスワードのダイジェスト処理はbcryptを使っています
  • ② Lambdaのエントリー・ポイント
  • ③ RDS Proxy経由でRDSのPostgreSQL接続するための定型処理
    • region, username, database等は適宜設定してください
    • RDS Proxyのホスト名は環境変数HOSTから取得しています
  • ④ usersテーブルが引数で与えられたemailと一致するレコードを選択
  • ⑤ レコードが選択できたら、⑥⑦実行
  • ⑥ passwordカラムの値と、引数で与えられたpaasswordのダイジェストが一致するか判定
  • ⑦ パスワードが一致したらステータスコード200(成功)、一致しなければ401(認証失敗)を戻す
  • ⑧ このLambdaはHTTPステータスのみを戻します
const pg = require("pg");
const bcrypt = require("bcryptjs");       // ← ①
const AWS = require("aws-sdk");

exports.handler = async (event) => {      // ← ②

  const signer = new AWS.RDS.Signer({     // ← ③
    region: "ap-northeast-1",
    hostname: process.env.HOST,
    port: 5432,
    username: "postgres",
  });
  const token = signer.getAuthToken({     // ← ③
    username: "postgres",
  });
  const dbConfig = {                      // ← ③
    user: "postgres",
    password: token,
    port: 5432,
    database: "apps",
    host: process.env.HOST,
    ssl: true,
  };
  const client = new pg.Client(dbConfig);  // ← ③

  client.connect();
  const res = await client.query({text: "SELECT * FROM users WHERE email = $1",
    values: [event.email]});               // ← ④
  await client.end();

  let status = 401;
  if (res.rows.length > 0)  {              // ← ⑤
    const auth = await bcrypt.compare(event.password, res.rows[0].password); //←⑥
    status = auth ? 200 : 401;             // ← ⑦
  }

  const response = {                       // ← ⑧
    statusCode: status,
  };
  return response;
};

ユーザー移行Lambdaトリガー

Email + Passwordで認証を行うCognitoのユーザープールを作成し、そこに以下のようなユーザー移行Lambdaトリガーを定義します。

  • ① usersテーブルでの認証処理authenticateUser、もとの認証処理と同じコードです
  • ② ユーザー移行Lambdaトリガーのエントリー・ポイント
  • ③ トリガーは移行時の認証処理UserMigration_Authenticationの場合の処理
    • 他にパスワードを忘れた場合の処理UserMigration_ForgotPasswordもありますが、今回は省略しました
  • ④ 認証処理の呼び出し
  • ⑤ 認証成功なら⑥⑦を実行
  • ⑥ Congnitoに移行する属性をここに設定します、ここではメールアドレス
  • ⑦ ユーザー登録ステータスは確認済み(メールによるアクティベーション処理などは無し)
  • ⑧ ユーザー登録時の完了メール送信は無し
const pg = require("pg");
const bcrypt = require("bcryptjs");
const AWS = require("aws-sdk");

const authenticateUser = async (email, password) => { // ← ①

  const signer = new AWS.RDS.Signer({
    region: "ap-northeast-1",
    hostname: process.env.HOST,
    port: 5432,
    username: "postgres",
  });
  const token = signer.getAuthToken({
    username: "postgres",
  });
  const dbConfig = {
    user: "postgres",
    password: token,
    port: 5432,
    database: "apps",
    host: process.env.HOST,
    ssl: true,
  };
  const client = new pg.Client(dbConfig);

  client.connect();
  const res = await client.query({text: "SELECT * FROM users WHERE email = $1",
    values: [email]});
  await client.end();

  if (res.rows.length > 0)  {
    const auth = await bcrypt.compare(password, res.rows[0].password);
    if (auth) {
      return true;
    }
  }
  return false;
};

exports.handler = async (event) => {                            // ← ②
  console.log("=== handler ", event);
  if (event.triggerSource == "UserMigration_Authentication") {  // ← ③
                                                                // ↓ ④
    const auth = await authenticateUser(event.userName, event.request.password);
    if (auth) {                                                 // ← ⑤
      event.response.userAttributes = {                         // ← ⑥
        email: event.userName,
        email_verified: "true",
      };
      event.response.finalUserStatus = "CONFIRMED";             // ← ⑦
      event.response.messageAction = "SUPPRESS";                // ← ⑧
    }
  }
  return event;
};

まとめ

上のような簡単なコードを書くだけで、移行処理が簡単にできる事が確認できました。

ユーザー管理自体は利用者には直接価値をもたらす機能ではありません。しかしサービスの一部として、ちゃんと動いている必要があります。
Cognitoは、このようなユーザー管理をまとめて面倒見てくれる有用なサービスだと思います。しかも移行処理も組み込まれているのには感心しました。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ