Amazon Cognitoでユーザ認証機能をサクっと作ってみる

# はじめに
Amazon Cognitoはユーザ認証・認可機能を提供するサービスです。ユーザ認証はCognitoの機能で行うことも、GoogleやFacebookなどのソーシャルプロバイダーと連携して行うこともできます。
認証が完了したユーザにリソースへのアクセス権を与えるのが認可で、Cognitoを使うとユーザ認証から認証完了後にAWSリソースへのアクセス権の割り当てるところまでをワンストップ、且つ簡単に実装できます。ここではその一例としてAWS S3にファイルアップロードできるアプリ（ログイン機能付き）を作成してみたいと思います。

# 手順
1. Cognitoユーザプールを作成する
1. CognitoIDプールを作成する
1. S3を作成する
1. IAMロールを編集する
1. Webアプリを作成する
1. ログインユーザを登録する

## Cognitoユーザプールを作成する
Cognitoユーザプールとは前述のユーザ認証機能で使用するデータプールになります。ユーザプールを作成すると、認証情報（ID/パスワード）が登録できるようになります。ユーザプールはAWSコンソールから数ステップで作成できます。

#### ユーザプールの作成方法
* AWSコンソールにログインし、Amazon Cognitoのサービスを検索し、メニュー画面から「ユーザプールの管理」を選択します。
* ユーザプールの管理画面の右上にある「ユーザプールを作成する」ボタンを押すと、作成画面が現れるため、プール名に適当な名前をつけて、「デフォルトを確認する」から設定を行います。
* デフォルト設定が表示されるので、そのままで「プールの作成」ボタンを押します。
* プールが作成されるので、左側のメニューから全般設定-アプリクライアントを選択します。
* 「アプリクライアントを追加」を選択すると、アプリクライアントの設定画面が表示されます。アプリクライアント名に適当な名前を付けてから、クライアントシークレットを生成のチェックボックスを外した上で「アプリクライアントの作成」を押します。
* 次の画面で表示される「アプリクライアント ID」をメモっておきます。

これでユーザプールが作成できました。

## CognitoIDプールを作成する
CognitoIDプールとはユーザ認可機能の方で使用するデータプールになります。認証済みのユーザーに対して、任意のIAMロールを割り当てることでIAMロールで定義されたリソースへのアクセスが可能になります。認証プロバイダーとして、上記で作成したユーザプールを紐付けることで、ユーザプールで認証を行い、IDプールで認可するという連携が可能になります。

#### IDプールの作成方法
* AWSコンソールからAmazon Cognitoのサービスを検索し、メニュー画面から「IDプールの管理」を選択します。
* IDプール作成画面が表示されるので、適当なIDプール名を付与し、認証IDプロバイダーのCognitoタブのところに上で作成したユーザプールIDとアプリクライアントIDを入力した上で、「プールの作成」ボタンを押します。
* 「Identify the IAM roles to use with your new identity pool」という画面が現れます。ここでは未認証/認証済みのユーザに割り当てるIAMロールを定義します。IAMロール名はそれぞれCognito_（IDプール名）Unauth_Role/Cognito_（IDプール名）Auth_Roleという名前が割り当てられます。ここでは何も変えずに「許可」ボタンを押します。

これでIDプールが作成できました。

## S3を作成する
ローカルファイルのアップロード先となるS3バケットを作成します。

#### S3の作成方法
* AWSコンソールからS3のサービスを検索し、メニュー画面の右上の方にある「バケットを作成」ボタンを押します。
* 適当なバケット名を付けて、他はデフォルトのままで「バケットを作成」ボタンを押します。

ちなみにデフォルトではCORSが許可されていないため、外部（別ドメイン）からバケットにファイルアップロードしようとすると失敗します。このため作成したバケットにCORS設定を行います。

#### CORSの設定方法
* 作成したバケットの「アクセス許可」タブの一番下にある「Cross-Origin Resource Sharing (CORS)」の編集ボタンを押します。
* 開いたエディタに以下の内容をペーストして「変更の保存」ボタンを押します。

```
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]

```

これでS3バケットが作成できました。

## IAMロールを編集する
IDプール作成時に割り当てられたIAMロールについては、デフォルトでは何の権限も付与されていません。このため、認証済みロール（Cognito_（IDプール名）Auth_Role）に対して、S3バケットへのアクセス権を付与します。これを行うことで、ユーザ認証が完了するとS3バケットへのアクセスが可能となります。

#### IAMロールの編集方法
* AWSコンソールからIAMのサービスを検索し、左側のメニューバーにある「ロール」を選択します。
* 「Cognito_（IDプール名）Auth_Role」で検索を行い、表示結果を選択します。
* 許可タブの右上にある「許可を追加」ボタンを押して、更に表示される「ポリシーをアタッチ」を選択します。
* 「S3」で検索を行い、表示結果から「AmazonS3FullAccess」のチェックボックスにチェックを入れて、「ポリシーをアタッチ」ボタンを押します。

これで認証済みIAMロールへの権限付与ができました。

## Webアプリを作成する
AWS側の準備ができたので、Webアプリ（React）を作成します。

アプリはCreateReactAppを使って構築します。
```
npx create-react-app my-app
cd my-app
```

AWS SDKを使うため事前にインストールしておきます。
```
npm i aws-sdk
npm i amazon-cognito-identity-js
```
なおSDKの設定については下記を参照してください。
https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/configuring-the-jssdk.html


次にmy-appディレクトリに作成されたファイル群のうち、以下のファイルについて内容を書き換えます。

```html:/public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>Cognito demo app</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
```
```react:/src/App.jsx
import './App.css';
import { useState } from 'react'

var AWS = require("aws-sdk");
var AmazonCognitoIdentity = require('amazon-cognito-identity-js');

function App() {

  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [s3Client, setS3Client] = useState('')
  const [loginFlg, setLoginFlg] = useState(false)
  const [uploadFile, setUploadFile] = useState('')

  // AWS関連のパラメータ設定
  const Region = '（AWSリージョン名）'
  const IdentityPoolId = '（CognitoIDプールのID）'　
  const poolData = {
    UserPoolId: '（CognitoユーザプールのID）'
    ClientId: '（ユーザプールのアプリクライアントID）', 
  };
  const CognitoId = 'cognito-idp.（リージョン名）.amazonaws.com/（ユーザプールID）'
  const BucketName = '（S3バケット名）'

  // ログイン用の関数
  const login = () => {
    try {
      // 認証情報の設定
      const authenticationData = {
        Username: email,
        Password: password,
      }
      const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails(
        authenticationData
      ) 
      const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData)
      const userData = {
        Username: email,
        Pool: userPool,
      }
      const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData)
      
      // ユーザ認証
      cognitoUser.authenticateUser(authenticationDetails, {
        onSuccess: function(result) {
  
          //　認証成功の場合、認可情報をSDKに設定
          const idToken = result.getIdToken().getJwtToken()
          AWS.config.region = Region
          AWS.config.credentials = new AWS.CognitoIdentityCredentials({
            IdentityPoolId: IdentityPoolId, 
            Logins: {
              [CognitoId]: idToken,
            },
          })
      
          //　認可情報をSDKに反映
          AWS.config.credentials.refresh(error => {
            if (error) {
              alert('Login failure!')
              console.error(error);
            } else {
              const s3 = new AWS.S3({
                apiVersion: "2006-03-01",
                params: { Bucket: BucketName }
              });
              setS3Client(s3)
              setLoginFlg(true)
              alert('Successfully login!')
            }
          });
        },
      
        onFailure: function(err) {
          alert('Login failure!')
          console.log(err.message || JSON.stringify(err));
        },
      });
    } catch(e) {
      alert('Login failure!')
      console.log(e)
    }
  }

  // ログアウト用の関数
  const logout = () => {
    try {
      // 認証情報の設定
      const userPool = new AmazonCognitoIdentity.CognitoUserPool(poolData)
      const userData = {
        Username: email,
        Pool: userPool,
      }
      const cognitoUser = new AmazonCognitoIdentity.CognitoUser(userData)
  
      // ログアウト
      cognitoUser.signOut();
      setS3Client('')
      setLoginFlg(false)

      alert('Successfully logout!')
    } catch(e) {
      alert('Logout failure!')
      console.log(e)
    }
  }

  // ファイルアップロード用の関数
  const fileUpload = async() => {
    try {
      const upload = new AWS.S3.ManagedUpload({
        params: {
          Bucket: BucketName,
          Key: uploadFile.name,
          Body: uploadFile
        }
      })

      var promise = upload.promise();

      promise.then(
        function(data) {
          alert("Successfully uploading file!");
          setUploadFile('')
        },
        function(err) {
          console.log(err)
          return alert("There was an error uploading file!");
        }
      );
    } catch(e) {
      console.log(e)
    }
  }

  if(!loginFlg){
    return (
      <div className="App py-4">
        <div className="container">
          <h1 className='mb-5'>Login</h1>
          <div className="mb-3">
            <label htmlFor="email" className="form-label">Email address</label>
            <input value={email} onChange={(e) => setEmail(e.target.value) } type="email" className="form-control" id="email" placeholder="name@example.com"/>
          </div>
          <div className="mb-3">
            <label htmlFor="password" className="form-label">Password</label>
            <input value={password} onChange={(e) => setPassword(e.target.value)} type="password" className="form-control" id="password"/>
          </div>
          <button type="button" className="btn btn-primary" onClick={login}>ログイン</button>
        </div>
      </div>
    );
  } else {
    return (
      <div className="App py-4">
        <div className="container">
          <h1 className='mb-5'>ファイルアップロード</h1>
          <div className="mb-3">
            <input onChange={(e) => setUploadFile(e.target.files[0])} className="form-control" type="file" id="formFile"/>
          </div>
          <button type="button" className="btn btn-primary" onClick={fileUpload}>アップロード</button>
          <button type="button" className="btn btn-secondary mx-2" onClick={logout}>ログアウト</button>
        </div>
      </div>
    );
  }
 
}

export default App;
```
ローカルサーバを起動します。
```
npm start
```

[http://localhost:3000/](http://localhost:3000/)にアクセスして、以下の画面が表示されれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576521/715b1151-174d-d0f0-3bb2-4783688c28a2.png)

## ログイン情報を登録する
最後にCognitoユーザプールにログイン情報（Eメール・パスワード）を登録します。

#### 登録方法
* AWSコンソールから先ほど作成したCognitoユーザプールを選択する。
* 左側のメニューバーから全般設定-ユーザとグループを選択する。
* 「ユーザ」タブから「ユーザの作成」ボタンを押す。
* ユーザ名と仮パスワードを入力した後、「Eメールを検証済みにしますか？」以外のチェックを外して「ユーザの作成」を押す。

#### アカウントのステータス変更
これでログイン情報が作成されますが、初期状態ではアカウントのステータスが「FORCE_CHANGE_PASSWORD」（初期パスワードの変更が必要）となっており、このままでは使えません。

本来初期パスワード変更画面をアプリ側で作って対応する必要がありますが、以下のようにAWS CLIから「CONFIRMED」にステータスを変える方法もありますので、こちらでは割愛します。

https://www.shimashima.com/cognito-status/

これでユーザ登録が完了しました。Webアプリからログインできるようになっているはずですので、お試しください！

## 参照
[AmazonCognitoデベロッパーガイド](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html)
[AWS SDK for Javascryptデベロッパーガイド](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/welcome.html)
[amazon-cognito-identity-js](https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js)