【Cognito】Amazon Cognito Identity SDK for JavaScriptで動くサンプルを作ってみた #1/2【JavaScript】

# はじめに
Amazon Cognito User Poolを使ってJavaScriptから認証するためのアプリのサンプルを作ってみました。
ついでに、Cognito User PoolをバックエンドのIdentity Providerとして、Cognito Federated Identitiesと連携させる処理も実装してみました。

利用したライブラリはこちらです。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#amazon-cognito-identity-sdk-for-javascript

本稿に関して作ったソースコードは下記に置いてあります。
https://github.com/tmiki/cognito-sample-js-webapp

分量が多くなったので、以下の2部構成としました。

[【Cognito】Amazon Cognito Identity SDK for JavaScriptで動くサンプルを作ってみた #1/2【JavaScript】](https://qiita.com/tmiki/items/f36edc6c7473f5baa398)　**←いまココ**
[【Cognito】Amazon Cognito Identity SDK for JavaScriptで動くサンプルを作ってみた #2/2【JavaScript】](https://qiita.com/tmiki/items/4e7d5f12e5a044f3837e)


# 目的・概要
本稿の目的は概ね下記の通りです。

- JavaScriptからCognito User Pool/Federated Identitiy Poolを利用する場合の基本的な仕組みを理解する。
- Amazon Cognito Identity SDK for JavaScriptの各処理のタイミングで、Webブラウザ側のLocalStorageに格納されている値がどのように変化するか理解する。

# 基本的な概念
## Amazon Cognito Identity SDK for JavaScript
Webアプリ／Mobileアプリで、Cognito Userpoolを利用したサインイン／サインアウト等の処理を簡単に実現するためのライブラリです。
https://github.com/aws-amplify/amplify-js/tree/master/packages/amazon-cognito-identity-js#amazon-cognito-identity-sdk-for-javascript

また、Cognito Federated Identitiesと連携させることもできます。

通常の開発に利用するのであれば、Amplify JSを使った方が良いと思います。
https://aws-amplify.github.io/docs/js/authentication

本稿では仕組みを理解するため、まずはAmazon Cognito Identity SDK for JavaScriptを使って実装してみました。


## Cognito User PoolとFederated Identities
User PoolとFederated Identity及び関連していそうな個所について調べた内容をまとめました。色々と過不足・事実誤認があるような気がするので、詳しい方はコメント頂けると助かります。

Cognitoには、UserPoolとFederated Identitiesの種類のサービスがある。

- Cognito UserPool
	- Cognito UserPoolはIdentity Provider（IdP）であり、OpenIdConnect相当のAuthentication/Authorizationが可能
	- Cognito UserPoolは自前のユーザDBを持つ（Identity Providerなので）
	- Cognito UserPoolは外部のOpenIdConnect準拠のIdPと連携できるが、まずはいったん忘れる
	- クライアントからのAuthenticationが成功すると、Cognito UserPoolは3つのTokenを返す。そのうちのAccess TokenがAuthorizationに利用される。
https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html
	- クライアントからAPI（API Gateway経由／ALB経由／その他オンプレミスのサーバのいずれかに関わらず）を実行する際に、Access TokenをHTTPリクエストヘッダ「Authorization:」として毎回付与する
	- APIサーバ側は、「Authorization:」リクエストヘッダの内容をverifyし、対象のユーザがAPIを実行する権限があることを確認する。
	- Verifyの手順はこのドキュメントに則ればよいと思う（※未実装）。https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
- Cognito Federated Identities
	- クライアントを認証し、AWSリソースを操作する権限を安全に与えるための、AWS独自の仕組み
	- AWSリソースを操作するために必要なAWS Credentials（AccessKeyとSecretAccessKeyのペア）を安全に発行・運用することがテーマ。
	- 外部のOpenIdConnect準拠のIdP（e.g. Amazon, Google, Facebook, GitHub）のアカウントを1つのFederated Identityに紐づけ、名寄せすることが出来る。（まずは必要ないので、一旦忘れる）
	- クライアントのAuthenticationが成功すると、Cognito Federated IdentitesはTemporary AWS Credentials（AccessKey/SecretAccessKey/SessionToken）を生成し、返却する
	- Temporary AWS Credentialsは、予め指定したIAM Roleに紐づく。
	- つまり、クライアントはAuthenticationが成功すると、対応するIAM RoleのPermissionsの権限を手に入れることができる、と言える。
	- Temporary AWS Credentialsがあれば、Signature V4の署名を付与し、AWS APIの実行、API Gateway APIの実行が出来る。
	- API Gatewayの設定で「Authorization」が「AWS_IAM」となっているものは、上記Temporary AWS Credentialsを用いて署名された署名を検証する、という意味。
	- Cognito Federated Identities自体は、Authentication処理そのものは提供しない
	- Authenticationの方式は、外部のOpenIdConnect準拠のIdPを利用する方式と、自前のユーザDB・認証ロジックを利用するCustom方式の2種類がある
	- Cognito Federated Identitiesに関するフローはこちら。https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html
	- Cognito User Poolと連携する方式は前者。上記のフローの「External Provider Authflow」が該当する。「Enhanced (Simplified) Authflow」がシンプルで良い。
	- 後者のCustom方式のフローは「Developer Authenticated Identities Authflow」。通常は「Enhanced Authflow」を使う。


認証後、クライアントが得られるものは、それぞれ以下の通り。

- Cognito UserPool
	- ID Token（Authenticationの連携に用いられる？少なくとも、Cognito Federated IdentitiesのGetId API及びGetOpenIdToken APIの実行時には必要。）
	- Access Token（※これがAuthorization:ヘッダとして指定される）
	- Refresh Token
- Cognito Federated Identities
	- Federated Identity PoolのIdentity Id
	- Temporary AWS Credentials（AccessKey/SecretAccessKey/SessionTokenの組み合わせ。権限はIAM Roleに紐づく）

## API GatewayでのAuthorization
Cognito User Pool及びFederated Identitiesは、API Gatewayと下記のように連携できます。
最近はUser Poolが出来たので、処理もフローもシンプルになっています。API Gatewayとの連携だけであれば、Federated Identitiesを使う必要はありません。
DynamoDBやS3などのその他のAWSリソースが必要な場合に、Federated Identitesとの連携が必要になってきます。

- 「Custom Authorizer」を使って、Authorization:ヘッダを確認する（Cognito User Poolとの連携）
	- Cognito UserPoolで認証成功したユーザに対し、API実行を許可させることを目的とした仕組み。
	- もともと、Lambda Authorizerと呼ばれていた機能で、API Gatewayに対するリクエストのverifyを実装する。
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
	- API Gateway宛のリクエストが行われるたびに、事前に実装・紐づけをしたAWS Lambda関数が実行される。
	- クライアントからAPI Gateway宛にリクエストが発行される際、「Authorization:」ヘッダは必ず付与されている。
	- 当該AWS Lambda関数の中で「Authorization:」ヘッダのverifyを行い、問題無ければAPIを実行させる。
	- Cognito UserPoolで発行されたAccess Tokenをverifyさせたい場合は、上記のAWS Lambda関数を個別に実装する必要はなく、API Gatewayを下記のように設定するだけで良い。
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html
- 各メソッドの「Method Request」で、「Authorization」方式に「AWS_IAM」を指定する（Cognito Federated Identitiesとの連携）
	- クライアントからAPI Gatewayへのリクエストの際には、Temporary AWS Credentialsを使ってリクエスト内容に署名（Signature V4）を付与する。
https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
	- API Gateway側で署名をverifyし、問題が無ければAPIを実行させる。
	- Temporary AWS CredentialsはIAM Role/Permissionsに紐づいている。
	- 対応するIAM RoleのPermissionsで、どのAPI Gatewayへのリクエスト許可する／拒否するか、指定することができる。

# 実行環境
上記サンプルの実行に必要な環境等は下記の通りです。
ローカル環境はnode.jsとnpmが動けば何でもOKです。WindowsでもLinuxでもmacOSでも。

VirtualBoxなどを使ってVMで動かしてもかまいません。ただしこの場合、Webブラウザから「localhost」でアクセスできるようにしておかないと、Webブラウザから怒られると思います。

- AWS environment
    - AWSアカウント（当たり前ですが、事前に作っておいてください）
- Local environment
    - npm 5.6.0
    - Node v8.11.4 （検証は左記バージョンで実施しましたが、v6やv10でも動くかもしれません。未検証。）

基本的には、 [README.md](https://github.com/tmiki/cognito-sample-js-webapp/blob/master/README.md) に従ってセットアップしてください。

AWS側のセットアップで重要なポイントは下記あたりでしょうか。

- Cognito User Poolを作る際に、Attributesを適切に設定する。「email」に「Required」のチェックを入れる。
- 作成したUser Pool用のApp clientsを作る際に、「Generate client secret」のチェックを外す。
- Cognito Identity Poolを作る際には、User PoolのPool IdとApp client Idが必要。

次のローカル開発環境のセットアップの際には、以下の3つのパラメータが必要になりますので途中で控えておきます。

- Cognito User PoolのPool Id
- Cognito User PoolのApp client Id
- Cognito Identity PoolのIdentity Pool Id

## Cognito User Poolをつくる
AWS Management Consoleから「Cognito」を開き「Manage User Pool」ボタンを押します。
最初はUser Poolがありません。画面の案内に従い、設定していきます。ここでは「sample-userpool-1」という名前で作ります。
![2018-11-14_01h47_01.png](https://qiita-image-store.s3.amazonaws.com/0/137777/2de1b7f3-aa6a-9244-83a3-be7beb173884.png)

Sign UpしたユーザをメールでVerificationするための設定が必要です。「Attributes」の「Which standard attributes do you want to require?」から、「email」を「Required」にしておきます。
「How do you want your end users to sign in?」のチェックは、ログインユーザ名としてemailアドレスを利用できるかどうか、の設定です。好みの問題ですが、ここでは有効にしておきます。
![2018-11-14_01h47_48.png](https://qiita-image-store.s3.amazonaws.com/0/137777/a726c6cf-995f-2dbd-0259-9643cec0c292.png)

併せてApp clientsも作ります。ここでは「sample-js-webapp-1」という名前で作ります。
**「Generate client secret」にチェックが入っているので外します。（重要）**
![2018-11-14_01h58_52.png](https://qiita-image-store.s3.amazonaws.com/0/137777/ecbb261e-5bbc-8cab-c2fc-7c23030996ce.png)

他の値が問題ないか確認したのち、最後に「Review」からパラメータ一覧を確認します。問題無ければそのまま「Create pool」ボタンを押します。
![2018-11-14_02h01_14.png](https://qiita-image-store.s3.amazonaws.com/0/137777/6369f5cd-4fb4-cbbb-9368-f681275b6870.png)

User Poolの作成に成功すると「Your user pool was created succesfully.」と表示され、設定されたパラメータを確認することが出来ます。
![2018-12-10_14h31_38.png](https://qiita-image-store.s3.amazonaws.com/0/137777/9de171ad-5cb5-00a4-d3b9-76b774ad9c63.png)

**Pool Id** と **App client Id** が後で必要になるので、控えておきます。

## Cognito Federated Identities Poolを作る
AWS Management Consoleから「Cognito」を開き「Manage Identity Pools」ボタンを押します。

「Create new identity pool」ボタンを押し、Identity Pool作成画面に遷移します。初回はいきなりこの画面が表示されるかもしれません。
まず、「Identity pool name」に任意の名前を入れます。ここでは「sample_identitypool_1」という名前で作ります。
次に、「Authentication providers」を指定します。「Cognito」のタブがあるので、「User Pool ID」と「App client id」を入れます。値は、先ほどUser Poolを作った際に控えているはずです。
![2018-11-19_03h50_14.png](https://qiita-image-store.s3.amazonaws.com/0/137777/20e2ae62-f89b-14ed-9a0c-c6557d969b1d.png)

入力したら「Create Pool」ボタンを押します。
次に、IAM Roleを設定する画面に遷移します。これは、今から作るIdentity Poolのユーザに割り当てられるIAM Roleとなります。既定の認証が成功してFederated Identity Idを取得したユーザに対して、付与する権限を制御することが出来ます。Policy/Permissionは後で変更できるので、ここでは変更しないでそのままにしておきます。
「Allow」ボタンを押すことで、Identity Poolの作成およびIAM Roleの作成が行われます。
![2018-11-19_03h50_36.png](https://qiita-image-store.s3.amazonaws.com/0/137777/d3957086-c9da-075a-7995-b6b11992bc1d.png)

作成が完了したら、Identity PoolのIDを確認します。対象のIdentity Poolを開いたのち、「Sample code」から確認することが出来ます。
![2018-11-19_03h51_12.png](https://qiita-image-store.s3.amazonaws.com/0/137777/aef7cebd-396c-b361-039f-4a6db0bff38a.png)

後で必要になるので **Identity Pool ID** は控えておいてください。

## ローカル開発環境をつくる
[README.md](https://github.com/tmiki/cognito-sample-js-webapp/blob/master/README.md) に従ってセットアップしてください。
環境固有の値として以下の値が必要になります。これらの値をindex.jsに反映させる必要があります。

- Cognito User Pool/Identity Poolを作ったRegion名
- Cognito User PoolのPool Id
- Cognito User PoolのApp client Id
- Cognito Identity PoolのIdentity Pool Id


# 実行してみる
AWS側のセットアップ、ローカル開発環境側のセットアップが完了したら、npm run startでwebpack-dev-serverを起動させてください。
Webブラウザでアクセスすると下記のような画面になります。
なお、nav barは動きませんので注意してください。本当はこちらのボタンから遷移させたかったのですが、ページ数が増えるのでやめました。

![2018-12-10_12h52_39.png](https://qiita-image-store.s3.amazonaws.com/0/137777/5008bdda-a837-fe3c-2226-a6b1166af265.png)

## 新規にユーザ登録をする
「Username」「Password」「email address」を入力し、「Sign Up」ボタンを押してください。登録に成功すると、「Message area」にその旨のメッセージが表示されます。
ここでは「testuser11」という名称で登録しています。

また、Consoleにもいろいろログを出すようにしているので、併せて見てみてください。

AWS Management ConsoleのCognito User PoolのDashboardを見てみましょう。先ほど登録したユーザが存在していること、「Status」が「UNCONFIRMED」となっていることが分かります。
![2018-12-10_13h09_08.png](https://qiita-image-store.s3.amazonaws.com/0/137777/a48458ec-6ad9-a9c2-79b2-fff518061fb6.png)

ユーザ名をクリックすると、詳細を確認することができます。
![2018-12-10_13h11_02.png](https://qiita-image-store.s3.amazonaws.com/0/137777/8b1cd089-622b-7ae3-67f5-d63cbb2e90ff.png)


先ほど「email address」に入力したメールアドレス宛に、「Your verification code」という件名でメールが届いています。6桁の数字が書かれているので、これをメモします。
![2018-12-10_13h41_23.png](https://qiita-image-store.s3.amazonaws.com/0/137777/2548b1f0-e07d-c19c-ae72-fd76634c925a.png)

先ほどの画面に戻り「Verification code」に上記でメモした数字を入力し、「Verify Your Code」ボタンを押します。
Verification Codeの確認に成功すると、Consoleに「result:  SUCCESS」と出力されます。
\# 処理結果をMessage areaに出力する処理は入れ忘れました…

![2018-12-10_13h43_17.png](https://qiita-image-store.s3.amazonaws.com/0/137777/88b675fe-32ab-c203-c1ce-0a5d8aeea817.png)

AWS Management ConsoleのCognito User PoolのDashboardを見てみましょう。先ほどのユーザの「User Status」が「CONFIRMED」になっていることが確認できます。
![2018-12-10_13h47_11.png](https://qiita-image-store.s3.amazonaws.com/0/137777/91102c79-0736-45f5-0485-86e02dc3d05a.png)

もし「Your verification code」メールが届いていない場合は、「Resend Confirmation By Email」ボタンを押すことで、メールを再送することが出来ます。この場合、メール記載の「Verification code」は変更となり、古いものは使えなくなります。


## パスワードを初期化する
2行目の「Reset Password (Verification Code Will Be Sent)」ボタンと「Confirm Password (Verification Code And New password Are Needed)」は、パスワード初期化のためのボタンです。
「パスワードを忘れた場合はこちら」、みたいな機能を実現するためのものです。

「Reset Password (Verification Code Will Be Sent)」ボタンを押すと、パスワードを初期化され、「Your verification code」メールが送信されます。
メールに記載の「Verification Code」と、「Password」に新しいパスワードを入れて、「Confirm Password (Verification Code And New password Are Needed)」ボタンを押すことで、新たなパスワードを設定することが出来ます。


## ログインする
上記までの処理では、ログインは行われません。（というように実装しています。）
「Username」及び「Password」にユーザ名とパスワードを入れて「Login」ボタンを押してください。
成功すると、Message areaに「session validity: true」と出力されます。

![2018-12-10_13h51_25.png](https://qiita-image-store.s3.amazonaws.com/0/137777/de8bad5e-7f08-2d21-10de-3c999d2312dc.png)

なお、当該SDKライブラリは、ログインに成功するとLocalStorageにログインセッションの情報を保存します。
そのため、画面遷移が発生してもLocalStorageからログインセッションの情報を取り出すことが出来るので、ログイン状態を維持することが出来ます。
ではLocalStorageを見てみましょう。CognitoIdentityServiceから始まるキーが幾つか保存されています。
![2018-12-10_13h58_17.png](https://qiita-image-store.s3.amazonaws.com/0/137777/0444897f-d8ad-76be-d7e6-59d639181a7f.png)

上記のログイン状態は、リロード後も自動的にSDK側で読み込まれます。
Webブラウザをリロードしてみてください。ログインしたユーザ名等が表示されると思います。

![2018-12-10_13h59_58.png](https://qiita-image-store.s3.amazonaws.com/0/137777/aa1bf177-fd4d-f36a-6aef-26aa9cadde8e.png)

このアプリは、既にログイン済みの状態の場合、「login status」及び「login username」等にその旨表示するように作ってあります。また、AccessToken、IdToken、RefreshTokenをそのまま表示するようにしています。
\# 各Tokenの改行が変なのは仕様です。CSSの調整をサボりました。

## AWS Credentialsを取得する
ログイン状態のまま、「Get AWS Credentials」ボタンを押してみましょう。Cognito Federated Identities用のIdentity Id、そしてTemporary AWS Credentials（AccessKey/SecretAccessKey/SessionToken）を取得することができます。
**クライアントはこのTemporary AWS Credentialsを利用することで、DynamoDBやS3などのAWSリソースを直接操作することが出来ます。**
なお、クライアントに付与される権限（IAM Permissions）は、先ほどFederated Identity Pool作成時に併せて作成したIAM Roleで指定されているものとなります。


![2018-12-10_22h06_11.png](https://qiita-image-store.s3.amazonaws.com/0/137777/5185782d-af1d-8179-d1db-1b5081aad2d6.png)

AWS Management ConsoleのCognito Federated IdentitesのDashboardを見てみましょう。先ほど取得したIdentity Idが増えていることが分かります。
![2018-12-10_14h31_38.png](https://qiita-image-store.s3.amazonaws.com/0/137777/3062effc-bc47-b147-078d-13298c8ec86c.png)

Cognito Federated Identity PoolのIdentity Idを取得し、Temporary AWS Credentialsを取得するためには、下記のフローに則り、クライアント側（下記の図ではDeviceとなっている個所）の処理を実装する必要があります

Identity Pools (Federated Identities) Authentication Flow - Amazon Cognito
https://docs.aws.amazon.com/cognito/latest/developerguide/authentication-flow.html

この処理は、Amazon Cognito Identity SDK for JavaScriptには実装されておりません。これはAWS SDK for JavaScriptの「AWS.CognitoIdentityCredentials」クラスで実装されています。
詳細は未確認ですが、当該クラスのOverviewから、恐らく「External Provider Authflow」の「Basic (Classic) Authflow」で実装されていると推測しています。

Class: AWS.CognitoIdentityCredentials — AWS SDK for JavaScript
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentityCredentials.html

これは具体的には、下記のAPIを実行します。

GetId - Amazon Cognito Federated Identities
https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetId.html

GetOpenIdToken - Amazon Cognito Federated Identities
https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetOpenIdToken.html

AssumeRoleWithWebIdentity - AWS Security Token Service
https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html


AWS SDK for JavaScriptをJavaScriptのライブラリとして指定するには、「amazon-cognito-identity-js」ではなく、「amazon-cognito-js」を指定します。
ソースコードの最初の方で下記のようなオブジェクトを初期化していますが、これがまさに「amazon-cognito-js」を使うための初期化処理となります。

```javascript
const AWS = require('aws-sdk');
require('amazon-cognito-js');
```


## ログアウトする
「Logout」ボタンを押してください。Message areaに「Signing out has been finished.」と出力されたらログアウト成功です。
LocalStorageから、Cognito User Poolのログイン状態の情報が消えたかどうか、「Application」から確認してみましょう。Federated Identity Poolに関する情報は残っていますが、User Poolの情報は消えていることが分かります。
![2018-12-10_15h22_15.png](https://qiita-image-store.s3.amazonaws.com/0/137777/17182019-90c2-8fd0-8bc7-8dfc929c26a3.png)

# Furthermore
環境構築と基本操作の説明が長くなってしまったので、JavaScriptの実装についての解説・AWSリソースの操作については稿を改めたいと思います。
あと、APIサーバ側のAuthorization処理は全くの手つかずなので、これもそのうちやろうと思います。

# おわりに
結局のところ、Cognito User PoolにせよFederated Identitiesにせよ、元が複雑な仕組みであるため、一朝一夕にはマスターすることが難しいと思います。
一つ一つ、概念を理解しながら実装を進め、理解を深めていく以外に正攻法は無いでしょう。

ちなみにAmplify JavaScriptの方だといろいろ便利になっているようなので、今後新規に開発するのであればこちらを使った方が断然良いですね。
https://aws-amplify.github.io/docs/js/authentication#sign-in