Amazon Cognitoの機能を使用して認証・認可処理をしてみる

# 概要
Amazon Cognitoの機能を使用してユーザーの認証・認可処理の簡易なサンプルを作成したので、構築までのプロセスと出てくる用語等の簡単な説明をしていきます。

まず、作成したサンプルの処理の流れは以下の図に示すようです。
![cognito_login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/9339296f-f027-c3d7-7302-dcc1d056735f.png)

大まかにいうと、ログインして、APIをたたくだけです。
APIをたたく時に渡されるアクセストークン(後述)に応じて、結果を返すかどうかを決めるようにしています。

また、ここでは運用やセキュリティについて厳密には考えて作成していないので、ところどころガバガバな設定があるのでご容赦ください。

## Cognitoの概要
Cognitoではウェブアプリケーションやモバイルアプリケーションの認証認可処理をサポートしてくれます。

Cognitoの代表的な構成要素は2つあり、ユーザープールとIDプールです。
ユーザープールではユーザーの情報（ユーザー名、パスワード、メールとか）を管理してくれます。
IDプールでは他のAWSサービスへの認証情報を管理してくれます。

今回はCognitoの他にAWSサービスを使わないので、ユーザープールのみを使用します。

## Cognitoの設定

今回のサンプルにおけるCognitoの設定方法をみていきます。

### ユーザープールの作成

まず、AWSコンソールから、Cognitoのトップページに行きます。
Cognitoのトップページにて、「ユーザープールの管理」ボタンを押下します。

![2019-06-23-15-45-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/2926eb36-c018-c771-5649-87fad34045e4.png)

---

表示されたページの右上にある、「ユーザープールを作成する」ボタンを押下します。
![2019-06-23-15-42-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/2ad93b37-401d-ee11-853f-3b812e712eb1.png)

---

プール名に適当な名前をつけて、「ステップに従って設定する」ボタンを押下します。
![2019-06-23-15-56-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/0d8bd704-8439-7a40-d16b-9b1acfdd1bb7.png)

---

デフォルトだと標準属性にemailが入ってますが、簡単のためここでは外しました。
「次のステップ」ボタンを押下します。
![2019-06-23-16-00-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/d9937cee-2e19-1874-79e1-7bfb8d7d6786.png)

---

そのまま「次のステップ」ボタンを押下します。
![2019-06-23-16-04-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/021e43fc-1634-965f-1248-4f62e71c8dc0.png)

---

「どの属性を確認しますか？」という箇所で「検証なし」を選択して、「次のステップ」を押下します。
![2019-06-23-16-06-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/716d008f-0c4a-3814-08ad-aba9afb82097.png)

---

そのまま「次のステップ」ボタンを押下します。
![2019-06-23-16-14-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/4dc3850a-f5e7-f0bf-68d1-9d496f1388bb.png)

---

そのまま「次のステップ」ボタンを押下します。
![2019-06-23-16-15-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/39a8bf9a-55cd-530e-e7a6-bf6351b2fa35.png)

---

「アプリクライアントの追加」を押下します。
![2019-06-23-16-19-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/65d4ecae-fb36-e620-6389-015cc76526b7.png)

---

「アプリクライアント名」に適当に名前を付け、簡単のため「クライアントシークレットを作成」チェックボックスを外して、「アプリクライアントの作成」を押下します。
**ここで作成、設定したクライアントからのみ本ユーザープールから情報を取得することができます。**クライアントの識別にはclient_idなるIDを用います。(「クライアントシークレットを作成」したとしたら、クライアントシークレットも用いてクライアントの識別をするようです)
![2019-06-23-16-23-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/a3dd0cd3-17e2-a27a-24fd-4ea44299b164.png)

---

「次のステップ」ボタンを押下します。
![2019-06-23-16-25-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/24319075-7d59-594e-c552-dd0c97f145c6.png)

---

そのまま「次のステップ」ボタンを押下します。
![2019-06-23-16-27-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/e2eb21c8-0feb-8054-4cc5-434c78978279.png)

---

「プールの作成」を押下します。
![2019-06-23-16-29-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/37412641-ad85-ff93-420f-9c748aeb4a14.png)

ここまでの手順でユーザープールを作成することができました。

### カスタムスコープの設定
今回の構成においては、リソースサーバーが送られてきたアクセストークンに内包されているscope属性によって、該当の通信にはAPIをたたく権利があるのかを検証します。
その検証の際に使用するスコープをここで設定します。

Cognitoのコンソール画面の左側の「アプリの統合」配下の「リソースサーバー」を押下してください。
表示された画面で、「名前」「識別子」「スコープ」を適当に設定し、変更の保存を押下してください。
「識別子」「スコープ」はサーバー側で認可の設定を行うときに使用します。

![2019-06-23-16-56-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/0a5c77c0-db21-c4f9-01fe-58857486207c.png)

### アプリクライアントの設定
「有効なIDプロバイダ」の「Cognito User Pool」にチェックを入れます。
また、コールバックURLにログイン後にリダイレクトさせる先となるURLを記入します。
ここでは、
https://github.com/nannany/cognito-authn-authz
をローカルにて`npm run dev`コマンドで立ち上げたアプリケーションのURLを指定しています。
その他、「許可されているOAuthフロー」の`Implicit grant`にチェックを入れ、「許可されているOAuthスコープ」の`aws.cognito.signin.user.admin`にチェックを入れます。
「許可されているOAuthスコープ」にて先ほど作成したカスタムスコープにもチェックを入れます。
<img width="632" alt="2019-06-23-18-28-ap-northeast-1.console.aws.amazon.com.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/51c0b953-c451-b78d-447f-d2c2acbbcdc5.png">

### Cognitoのログイン画面にアクセスするためのドメインを設定

Cognitoにはあらかじめ用意されているログインページがあります。
そのログインページにアクセスするために、Cognitoのドメインを設定します。
「ドメインのプレフィックス」に適当な名前を入れ、「変更の保存」を押下します。
![2019-06-23-18-50-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/cf4414b4-5bb8-743d-37f2-850831f357b5.png)


### Cognitoがホストしているログイン画面の確認

ここまで設定すると、Cognito側であらかじめ用意されているログイン画面を表示させることができます。
下記に示すURLにブラウザからアクセスすることで確認することができます。
`${client_id}`部分にはCognitoコンソール画面の「全般設定」の「アプリクライアント」を押下して表示される「アプリクライアントID」を記入してください。

```
https://nannany-sample.auth.ap-northeast-1.amazoncognito.com/login?response_type=token&client_id=${client_id}&redirect_uri=http://localhost:3000/myPage
```

以下のような画面が表示されます。
![2019-06-23-19-02-nannany-sample.auth.ap-northeast-1.amazoncognito.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/f7d983c7-9296-e380-428f-c40acff0caea.png)

### Cognitoのユーザープールにユーザーを追加
以下のコマンドを実行します。
awsのコマンドラインインターフェースが必要になるので、適宜インストールしてください。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-windows.html
`${client_id}`は上記同様に記入してください。
`${user_pool_id}`は、Cognitoコンソール画面の「全般設定」を押下して表示される「プールID」を記入してください。

```
aws cognito-idp sign-up --client-id ${client_id} --username nannany --password p@ssw0rd
aws cognito-idp admin-confirm-sign-up --user-pool-id ${user_pool_id} --username nannany
```

## 作成されたユーザーの確認
Cognitoコンソール画面の「全般設定」配下の「ユーザーとグループ」を押下すれば、先ほど作成したユーザーが登録されていることが確認できます。
![2019-06-23-19-16-ap-northeast-1.console.aws.amazon.com.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/2a13fd54-0e97-85e4-3003-ff2a87398b58.png)

## 画面からログイン

[先ほど](#Cognitoがホストしているログイン画面の確認)と同じ画面からログインします。
が、今回は以下のようなURLでログイン画面にアクセスします。

```
https://nannany-sample.auth.ap-northeast-1.amazoncognito.com/login?response_type=token&client_id=${client_id}&redirect_uri=http://localhost:3000/myPage&scope=nannany.sample.cognito/goodbye.read
```
違いはURLの末尾にscopeパラメータが加えられている点です。
この画面からログイン成功した暁には、`nannany.sample.cognito/goodbye.read`のscopeが与えられますよ、ということです。

## ログイン後のリダイレクト先のURL

リダイレクト先のURLは以下のようになっています。

```
http://localhost:3000/myPage#access_token=eyJraWQiOiJ+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fiIsImFsZyI6IlJTMjU2In0=.eyJzdWIiOiIzY2I2NDhkOS1hYjNiLTQyNjItOGM0ZS1jODM3YTU3OTVjZDYiLCJldmVudF9pZCI6ImVkZDRkZTZhLWM3OTUtNGFiZS1hMjU2LWIzMjNkYWMxZjVhMCIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoibmFubmFueS5zYW1wbGUuY29nbml0b1wvZ29vZGJ5ZS5yZWFkIiwiYXV0aF90aW1lIjoxNTYxNjM5NjM3LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfdXk2a1ZxSWxOIiwiZXhwIjoxNTYxNjQzMjM3LCJpYXQiOjE1NjE2Mzk2MzcsInZlcnNpb24iOjIsImp0aSI6IjAwYjlmODFlLWU0YTgtNDdlNS1hOGJkLTY4YWJkZWMzZjMzYyIsImNsaWVudF9pZCI6In5+fn5+fn5+fn5+fn5+fiIsInVzZXJuYW1lIjoibmFubmFueSJ9.Lwk8imEoaoTVeOVKuj4IQYJ5TUWf0eqO_mg9EMku181spiNq1elyDbgxzCIq-qtsonRGcl5MUEsRk9l8m4XSdUtjK7Bf3IB4X4pg-p5zOSnfmnp_fFZL9cCIplj3wq6lfMMMn0qi0SrDPSoaCUu_pNTGwC0Ya0RFdeNm5vpyftxqUuoMiMh5KS7p4XeJ0bMGi5o14t-ugey6N1GsORRhTAdPs3jk3D_-ADFI74T1QHg9uXm701p7VnlaStb2o2tpJ7ZEwtzu55OdVbaPxNdhKCP4Jkm2mAcXidHzEuxb564BtPBRk_vAYGeDb7CPIpCGGMySiyIwW_S7b_xwA-lh1g&expires_in=3600&token_type=Bearer
```
ここでの**アクセストークン**は**Bearer Token**という種類のトークンで、**JWT**という表現形式のものが格納されています。

### アクセストークンとは？

アクセストークンの定義は[RFC6749](https://openid-foundation-japan.github.io/rfc6749.ja.html#anchor8)に載っています。
保護されたリソースにアクセスすることが可能であるかどうかを、アクセストークンをもとに判断します。
今回の例だと、`access_token=~~`の部分の右辺がアクセストークンに当たります。

### Bearer Tokenとは？

Bearer TokenはBearer認証スキームにおいて使用されるトークンです。
認証の方式にはBasic認証やDigest認証などがありますが、その一種としてBearer認証があります。

Bearer Tokenの定義は、[RFC6750](https://openid-foundation-japan.github.io/rfc6750.ja.html) でされています。  
そこではBearer Tokenは以下のように定義されています。

```
署名なしトークン (Bearer Token)
セキュリティトークン. トークンを所有する任意のパーティ (持参人 = bearer) は, 「トークンを所有している」という条件を満たしさえすればそのトークンを利用することができる. 
署名無しトークンを利用する際, 持参人は, 暗号鍵の所持を証明 (proof-of-posession) するよう要求されない.
```

若干ピンと来なかったので、bearerという言葉の意味は英和辞典で調べると

```
運ぶ人、運搬人、かごかき、(小切手・手形の)持参人、(手紙の)使者、実のなる草木
```

といった意味があるそうです。
英英でも調べてみると、

```
someone who brings you information, a letter etc
```

という意味があるそうです。
つまり、bearerという単語の意味としては、何かを持ってきたその人本人、という意味があるようです。
認証の文脈では、Bearer Tokenを持って通信しにきたそのHTTPリクエストに許可を与えるという意図があるのだと思います。

### JWTとは？

JWTについては、[RFC7519](https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html)で定義されています。
JWTは任意の情報をやり取りするためのコンパクトでurl-safeな仕様です。
具体的な構造は、JWS([RFC7515](https://tools.ietf.org/html/rfc7515))やJWE([RFC7516](https://tools.ietf.org/html/rfc7516))という別の仕様で定められています。

今回はJWS Compact Serializationという形式をとったアクセストークンを使用しています。

#### JWS

JWSでは

* JWSヘッダー
* JWSペイロード
* JWS署名

の3つの情報が表現されます。
それぞれの値をBase64エンコードしたものをピリオドつなぎにしたものがアクセストークンとして使用されています。

今回の例だと以下のようになっています。

```
↓JWSヘッダー
eyJraWQiOiJ+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fiIsImFsZyI6IlJTMjU2In0=
.
↓JWSペイロード
eyJzdWIiOiIzY2I2NDhkOS1hYjNiLTQyNjItOGM0ZS1jODM3YTU3OTVjZDYiLCJldmVudF9pZCI6ImVkZDRkZTZhLWM3OTUtNGFiZS1hMjU2LWIzMjNkYWMxZjVhMCIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoibmFubmFueS5zYW1wbGUuY29nbml0b1wvZ29vZGJ5ZS5yZWFkIiwiYXV0aF90aW1lIjoxNTYxNjM5NjM3LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfdXk2a1ZxSWxOIiwiZXhwIjoxNTYxNjQzMjM3LCJpYXQiOjE1NjE2Mzk2MzcsInZlcnNpb24iOjIsImp0aSI6IjAwYjlmODFlLWU0YTgtNDdlNS1hOGJkLTY4YWJkZWMzZjMzYyIsImNsaWVudF9pZCI6In5+fn5+fn5+fn5+fn5+fiIsInVzZXJuYW1lIjoibmFubmFueSJ9
.
↓JWS署名
Lwk8imEoaoTVeOVKuj4IQYJ5TUWf0eqO_mg9EMku181spiNq1elyDbgxzCIq-qtsonRGcl5MUEsRk9l8m4XSdUtjK7Bf3IB4X4pg-p5zOSnfmnp_fFZL9cCIplj3wq6lfMMMn0qi0SrDPSoaCUu_pNTGwC0Ya0RFdeNm5vpyftxqUuoMiMh5KS7p4XeJ0bMGi5o14t-ugey6N1GsORRhTAdPs3jk3D_-ADFI74T1QHg9uXm701p7VnlaStb2o2tpJ7ZEwtzu55OdVbaPxNdhKCP4Jkm2mAcXidHzEuxb564BtPBRk_vAYGeDb7CPIpCGGMySiyIwW_S7b_xwA-lh1g
```

JWSヘッダーとJWSペイロードをBase64デコードすると以下のような感じのJSONになります。

```
↓JWSヘッダー
{
    "kid": "~~~~~~~~~~~~~~~~~~~~~~~",
    "alg": "RS256"
}
↓JWSペイロード
{
    "sub": "3cb648d9-ab3b-4262-8c4e-c837a5795cd6",
    "event_id": "edd4de6a-c795-4abe-a256-b323dac1f5a0",
    "token_use": "access",
    "scope": "nannany.sample.cognito/goodbye.read",
    "auth_time": 1561639637,
    "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_uy6kVqIlN",
    "exp": 1561643237,
    "iat": 1561639637,
    "version": 2,
    "jti": "00b9f81e-e4a8-47e5-a8bd-68abdec3f33c",
    "client_id": "~~~~~~~~~~~~~~~",
    "username": "nannany"
}
```

JWS署名には、`エンコード済JWSヘッダー.エンコード済JWSペイロード`をJWSヘッダーから導き出される暗号鍵、アルゴリズムで暗号化した値が入ります。（たぶん）

### APIへのGETリクエスト

RFC6750では、以下の3つがBearer Tokenをリソースサーバ(今回でいうとAPIのエンドポイントが用意されているサーバ)へ送信する方法として定義されています。

* Authorizationリクエストヘッダフィールド
* Formエンコードされたボディパラメータ
* URIクエリパラメータ

今回は1つ目のAuthorizationリクエストヘッダフィールドに含める方法でAPIへGETリクエストを送っています。

具体的に送られるリクエストは以下のようです。

```
GET http://localhost:8080/goodbye HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: application/json, text/plain, */*
Origin: http://localhost:3000
Authorization: Bearer eyJraWQiOiJ+fn5+fn5+fn5+fn5+fn5+fn5+fn5+fiIsImFsZyI6IlJTMjU2In0=.eyJzdWIiOiIzY2I2NDhkOS1hYjNiLTQyNjItOGM0ZS1jODM3YTU3OTVjZDYiLCJldmVudF9pZCI6ImVkZDRkZTZhLWM3OTUtNGFiZS1hMjU2LWIzMjNkYWMxZjVhMCIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoibmFubmFueS5zYW1wbGUuY29nbml0b1wvZ29vZGJ5ZS5yZWFkIiwiYXV0aF90aW1lIjoxNTYxNjM5NjM3LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAuYXAtbm9ydGhlYXN0LTEuYW1hem9uYXdzLmNvbVwvYXAtbm9ydGhlYXN0LTFfdXk2a1ZxSWxOIiwiZXhwIjoxNTYxNjQzMjM3LCJpYXQiOjE1NjE2Mzk2MzcsInZlcnNpb24iOjIsImp0aSI6IjAwYjlmODFlLWU0YTgtNDdlNS1hOGJkLTY4YWJkZWMzZjMzYyIsImNsaWVudF9pZCI6In5+fn5+fn5+fn5+fn5+fiIsInVzZXJuYW1lIjoibmFubmFueSJ9.Lwk8imEoaoTVeOVKuj4IQYJ5TUWf0eqO_mg9EMku181spiNq1elyDbgxzCIq-qtsonRGcl5MUEsRk9l8m4XSdUtjK7Bf3IB4X4pg-p5zOSnfmnp_fFZL9cCIplj3wq6lfMMMn0qi0SrDPSoaCUu_pNTGwC0Ya0RFdeNm5vpyftxqUuoMiMh5KS7p4XeJ0bMGi5o14t-ugey6N1GsORRhTAdPs3jk3D_-ADFI74T1QHg9uXm701p7VnlaStb2o2tpJ7ZEwtzu55OdVbaPxNdhKCP4Jkm2mAcXidHzEuxb564BtPBRk_vAYGeDb7CPIpCGGMySiyIwW_S7b_xwA-lh1g
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
Content-Type: application/json;charset=utf-8
Referer: http://localhost:3000/myPage
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,ja;q=0.8
```

## リソースサーバ側実装

[このページ](https://qiita.com/suke_masa/items/0f75fa75a22a6551065b)を参考に実装しました。
ソースコードは以下のものを使いました。
https://github.com/nannany/cognito

起動させる場合には、環境変数`userPoolId`に使用するユーザープールの`userPoolId`を入れる必要があります。

## 動作gif

[ここ](#画面からログイン)で設定したscopeをに応じて、与えられる権限が変更されます。

以下、scopeを変えてそれぞれどのような動きをするかを載せました。

`scope`を`nannany.sample.cognito/goodbye.read`にした場合
![goodbye_only.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/4f9c0aca-e64d-992f-50f9-027e7b147f45.gif)

`scope`を`nannany.sample.cognito/hello.read`にした場合
![hello_only.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/44b95527-4259-6178-4ddd-e5c5c03d41af.gif)

`scope`を`aws.cognito.signin.user.admin`にした場合
![admin.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/105281/05f5bb44-efc0-82aa-17e2-fb96aac5cd33.gif)

---
参考URL
[Springまわりの設定のはなし](https://qiita.com/suke_masa/items/0f75fa75a22a6551065b)
[OAuth2まわりのはなし](https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be)
↑に限らず、認証・認可系の話をよく投稿されている川崎さんの記事はとても理解の助けになりました。
[AWS公式](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html)