ReactとAmazon Cognitoで認証機能を作ってみる

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

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

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

$ 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

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

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

$ yarn add amazon-cognito-identity-js

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

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

Screenshot 0031-09-07 at 5.08.56 PM.png

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

Screenshot 0031-09-07 at 5.11.02 PM.png

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

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

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

Screenshot 0031-09-07 at 6.15.43 PM.png

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

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

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

IDプールを作成する

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

Screenshot 0031-09-07 at 6.19.55 PM.png

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

Screenshot 0031-09-07 at 6.21.10 PM.png

ユーザープールID

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

IDプール

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

Screenshot 0031-09-07 at 11.31.07 PM.png

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

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

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

Screenshot 0031-09-07 at 6.29.39 PM.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でも届けられるように設定できます)。

Screenshot 0031-09-08 at 11.53.07 AM.png

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

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

Screenshot 0031-09-08 at 12.48.03 PM.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に変わります(ユーザープール/全般設定/ユーザーとグループ)。

Screenshot 0031-09-08 at 12.19.17 PM.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) => setAuthCode(event.target.value)

  const verifyCode = () => {
    const cognitoUser = new CognitoUser({
      Username: email,
      Pool: userPool
    })
    cognitoUser.confirmRegistration(authCode, true, (err: any) => {
      if (err) {
        console.log(err)
        return
      }
      console.log('verification succeeded')
      setEmail('')
      setAuthCode('')
    })
  }
  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

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

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リソースへのアクセスが制限されます。

Screenshot 0031-09-08 at 1.30.14 PM.png

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

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

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