Cognitoで学ぶ認証・認可 in AWS

# この記事について
Webアプリのアクセス制御を行いたい！となったときに学ぶべきなのは認証・認可の仕組みです。
AWSにはAmazon Cognitoというユーザー管理を行うための仕組みが存在し、これを利用すれば「実装するだけなら」簡単にアプリのアクセス制御を行うことができます。

この記事では「Cognitoが実際に何をやってくれているのか？」というところまで掘り下げながら、簡単なReactアプリを作っていきます。

## アジェンダ
1. Cognitoのユーザープールを作って触ってみる
2. Reactアプリに認証の仕組みを入れてみる
3. Cognitoで認証済みの人だけが叩けるAPIをLambda + API Gatewayで作る
4. CognitoのIDプールを作り、AWSでの認可の仕組みを学ぶ
5. Cognito IDプールで認可された人だけが叩けるAPIをLambda + API Gatewayで作る

## 使用する環境・バージョン
- React: 16.13.1
- aws-amplify: 3.3.1
- aws-amplify-react: 4.2.5
- @aws-amplify/ui-react: 0.2.21

## 読者に要求する前提知識
- Reactについての知識(本記事ではReactそのものの説明についてはしません)
- REST APIが何かがわかること
- (Lambda + API GatewayでのREST APIの作り方がわかること)
- IAMロールが何かがわかり、自分で作成できること

# Cognitoユーザープールの作成
まずは、AWSウェブコンソールからユーザープールを作成してみましょう。

## ユーザープール作成
![スクリーンショット 2020-09-19 19.48.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/291b7a1d-f414-a0c1-2d4b-fa3b5e70cdc8.png)
任意のプール名をつけたあと、設定に進みます。今回は「ステップにしたがって設定」を選択します。
![スクリーンショット 2020-09-19 19.56.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/6b4b9df9-0273-be90-4c32-ad50aee1ac5d.png)
認証(SignIn)時に要求する情報を「(ユーザーが決めた)ユーザー名」にするか「メールアドレスor電話番号」にするかの選択です。
今回は、メールアドレスのみを使用するようにしました。
![スクリーンショット 2020-09-19 19.57.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/a065c495-2e21-b500-5d2e-771c887d6272.png)
ユーザー作成(SignUp)時に、ユーザーが登録する必須項目を選択します。
今回の場合、メールアドレスの他に「name」を登録してもらうことにしました。
![スクリーンショット 2020-09-19 20.00.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/794dad27-bfcd-3bd5-f0e4-87c163ebc7bf.png)
パスワード関連の設定を行います。

- パスワードの強度を設定: 数字・特殊文字・大文字小文字を要求するか・長さを決めます。
- 自己サインアップ: ユーザー作成をAWSの管理画面からのみにするか、一般ユーザーもアプリ画面等から可能にするかを決めます。
- パスワード有効期限: Sign Up時に発行される、初回ログインのための仮パスワードの有効期限を設定します。

![スクリーンショット 2020-09-19 20.06.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/d4f079c4-8d24-b1f4-787e-c241820187d6.png)
ここでは以下の設定を行います。

- MFA(多要素認証)の設定: offにするかonにするかを選択します。今回は簡略化のためなし。
- パスワード忘れの時の回復手段: ユーザーがパスワードを忘れたときには、登録済みのメールアドレスから回復させるようにした。
- ロールの提供: Sign Up時やパスワード忘れからの復旧のときに、Cognitoがメールを送るためのロールを付与する。

他の設定は全てデフォルトのままにして、プールを作成します。
![スクリーンショット 2020-09-19 21.39.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/ffe72dcf-d0ea-bd78-ddbf-e57237a43e66.png)
ここで確認できるプールIDは控えておきましょう。

参考:[[新機能] Amazon Cognito に待望のユーザー認証基盤「User Pools」が追加されました！](https://dev.classmethod.jp/articles/cognito-user-pool/)

## アプリクライアントの作成
ユーザープールを使う方のアプリの登録・ID払い出しを行います。
アプリを作る際に「クライアントシークレットを作る」のチェックを外しておきましょう。

![スクリーンショット 2020-09-19 23.11.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/3f0e0639-0332-a619-dfbb-50074bfed0b0.png)

- 有効なIDプロバイダ: すべて選択をチェック
- コールバックURL: httpsかlocalhostを選択。ここでは暫定的にamazonを指定しておく。
- 許可されているOAuthフロー: Authorization code grantとImplicit grantを選択
- 許可されているOAuthスコープ: 全て選択

ここで作ったアプリクライアントIDもいずれ使うので控えておきます。

## Amazon Cognitoドメインを設定
アプリの統合→ドメイン名のタブを開くと、以下のような画面になる。
![スクリーンショット 2020-09-19 22.52.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/236b532d-add2-90e6-0bc0-350d9587a3b0.png)
Amazon Cognitoドメインは、1つのリージョンの中で一意なドメインを設定する必要があります。
まだ使われていなさそうなドメインを「ドメインのプレフィックス」に入力・設定を保存します。

## 動かせるか確認
まずは、「ユーザーとグループ」のタブから、ユーザーを一つ作成しておきます。

### OAuthフロー"Implicit Grant"の使用
アプリクライアントの作成のところで、"Implicit Grant"を許可しているので、まずはそれを試してみます。
以下のURLにアクセスします。

```
https://[amazon cognitoドメイン]/login?response_type=token&client_id=[アプリクライアントID]&redirect_uri=[コールバックURL]
```

すると以下のように、Cognitoが提供するサインインフォームが表示されます。
![スクリーンショット 2020-09-25 23.53.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/ad52e1a7-2106-7370-6a6d-7804d990841e.png)
ここで、先ほど作ったユーザーのメールアドレスとパスワードを入力してSign Inすると、コールバックURLにリダイレクトします。
この時のリダイレクト先URLハッシュの中に「IDトークン」「アクセストークン」が格納されています。

```
https://[コールバックURL]#id_token={IDトークン}&access_token={アクセストークン}&expires_in=3600&token_type=Bearer
```
この後の流れとしては、このURLハッシュで得られたIDトークンとアクセストークンをフロントエンド側で取得・保存し、アプリ内で利用することになります。

参考:[【AWS】これだけ見れば理解できるCognito〜認証機能つきサーバレスアーキテクチャの作成〜](
https://qiita.com/UpAllNight/items/ec522da70a6fe86a1d16)

### OAuthフロー"Authorization Code Grant"の使用
もう一つのOAuthフロー"Authorization Code Grant"も試してみます。
以下のURLにアクセスします。

```
https://[amazon cognitoドメイン]/login?response_type=code&client_id=[アプリクライアントID]&redirect_uri=[コールバックURL]
```
先ほどの"Implicit Grant"の違いは`response_type`が"code"か"token"かというところです。

アクセスしたら、先ほど同様にCognitoが用意したログインフォームがあるので、同様に作ったユーザーでログインします。
すると、以下のURLにリダイレクトされます。

```
https://[コールバックURL]?code={認可トークン}
```
これも、リダイレクトURLで得られた認可トークンを保存して使用していくことになります。

### 今は何をしていたのか
今試してみた「Cognitoが用意したUIを使う」方法は、以下のAWS Blackbelt Seminor資料における「Cognito Auth API と Hosted UI を利用」に該当します。
![スクリーンショット 2020-09-29 0.11.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/360fc450-ad3a-1a50-8d86-b614b942935b.png)
画像出典・資料:[[AWS Black Belt Online Seminar] Amazon Cognito](https://d1.awsstatic.com/webinars/jp/pdf/services/20200630_AWS_BlackBelt_Amazon%20Cognito.pdf)

「ログイン・サインインフォームのUIも自分で作りたい！」という場合や、「リダイレクトURLから得たトークンや認可コードを自分で保存・管理しておくのがめんどくさい！」という場合は、もう一方の「Cognito Identity Provider API を利用」を選択することになります。


# Reactでフロントエンドを作り、Cognito認証と連携させる
「Cognito Identity Provider API を利用」した方法がこちらです。

ここからは、Cognitoでユーザー認証をしたユーザーだけがアクセス・中身を見ることができるウェブページをReactで作成していきます。

## Reactアプリの枠組みを作成
`create-react-app`で簡単にアプリフレームが作れるので実行します。

```bash
$ npx create-react-app [appname]
```
これで、[appname]という名前のディレクトリができて、そこにReactアプリのフレームが出来上がっています。

## パッケージのインストール
Cognitoでの認証・認可をコードベースで扱うためのパッケージをインストールします。
かつては"Amazon Cognito Identity SDK for JavaScript"等のSDKを使用していましたが、今は"AWS Amplify"の方のパッケージにCognito周りのパッケージが統合され、そちらがかなり便利なので今回はこれを使います。
[公式Doc: Amplify Framework Documentation](https://docs.amplify.aws)

```bash
$ npm install aws-amplify aws-amplify-react @aws-amplify/ui-react
```

##コード作成
まずは`index.js`を以下のように修正します。

```diff:index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
+ import { Amplify } from 'aws-amplify';
+ 
+ Amplify.configure({
+   Auth: {
+       region: "Cognitoユーザープールを作ったリージョン",
+       userPoolId: "作成したユーザープールID",
+       userPoolWebClientId: "作成したアプリクライアントID"
+   }
+ });

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
```
`Amplify.configure()`メソッドを使って、ReactアプリでどのCognitoプールを利用するかを指定しています。
参考:[公式Doc: Amplify Doc AUTHENTICATION Create or re-use existing backend](https://docs.amplify.aws/lib/auth/start/q/platform/js)

次に、`App.js` を修正します。

```diff:App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
+ import { withAuthenticator } from '@aws-amplify/ui-react';

function App() {
  // (略)
}

- export default App;
+ export default withAuthenticator(App);
```
`withAuthenticator()`関数はhigher-order component (HoC)という、Reactコンポーネントを受け取って別のReactコンポーネントを返すものです。
`withAuthenticator(引数)`で得られるコンポーネントを利用することで、引数で渡したコンポーネントの閲覧にCognitoユーザー認証を要求するようにできます。

今回の場合は`App`コンポーネントを引数に渡しているので、アプリのどこの画面を見るにしてもCognito認証が必要です。
参考:[公式Doc: Amplify Doc AUTHENTICATION Authenticator](https://docs.amplify.aws/ui/auth/authenticator/q/framework/react#withauthenticator)

## アプリの起動
`npm run start`で`http://localhost:3000`が立ち上がり、アプリの挙動が確かめられるようになります。

`http://localhost:3000`にブラウザでアクセスしてみると、ReactのHello, Worldは表れず、以下のような認証画面が表れます。`withAuthenticator(App)`の機能通り、ここでユーザー認証が行われないと画面を閲覧することはできないようになっています。

![スクリーンショット 2020-09-28 16.24.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/8fc8a13d-4cbe-13ed-e8f0-00aa7ac9ea51.png)


ここで、先ほどCognitoユーザープール管理画面で作ったユーザー情報を入力します。
すると、`App`コンポーネントの中身である、ReactのHello, World!画面が無事に表れます。

![スクリーンショット 2020-09-28 16.24.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/2ef3a44c-ebc5-86a8-f1f6-eb26de5a5277.png)

## 画面カスタマイズ
Sign IN/Up周りのUIをデフォルトからカスタマイズする方法を一部紹介します。

### サインアウトボタンをつける
デフォルトではアプリ画面にログアウトボタンが存在しません。SAOみたいな狂気の世界にするつもりはない(はず)なので、ログアウトボタンをつけましょう。

Amplifyの`AmplifySignOut`コンポーネントが、ログアウトボタン機能を実装しているので、それを追加します。

```diff:App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
- import { withAuthenticator } from '@aws-amplify/ui-react';
+ import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        // (略)
+　　　　<AmplifySignOut />
      </header>
    </div>
  );
}

export default withAuthenticator(App);
```

### 認証フォームをカスタマイズする
デフォルトでは、Sign Upフォームは「メールアドレス」「パスワード」「電話番号」です。しかし例えば、電話番号をフォームから抜きたいというときはどうしたらいいでしょうか。

認証フォームを`withAuthenticator()`メソッドで導入した場合は、楽な代わりに細かいカスタマイズが難しいという側面があります。
実は、`withAuthenticator()`というのは、`AmplifyAuthenticator`コンポーネントをラップしたものです。なので、`withAuthenticator(App)`とするのは以下のように書くのと同じです。

```diff:App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
- import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';
+ import { AmplifyAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';

function App() {
  return (
    <div className="App">
+      <AmplifyAuthenticator>
        <header className="App-header">
          // (略)
 　　　　  <AmplifySignOut />
        </header>
+      </AmplifyAuthenticator>
    </div>
  );
}

- export default withAuthenticator(App);
+ export default App;
```
このとき、`AmplifyAuthenticator`コンポーネントのブロックの中で、カスタマイズした認証フォームコンポーネントを設置すれば、デフォルト表示からフォームを変えることができます。

例えば、SignUpフォームを「メールアドレス」「パスワード」だけにするときは、Sign Upフォームを作る`AmplifySignUp`コンポーネントに適切なPropsを追加してやります。

```diff:App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
- import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react';
+ import { AmplifyAuthenticator, AmplifySignOut, AmplifySignUp } from '@aws-amplify/ui-react';

function App() {
  return (
    <div className="App">
      <AmplifyAuthenticator>
+      <AmplifySignUp
+       slot="sign-up"
+       usernameAlias="email"
+       formFields={[
+         {
+           type: "email",
+           required: true,
+         },
+         {
+           type: "password",
+           required: false,
+         },
+       ]} 
+      />
        <header className="App-header">
          // (略)
 　　　　  <AmplifySignOut />
        </header>
      </AmplifyAuthenticator>
    </div>
  );
}

export default App;
```
こうすることで、`AmplifyAuthenticator`コンポーネントのデフォルト表示が上書きされて、自分で書いた`AmplifySignUp`コンポーネントが使われます。

どんなコンポーネントが存在して、そんなPropsがあるのかについては、以下の公式ドキュメントを参照ください。
参考:[公式Doc:Amplify Doc AUTHENTICATION Authenticator #Component](https://docs.amplify.aws/ui/auth/authenticator/q/framework/react#components)

## ユーザープールで得られる実態を確認
ここで、ブラウザのローカルストレージを確認してみます。
![スクリーンショット 2020-09-28 16.38.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/27ac082f-f3be-ecda-4e71-eb3561786967.png)

- idToken: ユーザープールに格納されている情報を確認するのに必要なJWTトークン
- accessToken: ユーザープールの情報の更新のために必要なJWTトークン
- refreshToken: idTokenやaccessTokenの有効期限が切れた時に、新しいものを取得するために必要なトークン
- LastAuthUser: 現在ログインしている(=最後にログインした)ユーザーの、ユーザープール上でのID
- userData: ユーザープールに登録されている情報

参考:[Cognitoのサインイン時に取得できる、IDトークン・アクセストークン・更新トークンを理解する](https://dev.classmethod.jp/articles/study-tokens-of-cognito-user-pools/)

Amplifyのコードを使っての認証に成功した暁には、これらの重要データが自動でローカルストレージに格納されています。Cognitoが用意したUIを使用した場合とは違い、自力でのトークン保存処理の実装が必要ありません。便利です。

また、idTokenやaccessTokenの有効期限が切れた場合(デフォルトで1時間)、refreshTokenというものを使ってトークンの更新が必要なのですが、それもAmplifyを使っている場合、開発者が意識しなくても自動で更新が行われています。これもAmplifyを使用するメリットの一つです。

これらの「Amplifyが裏でうまいことやってくれている仕組み」については、以下の記事で非常に詳しく説明されていますので、気になる方はご覧ください。
参考:[AmplifyでCognitoのHosted UIを利用した認証を最低限の実装で動かしてみて動作を理解する](https://dev.classmethod.jp/articles/learn-authentication-using-cognitos-hosted-ui-with-amplify/)


# Cognitoで認証した人だけが叩けるAPIを作って連携する
Cognito認証をするアプリのバックエンドで使うAPIは、認証済みの人だけが叩けるようにしないと困ります。
そのため、「Cognitoでのユーザー認証が適切になされている人のみが叩けるAPI」を作ります。

## Lambda関数を作る
API GatewayからGETリクエストを受け取ったら、`Secret API!`というbodyを含んだjsonを返す関数をLambdaで作ります。

```golang
package main

import (
	"encoding/json"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

type Response struct {
	Body string `body`
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	res := Response{Body: "Secret API!"}
	jsonBytes, _ := json.Marshal(res)

	return events.APIGatewayProxyResponse{
		Body:       string(jsonBytes),
		Headers:    map[string]string{"Access-Control-Allow-Origin": "*"},
		StatusCode: 200,
	}, nil
}

func main() {
	lambda.Start(handler)
}
```
コードについての解説は、以下の過去記事をご覧ください。
[AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた](https://qiita.com/saki-engineering/items/e5cf56301d94ceea3ce0)

注:Headersの`{"Access-Control-Allow-Origin": "*"}`は、CORSを有効にするために必要になります。

## API Gatewayの設定
### GETリクエストとLambda関数の連携
API Gatewayで、GETリクエストで上で作ったLambda関数を呼び出すように設定します(やり方は上記同様に、過去記事を参照ください)。
[AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた](https://qiita.com/saki-engineering/items/e5cf56301d94ceea3ce0)

### オーソライザの設定
ここで、APIのオーソライザの設定を行います。
![スクリーンショット 2020-09-23 15.56.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/7c1db68c-75e2-ca3f-5834-aef321e6b9ea.png)
以下の通りに設定して作成します。

- 名前: 任意の名前
- タイプ: Cognitoを利用
- Cognitoユーザープール: 先ほど作ったユーザープールの名前を選択
- トークンのソース: Authrizationを入力

こうすることで、HTTPリクエストのヘッダに`Authrization=IDトークン`をつけているものだけがこのAPIを叩けるようになります。
(Authrizationに有効なIDトークンが付いている=Cognitoでの認証済みとAPI Gatewayの方で判断するようになります)

### CORSの設定
`localhost`から、別ドメイン(独自ドメインを割り当てないなら通常`https://[文字列].execute-api.ap-northeast-1.amazonaws.com`となる)であるAPIを叩くので、CORS対策が必要になります。

アクションから、「CORSの有効化」を選択します。
![スクリーンショット 2020-09-28 23.04.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/cc79a4fd-516c-1c74-dbc6-9d24e7ca555a.png)
この設定のままで、CORSを有効化します。

参考:[【AWS】これだけ見れば理解できるCognito〜認証機能つきサーバレスアーキテクチャの作成〜](https://qiita.com/UpAllNight/items/ec522da70a6fe86a1d16#crosの設定)

### APIのデプロイ→テスト
この状態でデプロイしたAPIをターミナルからcurlで叩くと、権限なしエラーが返ってきます。
(デプロイの仕方は過去記事参照→[AWS Lambda+API Gateway+DynamoDBでCRUD APIを作るのをGolangでやってみた](https://qiita.com/saki-engineering/items/e5cf56301d94ceea3ce0))

```bash
$ curl [APIURL]
{"message":"Unauthorized"}
```
これで、Cognitoの認証なしにはAPIが叩けなくなっているということが確認できました。

## フロントエンドでAPIを呼び出す
Cognitoで認証して閲覧しているアプリから、このAPIが叩けるかどうかをテストしてみましょう。
Cognito周りのメソッドだけでなく、APIを叩くためのメソッドもAmplifyに存在するので、それを利用します。
参考:[Amplify公式Doc API(REST)](https://docs.amplify.aws/lib/restapi/getting-started/q/platform/js#configure-your-application)

まずは、`index.js`内の`Amplify.configure()`メソッドで、アプリ内で使うAPIを指定します。

```diff:index.js
Amplify.configure({
  Auth: {
      // (略)
  },
+ API: {
+   endpoints: [
+     {
+         name: "TestAPI",
+         endpoint: "APIのエンドポイントURL",
+         region: "APIが公開されているリージョン",
+     }
+   ]
+ }
});
```

次に、`App.js`を書き換えて、APIから受け取った文字列をアラートで表示するボタンを設置します。

```diff:App.js
function App() {
  return (
    <div className="App">
      <AmplifyAuthenticator>
      // (略)
      <header className="App-header">
        // (略)
+        <div class='container'>
+         <button id='button' onClick={() => showSecret()}>Click Me!</button>
+        </div>
        // (略)
      </header>
      </AmplifyAuthenticator>
    </div>
  );
}
```
ボタンをクリックした時に実行される関数`showSecret()`も、以下のように`App.js`内に実装します。

```App.js
import { Auth, API } from 'aws-amplify';

const showSecret = async function() {
    const apiName = 'TestAPI';  // index.jsで指定したnameと同じ名前にする
    const path = ''; 
    const myInit = { 
      headers: { 
        Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}`,
      },
    };

    API.get(apiName, path, myInit)
    .then(response => {
      console.log(response);
      alert(response.Body);
    })
    .catch(err => {
      console.log(err);
      alert(err);
    });

  };
```
`Authorization`ヘッダに、IDトークンを付与するように設定しています。

この状態でアプリを動かして、新たに作成したボタンを押すことで、APIからのレスポンスをアラートに表示させることができます。

# IDプールの作成
次に、ユーザープールに続いて、Cognitoの別機能であるIDプールを使っていきます。

## なぜIDプールを作成するのか
ユーザープールだけでもログインフォームが作れたのに、どうしてIDプールも作るの？ということを説明します。

Cognitoのユーザープールで行えるのは「認証」のみで「認可」は行うことができません。
例えば、「このリクエストを送っているのは、メールアドレス`http://example.com`で登録したAさんだな」ということを保証することはできても、「そのAさんにDBアクセスをやらせていいよ」と許可することはできないのです。

なので、認証したユーザーにDynamoDB等の「AWSリソースにアクセスさせるために」「IAMロールを割り当てる」という「認可」の作業を行うためには、IDプールが必要になるのです。
(逆に言えば、IAMロールを絡ませる必要が全くないアプリケーションの場合、ユーザープールだけで事足ります。)

## 作成手順
![スクリーンショット 2020-09-28 21.34.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/b350c465-b715-3e33-6ea4-815cbfcb8c40.png)
IDプール名に好きな名前をつけます。
認証されていないIDへのアクセス・認証フローの設定にはどちらにもチェックはつけずに、「認証プロバイダー」のタブを開きます。

![スクリーンショット 2020-09-21 1.18.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/3affdf64-663d-fe32-a8e7-313bda08ce59.png)
ここで、先ほど作ったユーザープールIDとアプリクライアントIDを入力して、次に進みます。

![スクリーンショット 2020-09-28 21.35.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/7bb9d02e-c04e-44f0-bc91-8e610baada0d.png)
ここで、認証されたユーザー・認証されなかったユーザーにどんなロールを与えるのかを設定する画面です。今回はそれぞれに対して「Auth_Role」「UnAuth_Role」を与えるように新しくロールを新規作成しました。

ここまでの設定が済んだら、IDプールの作成は完了です。
作成完了したら、「IDプールの編集」タブから、プールのIDを確認して控えておきましょう。

## Reactアプリとの連携
IDプールを作成できたら、それを先ほどのReactアプリで使えるようにしましょう。

`index.js`内の`Amplify.configure()`メソッドの中身を以下のように編集します。

```diff:index.js
Amplify.configure({
   Auth: {
       region: "Cognitoユーザープールを作ったリージョン",
       userPoolId: "作成したユーザープールID",
       userPoolWebClientId: "作成したアプリクライアントID"
+      identityPoolId: "(先ほど控えた)IDプールのプールID",
   }
});
```
この状態でアプリを起動することで、ログインユーザー(ユーザープールのユーザー1人)ごとに、IDプールのIDが割り振られる&ログインユーザーが持つIAMロールが与えられるようになります。

## IDプールから得られるものの実態を確認する
「IDプールのIDが与えられる」「IAMロールが与えられる」といっても、アプリの見た目上は何も変わらないので、「どこがどうなったの？？」と思う方もいるでしょう。
ここからは、IDプールと連携したことで何を得られたのか、実態を確認してみましょう。

### まずはコンソールでものを確認
`App.js`の中で`currentCredentials()`というメソッドを実行して、結果をみてみましょう。

```diff:App.js
// (略)
import { Auth } from 'aws-amplify';

function App() {
+  Auth.currentCredentials().then(console.log);
   // (略)
}

export default App;
```
この状態でアプリを起動すると、コンソール上で`currentCredentials()`で得られたデータを確認できます。
![スクリーンショット 2020-09-28 19.48.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/4e43760a-3577-85b3-0c3f-cf03e8302e89.png)
補足:この`currentCredentials()`メソッドは、IDプール未連携時にはエラーが返ってきます。

ここで得られる「accessKeyId」と「secretAccessKey」が、IDプールから得ることができる"AWS Security Credentials"というものの実態です。
また今回の場合、Security Credentialsの中でも"AWS **Temporary** Security Credentials"というものが利用されます。なので「sessionToken」も重要な役割を果たします。

### "AWS Security Credentials"とは？
「accessKeyId」と「secretAccessKey」の組み合わせのことです。

AWSでは、正しいアクセスキーIDとシークレットアクセスキー(=正しいAWS Security Credentials)を持っていることで、特定のAWSアカウントのあるIAMロールを持ったユーザーであると判定しています。

わかりやすい例としては、普段ターミナルでAWS CLIを叩く方だと、`~/.aws/credentials`の設定ファイルに普段使用するアクセスキーIDとシークレットアクセスキーを保存しているかと思います。これは、ターミナルからコマンドを実行している人が、正当なIAM UserかどうかをAWS Security Credentialsで判断している例です。

AWS CLIでのAWS Security Credentialsは、ユーザーがAWSコンソールで更新作業を行わない限りずっと有効であり、いわゆる**Permanent**なものです。
それに対して、今回のような「ユーザーがアプリにログインしている間だけ使える」ようなものを"AWS **Temporary** Security Credentials"と呼びます。

一般的に**Permanent**な方のAWS Security CredentialsはIAM Userに、**Temporary**な方のAWS Security CredentialsはIAM Roleに紐づいていることが多いです。

### AWS Security Credentialsの使い方
ここからは、Security Credentialsを我々はどのように使うのかを見ていきます。

まずは、「accessKeyId」と「secretAccessKey」を使って(=Security Credentialsを使って)利用するAWSサービスというのは、たいていREST APIになっています(AWS APIという)。
AWS APIはリクエストがあるたびに、「このリクエスト主が正当な権限の持ち主かどうか？」というのをリクエスト内容から判断する必要があります。

AWS APIが権限を確認できるように、ユーザーはhttpリクエストヘッダの`Authorization`フィールドに「Signature V4」という方式の署名を付与する必要があります。
その**「Signature V4」署名の生成に必要なのが「accessKeyId」と「secretAccessKey」**です。

そして、AWS **Temporary** Security Credentialsを使用する場合は、「権限が期限切れじゃないかどうか」ということを判断するために「sessionID」が使われます(リクエストヘッダの`X-Amz-Security-Token`フィールドに埋め込む)。

### まとめ
今回の場合、IDプールから得られるSecurity Credentialsを利用することで、認証されたアプリの利用者がIAMロールを使えるようになるのです。

参考:[【AWS】AWS APIの認証・認可の仕組みを理解する【Signature V4】](https://qiita.com/tmiki/items/a2a873ea97f5f455aa9b)
参考:[【Amplify】APIのAuthorization方式「AWS_IAM」を理解する#1　～解説編～【AWS】](https://qiita.com/tmiki/items/061b321199c9ff986e71)

# ユーザープールのグループを使ってユーザーごとに権限を分ける
IDプールと連携することで、「認証済みユーザーにはIAMロール"Auth_Role"」「認証されていないユーザーにはIAMロール"UnAuth_Role"」が渡されるように設定することができました。
しかし、認証済みユーザーに割り当てられるIAMロールが1つというのは少々窮屈です。例えば、認証済みユーザーの中でも、「一般ユーザー」と「管理者ユーザー」で違うロールを与えたいというケースはよくあるものでしょう。

今回は、Cognitoユーザープールの「ユーザーグループ」という機能を用いることで、認証済みユーザーにそれぞれ適切なロールを分けて与えられるように設定し直してみようと思います。

参考:[AWS CLIで動かして学ぶCognito IDプールを利用したAWSの一時クレデンシャルキー発行](https://dev.classmethod.jp/articles/get-aws-temporary-security-credentials-with-cognito-id-pool-by-aws-cli/)

## ユーザーグループの作成
![スクリーンショット 2020-09-28 22.04.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/c5879ae6-d029-4f61-e468-7d9222b09d41.png)
「ユーザーとグループ」からグループの作成を選択します。すると、以下のようなグループ作成画面が現れます。
![スクリーンショット 2020-09-28 22.04.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/800f67d0-439a-79a3-e32c-f270a4593fff.png)
ここで任意のグループ名をつけます。まずは管理者ユーザー用のグループを作るため、それっぽい名前をつけましょう。
IAMロールのところで、新しく「AdminAuth_Role」という名前(ポリシーはAuth_Roleと一旦同じものにしておく)を作成し、それを選択します。

同様に、一般ユーザー用のグループ(IAMロールはAuth_Role)も作成しておきます。

## ユーザーをグループに追加
ユーザーグループを作成したら、ユーザーをそこに所属させます。

ユーザー一覧からユーザーを選択して、「グループに追加」を選択することで可能です。
![スクリーンショット 2020-09-28 22.05.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/8738a0fe-9046-c200-c499-f03745dbd339.png)

## IDプールの設定
先ほど作ったIDプールの管理画面から、「IDプールの編集」を開きます。

編集画面の中から、「認証プロバイダー」というタブを開きます。
![スクリーンショット 2020-09-28 20.06.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/b85ee96b-ace8-04b5-67fb-9ddbb68e4559.png)
認証されたロールの選択というところで、「トークンからロールを選択する」と設定します。

これで、ユーザーグループごとに設定されたロールを与えるように設定されました。

# APIのアクセス許可をIAMロールで行う
せっかくユーザーごとに違うロールを与えられるようになったので、何かそれを活かした仕組みを作ってみたくなります。

ここでは、管理者ユーザーのロールを持った人にだけ叩けるAPIを作ってみようと思います。

## ロールの設定
管理者ユーザーのロールポリシー(AdminAuth_Role)を以下のように編集します。

```diff
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "mobileanalytics:PutEvents",
                "cognito-sync:*",
                "cognito-identity:*",
+               "execute-api:Invoke"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}
```
`"execute-api:Invoke"`を追加することで、API Gatewayで公開されているAPIを叩くための権限を付けています。

## API Gatewayでの設定
API Gatewayで、先ほど作ったAPIのGETメソッドリクエストの設定欄を開きます。
![スクリーンショット 2020-09-28 20.33.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/90fe3cfb-3055-df96-72f1-f17311e8a084.png)
「認可」のところを「AWS_IAM」に設定します。
こうすることで、`"execute-api:Invoke"`のポリシーがついたロールを持ったユーザー以外のリクエストを弾くようにできます。

## コードの準備
前述した通り、ユーザーがIAMロールを持っていることを伝えるためには、適切なヘッダをつけたhttpリクエストを送る必要があります。
しかし、Amplifyを使えばこの面倒なヘッダ設定を全部自動でやってもらえます。

まずはコードを直します。
APIの認証が「Cognitoユーザープールでの認証の有無」ではなくなったため、`Authorization`にIDトークンをつける必要は無くなりました。なので、その記述を削除します。

```diff:App.js
- const showSecret = async function() {
+ const showSecret = function() {

    const apiName = 'TestAPI';  // index.jsで指定したnameと同じ名前にする
    const path = ''; 
-   const myInit = { 
-      headers: { 
-        Authorization: `Bearer ${(await Auth.currentSession()).getIdToken().getJwtToken()}`,
-      },
-   };


    // getメソッドを叩いている
-    API.get(apiName, path, myInit)
+    API.get(apiName, path)
      .then(response => {
        alert(response.data);
      })
      .catch(err => {
        alert(err);
      });

  };
```
コード上では、AWS APIを利用するためのSignature V4署名やsessionID付与を行なっていません。
しかし、Amplifyの`API.get()`メソッドを使うことで、それらの前処理を全てAmplifyに任せるようになっているので、上のコードで問題なく動きます。

実際に、上記のコードでAPIを叩いたときのhttpリクエストをディベロッパーツールで見てみると、確かに`Authorization`ヘッダにSignature V4で生成した署名を、`X-Amz-Security-Token`の部分にsessionTokenが自動で埋め込まれている様子が確認できます。
![スクリーンショット 2020-09-28 22.30.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520877/139a14d5-75e0-31af-a80b-024c666213c1.png)

補足:Signature V4での署名を付与する際の`Authorization`ヘッダの仕様については、次のAWS公式ドキュメントを参照ください。→[タスク 4: HTTP リクエストに署名を追加する](https://docs.aws.amazon.com/ja_jp/general/latest/gr/sigv4-add-signature-to-request.html)

# まとめ
本記事の内容はこれで全てです。

Cognitoを触ることを通して、Cognitoの使い方だけでなく、一般的な・AWSでの認証認可の知識も深まります。
ここで行なったことは基本的なことばかりだと思うので、あとは公式ドキュメントを読みながら色々いじってみてください。