🙆‍♀️

Cognito UserPoolのサインアップ時に招待コードを検証する

2023/08/08に公開

目的

Webアプリを公開するとき、ユーザが想定外に一気に増えると恐ろしいですね。そんな状況を防ぐため、希望者に招待コードを発行し、正規の招待コードを持つ人だけがサインアップできるようにしたいです。ユーザ管理はCognito UserPoolを使う前提で、招待コードを入力させて検証する仕組みを実装します。

Cognito UserPoolの作り方は、以前の記事に従うものとして、差分だけ説明します。
https://zenn.dev/myonie/articles/9b5178aa91d7e0

出来上がりイメージです。不正な招待コードを入力してサインアップ不可になっている様子です。

UserPoolの設定

サインアップエクスピリエンスにて、招待コードを表すカスタム属性を追加しましょう。invitationという名前にし、長さは6で固定にしてみました。

続いてユーザプールのプロパティにて、Lambdaトリガーを設定します。このLambdaがサインアップ時に呼ばれますので招待コードを検証するロジックを書けばいいという仕組みですね。Lambdaの実装は後述しますが、サインアップ前に動作するLambdaとして登録します。

Lambdaの実装

ロジック

今回、超簡易的な招待コードを考えてみました。

  • まず適当な素数を用意します。 例) 1893
  • 1893を適当な自然数倍した数字を招待コードとします。 例) 1893 * 123 => 232839
  • 検証としては、この素数で割り切れるかチェックするだけです。

6桁の数字の中に1893の倍数は528個あるので、デタラメに打ち込むと0.05%の確率で当たります。まあ十分小さいリスクでしょう。公開鍵暗号方式のおもちゃバージョンみたいな考え方ですね。

この方式の良いところは、検証が割り算だけなので参照先が不要ですし、招待コード生成ツールなど作らなくても、いつでもどこでも招待コード頂戴と言われた時にささっと計算して発行できるところです。

実装

Lambda関数名=invitationCheckTest
N = 1893

def lambda_handler(event, context):
    try:
        invitation_code = int(event["custom:invitation"])
        assert invitation_code % N == 0
    except (ValueError, KeyError, AssertionError):
        raise Exception("You don't have valid invitation code.")
        
    return event

フロントエンド

フロントエンドはReactで実装しており、aws-amplifyのAuthenticatorを使ってサインイン・サインアップ画面を用意します。以下のように、AuthenticatorのformFieldsに招待コードを入れるフォームを用意すればOKです。なお本題と関係ありませんが、テーマをDarkにしています。

import React from "react";

import {
  Authenticator, 
  useTheme,
  ColorMode,
  defaultDarkModeOverride,
  ThemeProvider,
} from "@aws-amplify/ui-react";
import { Amplify } from "aws-amplify";

import "@aws-amplify/ui-react/styles.css";

Amplify.configure({
  Auth: {
    region: "ap-northeast-1",
    userPoolId: process.env.REACT_APP_USER_POOL_ID,
    userPoolWebClientId: process.env.REACT_APP_APP_CLIENT_ID,
  },
});

const App: React.FC = () => {
  const { tokens } = useTheme();
  const colorMode: ColorMode = "dark";
  const theme = {
    name: "my-theme",
    overrides: [defaultDarkModeOverride],
  }; 

  // 招待コードを追加したフォームフィールド	
  const formFields = {
    signUp: {
      "custom:invitation": {
        placeholder: "Enter your invitation code",
        order: 1,
      },
      username: {
        order: 2,
      },
      email: {
        order: 3,
      },
      password: {
        order: 4,
      },
      confirm_password: {
        order: 5,
      },
    },
  };

  return (
    <ThemeProvider theme={theme} colorMode={colorMode}>     
        <Authenticator
          initialState="signIn"
          formFields={formFields}         
        >
          {({ user, signOut }) => <Home user={user} signOut={signOut} />}
        </Authenticator>       
    </ThemeProvider>
  );
};

export default App;

参考: CDK

Cognitoの関連する部分をCDKで書くとこんな感じ

    // Cognito
    const userPool = new cognito.UserPool(this, "userPool", {
      selfSignUpEnabled: true,
      signInAliases: {
        username: true,
        email: true,
      },
      email: cognito.UserPoolEmail.withCognito(),
      customAttributes: {
        invitation: new cognito.StringAttribute({
          minLen: 6,
          maxLen: 6,
          mutable: true,
        }),
      },
    }); 

    userPool.addTrigger(
      cognito.UserPoolOperation.PRE_SIGN_UP,
      new lambda.Function(this, "invitationCheckTest", {
        runtime: lambda.Runtime.PYTHON_3_10,
        code: "path_to_lambda's code",
        handler: "xxx.lambda_handler",
        functionName: "invitationCheckTest",       
      })
    );

Discussion