見出し画像

AWS CDKを使ってCognitoとReactでサインインとサインアップ機能を作ってみた!

はじめに

こんにちは、株式会社SHIFT DAAE部のさとうです。
AWS CDKの勉強のために記事を執筆することにしました。
AWS CDKでCognitoを作成し、ローカルで起動したReactアプリケーションでサインインとサインアップ機能を作成し、トークンを取得するところまでを記載しています。

CognitoとReactアプリケーション間の繋ぎこみはamazon-cognito-identity-jsというライブラリーを利用しています。他にもamplifyのライブラリーを選択肢としてはあるのですが、今回はamplifyを利用していないため、amazon-cognito-identity-jsを利用しています。

amazon-cognito-identity-js:https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js

amplify:https://docs.amplify.aws/lib/auth/getting-started/q/platform/js/

対象者

  • AWS CDKとReactを利用して簡単なインフラとアプリを作ってみたい方

環境

  • WSL2

  • Node.js :16.15.0

  • AWS CDK:2.45.0

  • React:18.2.0

AWS CLIのセットアップ

以下の記事に沿ってセットアップを行います。

https://aws.amazon.com/jp/getting-started/guides/setup-cdk/module-one/

※セットアップについてはDAAE部の栗山さんが記事を出しているため、そちらも参考にしてください!!

https://note.com/shift_tech/n/n955378812278?magazine_key=m5a7af0c33398

まず私の環境ではAWS CLIをインストールしていなかったため、インストールしていきます。

以下の記事に従ってインストールしていきます。

https://aws.amazon.com/jp/getting-started/guides/setup-environment/module-three/ https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html

以下のコマンドを実行するとAWS CLIのセットアップが完了します。

$ sudo apt install unzip
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install
You can now run: /usr/local/bin/aws --version
$ aws --version
aws-cli/2.8.2 Python/3.9.11 Linux/5.10.16.3-microsoft-standard-WSL2 exe/x86_64.ubuntu.20 prompt/off 

次にAWSの認証情報をセットアップします。

※Access Key IDとSecret Access Keyは秘密情報のためぼかしています。

$ aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXXXX
AWS Secret Access Key [None]: XXXXXXXXXXXXXXXXXXXXXXXXXXXX
Default region name [None]: ap-northeast-1
Default output format [None]: json

これでAWS CLIの準備が整ったため、コマンドが実行できることを確認します。

$ aws ec2 describe-vpcs
{
    "Vpcs": [
        {
            "CidrBlock": XXX
            ・・・
        }
    ]
}

AWS CDKのインストール

次にAWS CDKをグローバルにインストールします。

$ npm install -g aws-cdk
$ cdk --version
2.45.0 (build af1fb7c)

このままではデプロイができず、新しい環境にCDKをデプロイする際、ブートストラップという作業が必要になります。

$ cdk bootstrap aws://XXXXXXXXXXXXXXXXX/ap-northeast-1Bootstrapping environment aws://XXXXXXXXXXXXXXXXX/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
・・・
 ✅  Environment aws://XXXXXXXXXXXXXXXXX/ap-northeast-1 bootstrapped.

CloudFormationを確認すると以下のようにスタックが作成されたことが確認できます。

次に以下のコマンドを実行し、今回利用するスタックを作成します。

$ mkdir cdk-sample
$ cd cdk-sample/
$ cdk init --language typescript
Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.
・・・
Executing npm install...
✅ All done!

実際にデプロイしてみます。

$ cdk deploy

✨  Synthesis time: 4.62s

CdkSampleStack: building assets...
・・・
✨  Total time: 22.53s

スタックが作成されたことが確認できました。

Cognitoのデプロイ

次にCognitoをデプロイします。以下のコードを記載してデプロイします。

import * as cdk from "aws-cdk-lib";
import * as cognito from "aws-cdk-lib/aws-cognito";
import { Construct } from "constructs";

export class CdkSampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // User Pool
    const userPool = new cognito.UserPool(this, "userpool", {
      userPoolName: "sample-user-pool",
      selfSignUpEnabled: true,
      signInAliases: {
        email: true,
      },
      accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
    });

    // User Pool Client
    const userPoolClient = new cognito.UserPoolClient(this, "userpool-client", {
      userPool,
      authFlows: {
        adminUserPassword: true,
        custom: true,
        userSrp: true,
      },
      supportedIdentityProviders: [
        cognito.UserPoolClientIdentityProvider.COGNITO,
      ],
    });
  }
}
$ cdk deploy

✨  Synthesis time: 4.54s
・・・

✨  Total time: 27.57s

実際にリソースが作成されたことが確認できました。

トークン取得処理の実装

ユーザーサインアップ→ユーザーの確認→ユーザー認証を行いトークンを取得するところまで実施します。

まずはReactプロジェクトを作成します。

npx create-react-app --template typescript cdk-app

今回は簡単なフォームを作成するので、MUIとReact Hook Formを利用してぱぱっと用意します。 ライブラリをインストールします。

  • MUI

npm install @mui/material @emotion/react @emotion/styled
  • React Hook Form

npm install react-hook-form
  • amazon-cognito-identity-js

npm install amazon-cognito-identity-js

ディレクトリ構成は以下のようにしています。
今回はフォームのエラー出力や認証のエラーハンドリングは特に行っていません。

またcreate-react-appで作成した雛形から更新・新規作成したソースのみ記載しています。

─── src
│   ├── App.tsx
│   ├── SignUp.tsx
│   ├── ConfirmRegistration.tsx
│   ├── SignIn.tsx
│   ├── config.ts
└── .env

コードは以下のように記載しています。

  • App.tsx

import { useState } from "react";
import { SignUp } from "./SignUp";
import { ConfirmRegistration } from "./ConfirmRegistration";
import { SignIn } from "./SignIn";
// ref:https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js

const App = () => {
  const [status, setStatus] = useState("unregistered");
  const [email, setEmail] = useState("");

  return (
    <>
      {status === "unregistered" && (
        <SignUp setStatus={setStatus} setEmail={setEmail} />
      )}
      {status === "unconfirmed" && (
        <ConfirmRegistration setStatus={setStatus} email={email} />
      )}
      {status === "confirmed" && <SignIn />}
    </>
  );
};

export default App;
  • SignUp.tsx

import React from 'react'
import { Button, Container, Stack, TextField } from "@mui/material";
import * as AmazonCognitoIdentity from "amazon-cognito-identity-js";
import { getUserPool } from "./config";
import { SubmitHandler, useForm } from "react-hook-form";

interface SignUpForm {
  email: string;
  password: string;
}

interface Props {
  setStatus: React.Dispatch<React.SetStateAction<string>>;
  setEmail: React.Dispatch<React.SetStateAction<string>>;
}

export const SignUp = (props: Props) => {
  const { register, handleSubmit } = useForm<SignUpForm>();

  const signUp: SubmitHandler<SignUpForm> = (data) => {
    const formValue = {
      email: data.email,
      password: data.password,
    };

    const attributeList = [];

    const dataEmail = {
      Name: "email",
      Value: data.email,
    };

    const attributeEmail = new AmazonCognitoIdentity.CognitoUserAttribute(
      dataEmail
    );

    attributeList.push(attributeEmail);

    const validationData: AmazonCognitoIdentity.CognitoUserAttribute[] = [];

    const userPool = getUserPool();

    userPool.signUp(
      formValue.email,
      formValue.password,
      attributeList,
      validationData,
      (err, result) => {
        if (err) {
          alert(err.message || JSON.stringify(err));
          return;
        }
        const cognitoUser = result!.user;
        console.log("user name is " + cognitoUser.getUsername());
        props.setStatus("unconfirmed");
        props.setEmail(data.email);
      }
    );
  };

  return (
    <Container className="unregistered" maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={2}>
        <TextField
          required
          label="メールアドレス"
          type="email"
          {...register("email")}
        />
        <TextField
          required
          label="パスワード"
          type="password"
          {...register("password")}
        />
        <Button
          onClick={handleSubmit(signUp)}
          color="primary"
          variant="contained"
        >
          送信
        </Button>
      </Stack>
    </Container>
  );
};
  • ConfirmRegistration.tsx

import React from 'react'
import { Button, Container, Stack, TextField } from "@mui/material";
import * as AmazonCognitoIdentity from "amazon-cognito-identity-js";
import { getUserPool } from "./config";
import { SubmitHandler, useForm } from "react-hook-form";

interface ConfirmRegistrationForm {
  code: string;
}

interface Props {
  setStatus: React.Dispatch<React.SetStateAction<string>>;
  email: string;
}

export const ConfirmRegistration = (props: Props) => {
  const { register, handleSubmit } = useForm<ConfirmRegistrationForm>();

  const confirmRegistration: SubmitHandler<ConfirmRegistrationForm> = (
    data
  ) => {
    const userPool = getUserPool();
    const userData = {
      Username: props.email,
      Pool: userPool,
    };
    const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
    cognitoUser.confirmRegistration(data.code, true, (err, result) => {
      if (err) {
        alert(err.message || JSON.stringify(err));
        return;
      }
      props.setStatus("confirmed");
    });
  };

  return (
    <Container className="unconfirmed" maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={2}>
        <TextField required label="認証コード" {...register("code")} />
        <Button
          onClick={handleSubmit(confirmRegistration)}
          color="primary"
          variant="contained"
        >
          送信
        </Button>
      </Stack>
    </Container>
  );
};
  • SignIn.tsx

import { Alert, Button, Container, Stack, TextField } from "@mui/material";
import * as AmazonCognitoIdentity from "amazon-cognito-identity-js";
import { getUserPool } from "./config";
import { SubmitHandler, useForm } from "react-hook-form";
import { useState } from "react";

interface SignInForm {
  email: string;
  password: string;
}

export const SignIn = () => {
  const { register, handleSubmit } = useForm<SignInForm>();
  const [message, setMessage] = useState("サインアップ成功");

  const signIn: SubmitHandler<SignInForm> = (data) => {
    const userPool = getUserPool();
    const userData = {
      Username: data.email,
      Pool: userPool,
    };

    const authenticationData = {
      Username: data.email,
      Password: data.password,
    };
    const authenticationDetails =
      new AmazonCognitoIdentity.AuthenticationDetails(authenticationData);

    const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData);
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: (result) => {
        const accessToken = result.getAccessToken().getJwtToken();
        const idToken = result.getIdToken().getJwtToken();
        const refreshToken = result.getRefreshToken().getToken();
        const expires = result.getAccessToken().getExpiration() * 1000;
        console.log(accessToken);
        console.log(idToken);
        console.log(refreshToken);
        console.log(expires);
        setMessage("サインイン成功");
      },

      onFailure: (err) => {
        alert(err.message || JSON.stringify(err));
      },
    });
  };

  return (
    <Container className="signIn" maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={2}>
        <Alert severity="success">{message}</Alert>
        <TextField
          required
          label="メールアドレス"
          type="email"
          {...register("email")}
        />
        <TextField
          required
          label="パスワード"
          type="password"
          {...register("password")}
        />
        <Button
          onClick={handleSubmit(signIn)}
          color="primary"
          variant="contained"
        >
          送信
        </Button>
      </Stack>
    </Container>
  );
};
  • config.tsx

import * as AmazonCognitoIdentity from "amazon-cognito-identity-js";

export const getUserPool = () => {
  const poolData = {
    UserPoolId: process.env.REACT_APP_COGNITO_USER_POOL_ID || "",
    ClientId: process.env.REACT_APP_COGNITO_USER_POOL_CLIENT_ID || "",
  };

  const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData);
  return userPool;
};
  • .env

REACT_APP_COGNITO_USER_POOL_ID="XXXXXXXXXXXXXXXXXXXX"
REACT_APP_COGNITO_USER_POOL_CLIENT_ID="XXXXXXXXXXXXXXXXXXXXXXXXX"

※envファイルには作成したCognitoのユーザープールIDとユーザープールクライアントIDを記載します。 以下のコマンドから取得できます。

$ aws cognito-idp list-user-pools --max-results 20
{
    "UserPools": [
        {
            "Id": "XXXXXXXXXXXXXXXXXXXX",
            "Name": "sample-user-pool",
            "LambdaConfig": {},
            "LastModifiedDate": "2022-11-10T17:26:37.541000+09:00",
            "CreationDate": "2022-11-10T17:26:37.541000+09:00"
        }
    ]
}

$ aws cognito-idp list-user-pool-clients --user-pool-id XXXXXXXXXXXXXXXXXXXX
{
    "UserPoolClients": [
        {
            "ClientId": "XXXXXXXXXXXXXXXXXXXXXXXXX",
            "UserPoolId": "XXXXXXXXXXXXXXXXXXXX",
            "ClientName": "userpoolclientXXXXXXXXX"
        }
    ]
}

それでは実際に動作確認を行います。
npm run startを実行し、アプリを起動させるとサインアップ画面が表示されます。
ファイルとしては「SignUp.tsx」が表示されています。

メールアドレスとパスワードを入力し、「送信」ボタンを押します。

次に認証コードの入力画面に遷移します。ファイルとしては「ConfirmRegistration.tsx」が表示されています。

少しするとメールアドレスに認証コードが送られるため、画面上に入力し、「送信」ボタンを押します。

ちなみにこの時のユーザープールのユーザーは以下のようになっており、確認ステータスが「未確認」となっています。

認証コードの確認が成功すると、サインイン画面に遷移します。
ファイルとしては「SignIn.tsx」が表示されています。

この時のユーザープールのユーザーは以下のようになっており、確認ステータスが「確認済み」となっています。

先ほど登録したメールアドレスとパスワードを入力することで、ログにトークン情報が表示され、トークンが取得できたことが確認できます。

最後に

今回はAWS CDKでCognitoを作成し、ローカルで起動したReactアプリケーションでサインインとサインアップ機能を作成し、トークンを取得する処理を実装してみました。

今度はステップアップしてAWS CDKでAPIGateway+Lambda+DynamoDBでサーバレスアプリを作っていきたいですね。


執筆者プロフィール:さとう
DAAE部所属の開発エンジニアです。フルスタックなエンジニアになれるよう頑張ります。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら 
https://recruit.shiftinc.jp/career/