ReactとAmazon Cognitoでユーザーを管理する

Amazon Cognitoを使ったのでその覚書ついでに:pencil2:
細かい説明とかはないです:relaxed:

# ユーザー管理機能を設定する前に

Amazon Cognitoでユーザーを管理できるようにしますので、とりあえず、Reactのアプリを作成します。

```shell
$ npx create-react-app react-aws-cognito --typescript
```

アプリが作成されたら、srcディレクトリの中に、Cognitoを利用するためのコンフィグファイルを作成します。

```awsConfiguration.ts
const awsConfiguration = {
  region: 'ap-northeast-1',
  IdentityPoolId: 'ap-northeast-1:**********',
  UserPoolId: 'ap-northeast-1_**********',
  ClientId: '**********',
}

export default awsConfiguration
```

**********となっている部分はこれから説明します。

以下もインストールしてください。

```shell
$ yarn add amazon-cognito-identity-js
```

Cognitoをjsで楽にいじられるモジュールです。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js

# ユーザープールを作成する

<img width="1440" alt="Screenshot 0031-09-07 at 5.08.56 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/60204c81-ebb0-4193-0aa6-1d877f7217d1.png">

Cognitoに入ると上のような画面になるので、まずは「ユーザープールの管理」に入ります。
「ユーザープールを作成する」ボタンがあると思いますので、そこから作成画面へ入っていきます。

<img width="1440" alt="Screenshot 0031-09-07 at 5.11.02 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/68245b90-60d5-f4ea-4623-21c2abdc14a6.png">

こんな具合で設定画面が出てくるので、順に設定します。

ユーザープールはそのままの意味で、ユーザーのみずたまりです。
ユーザーの倉庫のようなものです。

今回はデフォルトで作ります。

<img width="1440" alt="Screenshot 0031-09-07 at 6.15.43 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/bd4da92f-05ce-de47-03cf-93b8ea3c078e.png">

デフォルトの場合こんな設定になります。
必須の属性はemailとなり、これはサインアップ時に必須の項目となります（細かく設定するとこの辺りも変えられます）。

パスワードの最小長や多要素認証、また、登録時に送信するEメールやSMSの内容を編集したりもできます。
lambdaと連携させることも可能です:gear:

次にIDプールを作ります。

# IDプールを作成する

フェデレーテッドアイデンティティから「新しいIDプールの作成」をクリックすると、以下のような画面が表示されます。

<img width="1440" alt="Screenshot 0031-09-07 at 6.19.55 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/b69d1726-1ee9-5506-7db6-80b3360ba02b.png">

IDプール名を入力し、認証プロバイダーのところでCognitoタブを選択して必要な情報を入れます。

<img width="1158" alt="Screenshot 0031-09-07 at 6.21.10 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/11205c97-e2d9-7dc9-2a26-1b4b76d077f1.png">

## ユーザープールID
　作成したユーザープールの「全般設定」（ユーザープールから作成したのを選択すると表示されます）にプールIDがあるのでこれを入力してください、

## IDプール
　作成したユーザープールの「アプリクライアント」からアプリクライアントを作成すると表示される、アプリクライアントIDを設定してください。

<img width="1083" alt="Screenshot 0031-09-07 at 11.31.07 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/b9292705-374d-c72d-6f6b-7da2d188d654.png">

今回はJavascriptSDKを利用するのですが、その場合は「クライアントシークレットを生成」のチェックを外してください。JSのSDKでの対応ができていないようですね。。。
https://qiita.com/noobar/items/6615501b035e47792227

入力したら、「プールの作成」を押し、IAMロールを作成します。
ここのロールは、Authenticated UserとUnauthenticated Userそれぞれのアプリケーションの利用可能範囲を定めたものを表します。

Authenticated Userには、アプリケーションに登録したユーザーとして実行可能な範囲のアクションと、そのアクションに伴うAWSリソースへのアクセスを認めるべきです。
反対に、Unauthenticated UserにはAuthenticated Userと比較してより厳しい範囲でのアプリケーションの利用を認めるようにした方が無難です。

<img width="1424" alt="Screenshot 0031-09-07 at 6.29.39 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/d1a9e482-4c15-cf2e-5a07-ac716a96d78a.png">

IDプールに紐づくIAMロールを作成するので、「許可」を押して次へ行きましょう。
作成が成功したら、「Amazon Cognitoでの作業開始」が出てくるので、ドロップダウンからjavascriptを選びます。
すると、js用のコードが出てきて、その中に、IdentityPoolIdが表記されているので、それをコピーしておきます。

ここまでで今一度、
IdentityPoolId、UserPoolId、 ClientIdを整理しておきましょう（冒頭のコンフィグファイルの項目です）。

IdentityPoolId ・・・ IDプール作った時に出てきたやつ。
UserPoolId ・・・ ユーザープールの「全般設定」に表示されるプールID。
ClientId ・・・ ユーザープールの「アプリクライアント」に表示されるアプリクライアントID。

上記は全てコピーして、冒頭で書いたawsConfiguration.tsに書き込みます。
srcディレクトリ内はこんな感じになります。:point_down_tone2:

```
.
├── auth(SignUp.tsx, Verification.tsx, SignIn.tsx, SignOut.tsx)
├── App.css
├── App.test.tsx
├── App.tsx
├── awsConfiguration.ts
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
└── serviceWorker.ts
```

# サインアップ

tsxで書いたサインアップのコンポーネントです。
肝はSignUp関数です。

必須の属性として設定されていたemailとパスワードを入力すると、入力したemailあてにメールが届きます（もちろん電話番号で設定するとSMSでも届けられるように設定できます）。

<img width="422" alt="Screenshot 0031-09-08 at 11.53.07 AM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/4aa69f31-9ddc-e49c-0d5c-23156914e787.png">

このメールの文言もAWSコンソールで設定できます。
コードの有効期限はデフォルトでは７日になっているようです。

ちなみに、パスワードのポリシーはデフォルトで以下のようになっているので、注意してください。

<img width="213" alt="Screenshot 0031-09-08 at 12.48.03 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/08418e98-d12c-0627-966b-f2ca6ecdc64e.png">

```SignUp.tsx
import React from 'react'
import '../App.css'

import {
  CognitoUserPool,
  CognitoUserAttribute
} from "amazon-cognito-identity-js"
import awsConfiguration from '../awsConfiguration'

const userPool = new CognitoUserPool({
  UserPoolId: awsConfiguration.UserPoolId,
  ClientId: awsConfiguration.ClientId,
})

const SignUp: React.FC = () => {
  const [email, setEmail] = React.useState<string>('')
  const [password, setPassword] = React.useState<string>('')

  const changedEmailHandler = (event: any) => setEmail(event.target.value)
  const changedPasswordHandler = (event: any) => setPassword(event.target.value)
  const signUp = () => {
    const attributeList = [
      new CognitoUserAttribute({
        Name: 'email',
        Value: email
      })
    ]
    userPool.signUp(email, password, attributeList, [], (err, result) => {
      if (err) {
        console.error(err)
        return
      }
      setEmail('')
      setPassword('')
    })
  }

  return (
    <div className="SignUp">
      <h1 style={{ textAlign: 'left' }}>SignUp</h1>
      <input type="text" placeholder="email" onChange={changedEmailHandler} />
      <input type="text" placeholder="password" onChange={changedPasswordHandler} />
      <button onClick={signUp}>SignUp</button>
    </div>
  )
}

export default SignUp
```

# 検証

サインアップで受け取った検証コードを使ってサインアップを完了しましょう。

検証コードと必須の属性であるemailを入力する以下のコードを実行すると、アカウントのステータスがUNCONFIRMEDだったのが画像のようにCONFIRMEDに変わります（ユーザープール/全般設定/ユーザーとグループ）。

<img width="1129" alt="Screenshot 0031-09-08 at 12.19.17 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/6a6bedd8-9da3-48af-96f0-337be5ef2d03.png">

```Verification.tsx
import React from 'react'
import '../App.css'

import {
  CognitoUserPool,
  CognitoUser
} from "amazon-cognito-identity-js"
import awsConfiguration from '../awsConfiguration'

const userPool = new CognitoUserPool({
  UserPoolId: awsConfiguration.UserPoolId,
  ClientId: awsConfiguration.ClientId,
})

const Verification: React.FC = () => {
  const [email, setEmail] = React.useState<string>('')
  const [verificationCode, setVerificationCode] = React.useState<string>('')
  const changedEmailHandler = (event: any) => setEmail(event.target.value)
  const changedVerificationCodeHandler = (event: any) => setVerificationCode(event.target.value)

  const verifyCode = () => {
    const cognitoUser = new CognitoUser({
      Username: email,
      Pool: userPool
    })
    cognitoUser.confirmRegistration(verificationCode, true, (err: any) => {
      if (err) {
        console.log(err)
        return
      }
      console.log('verification succeeded')
      setEmail('')
      setVerificationCode('')
    })
  }
  return (
    <div className="Verification">
      <h1>Authenticate</h1>
      <input type="text" placeholder="verification code" onChange={changedVerificationCodeHandler} />
      <input type="text" placeholder='email' onChange={changedEmailHandler} />
      <button onClick={verifyCode}>Authenticate</button>
    </div>
  )
}

export default Verification
```

# サインイン

それでは検証成功したアカウントでサインインしてみます。
SignIn関数でサインインを行い、認証が成功すればonSuccessを通ってresultを返します。
ここではresultからaccessTokenを取り出してログに出しています。

```SignIn.tsx
import React from 'react'
import '../App.css'

import {
  CognitoUserPool,
  CognitoUser,
  AuthenticationDetails
} from "amazon-cognito-identity-js"
import awsConfiguration from '../awsConfiguration'

const userPool = new CognitoUserPool({
  UserPoolId: awsConfiguration.UserPoolId,
  ClientId: awsConfiguration.ClientId,
})

const SignIn: React.FC = () => {
  const [email, setEmail] = React.useState<string>('')
  const [password, setPassword] = React.useState<string>('')
  const changedEmailHaldler = (e: any) => setEmail(e.target.value)
  const changedPasswordHandler = (e: any) => setPassword(e.target.value)

  const signIn = () => {
    const authenticationDetails = new AuthenticationDetails({
      Username : email,
      Password : password
    })
    const cognitoUser = new CognitoUser({
      Username: email,
      Pool: userPool
    })
    
    cognitoUser.authenticateUser(authenticationDetails, {
      onSuccess: (result) => {
        console.log('result: ' + result)
        const accessToken = result.getAccessToken().getJwtToken()
        console.log('AccessToken: ' + accessToken)
        setEmail('')
        setPassword('')
      },
      onFailure: (err) => {
        console.error(err)
      }
    })
  }

  return (
    <div className="SignIn">
      <h1>SingIn</h1>
      <input type="text" placeholder='email' onChange={changedEmailHaldler}/>
      <input type="text" placeholder='password' onChange={changedPasswordHandler}/>
      <button onClick={signIn}>Sign In</button>
    </div>
  )
}

export default SignIn
```

サインイン中のアカウントを検知する場合は、

```tsx
const cognitoUser = userPool.getCurrentUser()
if (cognitoUser) {
  // sign inしている状態
  console.log('signing in')
} else {
  // sign inしていない状態
  console.log('no signing in')
}
```

という感じで行い、サインアウト時もこれがtrueの時に処理できるようにします。

# サインアウト

サインアウトはこちら。
userPool.getCurrentUser()で現在サインイン中のアカウントを検知し、
サインインされていればcognitoUser.signOut()を、
そうでなければとりあえず念の為localStorageを綺麗にしています（サインインするとlocalStorageにもaccessToken等のデータが入るので）。

```SignOut.tsx
import React from 'react'
import '../App.css'

import { CognitoUserPool } from "amazon-cognito-identity-js"
import awsConfiguration from '../awsConfiguration'

const userPool = new CognitoUserPool({
  UserPoolId: awsConfiguration.UserPoolId,
  ClientId: awsConfiguration.ClientId,
})

const SignOut: React.FC = () => {
  const signOut = () => {
    const cognitoUser = userPool.getCurrentUser()
    if (cognitoUser) {
      cognitoUser.signOut()
      localStorage.clear()
      console.log('signed out')
    } else {
      localStorage.clear()
      console.log('no user signing in')
    }
  }

  return (
    <div className="SignOut">
      <h1>SignOut</h1>
      <button onClick={signOut}>Sign Out</button>
    </div>
  )
}

export default SignOut
```

全部まとめて整理するとこんな感じ？:point_down_tone2:

```App.tsx
import React from 'react'
import './App.css'

// components
import SignUp from './auth/SignUp'
import Verification from './auth/Verification'
import SignIn from './auth/SignIn'
import SignOut from './auth/SignOut'

import { CognitoUserPool } from "amazon-cognito-identity-js"
import awsConfiguration from './awsConfiguration'

const userPool = new CognitoUserPool({
  UserPoolId: awsConfiguration.UserPoolId,
  ClientId: awsConfiguration.ClientId,
})

const App: React.FC = () => {

  const authentication = () => {
    const cognitoUser = userPool.getCurrentUser()
    // サインインユーザーがいればアプリのメイン画面へ、
    // いなければサインアップ、検証、サインイン画面を表示する。
    if (cognitoUser) {
      return (
        <div className="authorizedMode">
          <SignOut />
        </div>
      )
    } else {
      return (
        <div className="unauthorizedMode">
          <SignUp />
          <Verification />
          <SignIn />
        </div>
      )
    }
  }

  return (
    <div className="App">
      <header />
      { authentication() }
    </div>
  )
}

export default App
```

# グループ（IAMロール）の設定

Cognitoでは特定のIAMロールを持つグループを作成することができ、
そのグループに属するユーザーはそのIAMロールのポリシーに従ってAWSリソースへのアクセスが制限されます。

<img width="1440" alt="Screenshot 0031-09-08 at 1.30.14 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245704/d9992be6-be66-da98-4915-567ed98bdf00.png">

IAMロールから割り当てたいロールを選択してグループを作成します。
以降、AWSコンソールからグループにユーザーを割り当てることができるようになります。

ですが、これ、毎回手動でAWSコンソールをいじるのも面倒すぎるので、JavascripSDKを利用してやりたいです。
adminAddUserToGroupを利用して実現します。
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityServiceProvider.html#adminAddUserToGroup-property

```ts
import AWS from 'aws-sdk'
const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider()
const params = {
  GroupName: 'STRING_VALUE', /* required */
  UserPoolId: 'STRING_VALUE', /* required */
  Username: 'STRING_VALUE' /* required */
}
cognitoidentityserviceprovider.adminAddUserToGroup(params, function(err, data) {
  if (err) console.log(err, err.stack); // an error occurred
  else     console.log(data);           // successful response
})
```

上のように書くみたいです（試してませんが）。
検証が成功した後にこの処理を挟めば良いと思います。

Usernameはおそらくメールアドレスを入力すれば問題ないかと思います。
もしかしたら必須の属性設定によるかもしれませんが。。。

基本的には以上でCognitoを利用した認証機能を実装できるはずです:hugging:

デモ作ってみました。ここで説明したawsConfiguration.tsは入っていません。
https://github.com/yutaro1204/AmazonCognitoWithReact