Cognito UserPoolのサインアップ時に招待コードを検証する
目的
Webアプリを公開するとき、ユーザが想定外に一気に増えると恐ろしいですね。そんな状況を防ぐため、希望者に招待コードを発行し、正規の招待コードを持つ人だけがサインアップできるようにしたいです。ユーザ管理はCognito UserPoolを使う前提で、招待コードを入力させて検証する仕組みを実装します。
Cognito UserPoolの作り方は、以前の記事に従うものとして、差分だけ説明します。
出来上がりイメージです。不正な招待コードを入力してサインアップ不可になっている様子です。
UserPoolの設定
サインアップエクスピリエンスにて、招待コードを表すカスタム属性を追加しましょう。invitation
という名前にし、長さは6で固定にしてみました。
続いてユーザプールのプロパティにて、Lambdaトリガーを設定します。このLambdaがサインアップ時に呼ばれますので招待コードを検証するロジックを書けばいいという仕組みですね。Lambdaの実装は後述しますが、サインアップ前に動作するLambdaとして登録します。
Lambdaの実装
ロジック
今回、超簡易的な招待コードを考えてみました。
- まず適当な素数を用意します。 例) 1893
- 1893を適当な自然数倍した数字を招待コードとします。 例) 1893 * 123 => 232839
- 検証としては、この素数で割り切れるかチェックするだけです。
6桁の数字の中に1893の倍数は528個あるので、デタラメに打ち込むと0.05%の確率で当たります。まあ十分小さいリスクでしょう。公開鍵暗号方式のおもちゃバージョンみたいな考え方ですね。
この方式の良いところは、検証が割り算だけなので参照先が不要ですし、招待コード生成ツールなど作らなくても、いつでもどこでも招待コード頂戴と言われた時にささっと計算して発行できるところです。
実装
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