[Amazon Cognito + AWS CLI + shell + α] Cognitoユーザー認証(MFAあり)で付与されるAWSの一時Credentialを取得する

# はじめに

AWSアカウントに対する操作で、AWS CLIやSDKを使用したいという場合、その認証情報としては`IAMユーザーのアクセスキー`を発行して使用するのが一般的かと思います。
ただアクセスキーは発行すると管理する必要がありますし、強い権限のアクセスキーが漏洩した場合、悪意を持って使用される危険性があります。

なので、環境の運用の仕方によってはIAMユーザーのアクセスキーを発行すること自体を避ける場合もありますよね。そこでAWSに慣れてくると候補に上がってくるのが、一時Credentialの利用かと思います。

一時Credentialを利用する方法としては、AWS IAM Identity Centerで実装するなどいくつかあると思いますが

今回は、

- **Amazon Cognito(以下単にCognito)のログインユーザーに付与されるIAMロールの一時Credentialを取得して使用する**

という機会がありましたので紹介します。

# 前提: Cognitoの設計

Cognitoは、機能を利用することで様々な認証認可の実装が可能な機能群です。

詳細は別の機会に譲ることとしますが、Cognitoの提供する機能を組み合わせて作っていくだけでも、実装できる認証認可パターンは複数あります。

それぞれメリット・デメリット、リスクが存在し、採用する設計の決定はそこを検討した上で行う必要があります。

## Cognitoユーザー(=一時Credential利用者)の要件

今回は下記のような要件でCognitoユーザーを定義します。

- 認証方法は「メールアドレス」と「パスワード」とする
- **ユーザーのMFA利用を有効(強制)とする**
    - MFAはGoogle Authenticatorを使用する。
- **ユーザーは自己サインアップを可能とする**
    - ユーザー自身でユーザーを登録できる状態。今回「パスワード確認・変更」のプロセスをスキップするため。
- ユーザーはCognitoで認証された場合、特定のAWSアカウントのリソースに対する権限を行使できる
- ユーザーは、`Cognitoユーザー登録(自己サインアップ)〜Credential取得`まで、AWS IAMの権限を特に必要としない。
    - ちょっとややこしいんですが、AWS CLIのコマンドで実施するものの、Cognitoのユーザー登録〜Credential取得までのコマンド実行は、AWS Credentialがなくても実行できます。

## Cognito管理者(=AWSアカウント管理者)の要件

- Cognito管理者はAWSリソースを操作する権限を有していること
    - Cognito関連のリソースを作成するために必要

## Tips: 上記の設計の場合のリスクについて

上記の設計の場合、

- **Cognito関連のリソース情報を知っている者が、自分でユーザー登録を実行し、一時Credentialを利用できてしまう可能性がある**

というセキュリティ上のリスクがあります。環境での仕様を検討する際はこの点を十分に検討するようにしてください。単純な回避策としては、ユーザー登録を管理者のみにするなどで可能かと思います。

自分は今回、

- Cognito管理者とCognitoユーザーが同一人物なので、Cognito関連のリソース情報が悪用されるリスクは低い

と判断してこの設計を採用しています。

# 1. Amazon Cognito の設定

ここはAWSアカウントのIAM Credentialを持っている状態で実施します。

## 1-1. Cognito ユーザープールの作成・設定

ユーザープールを作成します。下記以外はデフォルトの設定で進めました。

- サインインオプションは「Eメール」を選択する
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/34b2dd90-cd35-a23a-886a-ccbdacb0f116.png)

- MFA強制とする
    - ここで設定しておかないと、後々MFA認証が使用できないようです。
![Untitled 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/50ce05d2-c9bf-2311-fb9e-447a3e6eb9b9.png)


- ユーザーの自己登録を有効とする
    - チェックを入れるようにしてください。
![Untitled 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/3cd829bc-72a0-3fcc-3b2e-cf9e28e8b371.png)

- メッセージ配信設定
    - 今回はSESは利用しないので「Cognito で E メールを送信」を選択します。
![Untitled 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/6851fd4f-066c-8813-d928-7da001c03322.png)


- 高度なアプリケーションクライアントの設定
    - 「認証フロー」で「ALLOW_USER_PASSWORD_AUTH」を追加します。
![Untitled 4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/d1c0c01a-f05b-68e2-7a48-818a5ac4bcae.png)
上記の設定が終わったら`ユーザープール名`と`アプリケーションクライアント名`を入力してユーザープール作成を完了します。
`アプリクライアントID`を後ほど使用するので、メモしておきます。
![Untitled 5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/e8f4112a-c130-18b5-9319-c3077e01b513.png)

## 1-2. Cognito IDプールの作成・設定

次に、IDプールを作成します。設定で気にするところは下記です。

- 認証
    - Cognitoユーザープールを使用する設定とします。
![Untitled 6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/83aa7046-e982-37f7-4d7b-d5c83bfc3b4a.png)


- 許可を設定
    - IAM Roleを割り当てます。**ここに設定したIAM Roleの権限が、Cognitoで認証後に取得する一時Credentialの権限**になります。
    - Roleはここで新規作成しても、あらかじめ作っておいたIAM Roleを割り当てる形でもOKです
    - Roleに割り当てる権限は先程のリスクを考慮した上で設定してください。
![Untitled 7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/296f2685-a406-89df-f5b9-239440a4f4ba.png)


- ID プロバイダーを接続
    - 先程作成したCognitoユーザープールIDと、そのアプリクライアントIDを入力します。
![Untitled 8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/5daa7a9a-3c8e-1400-9938-1020ae5ccb7e.png)


ほかはすべてデフォルト入力とし、IDプール作成を完了します。

# 2. Cognitoへユーザー登録 + MFA検証

AWS CLIやコマンドでユーザー登録と、そのユーザーのMFA検証を実行していきます。途中でMFAを検証するために、シェル関連ツール(qrencode)を使用します。

ここから先はAWS CLIを使用しているため若干紛らわしいのですが、下記に示す操作は**該当AWSアカウントのIAM権限なしで実行できる**操作です。その事自体がリスクにもなりうるので注意が必要となります。

## 前提: シェルの実行環境

私の実行環境はMacBook Pro(Intel)です。他OSの場合は適宜読み替えてください。(OSに依存している部分はないかと思っています)

また、コマンドの実行に当たって下記をシェル環境に導入済みです。

- AWS CLI
- jq
- qrencode

## 2-1. Cognitoへのユーザー登録とユーザー検証の実施

まずユーザー登録(サインアップ)します。

```bash
# 自己登録によるユーザーサインアップ
CLIENT_ID=<Cognitoユーザープール　アプリケーションクライアントIDを入力>
USER_EMAIL="istio@example.com"
PASSWORD="P@ssw0rdO123"
aws cognito-idp sign-up \
  --client-id ${CLIENT_ID} \
  --username ${USER_EMAIL} \
  --password ${PASSWORD} \
  --user-attribute "Name=email,Value=${USER_EMAIL}"
```

次に、登録したEメールに届く確認コードを入力し、ユーザーのサインアップを完了させます。

```bash
# 確認コードによる検証  
CONFIRMATION_CODE=xxxxxx
aws cognito-idp confirm-sign-up \
  --client-id ${CLIENT_ID} \
  --username ${USER_EMAIL} \
  --confirmation-code ${CONFIRMATION_CODE}
```

## 2-2. MFAセットアップを開始

CLIのコマンドでMFA登録に向けて進めていきます。

チャレンジの情報やセッションの情報を使用するため、シェルの変数に格納していきます。

```bash
# Cognitoのユーザー認証フローを開始
Parameters=$(aws cognito-idp initiate-auth \
  --client-id ${CLIENT_ID} \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters "USERNAME=${USER_EMAIL},PASSWORD=${PASSWORD}" \
  --output json) && echo ${Parameters}

# 応答から必要な情報を格納
Session=`echo ${Parameters} | jq -r '.Session'`
USER_ID_FOR_SRP=`echo ${Parameters} | jq -r '.ChallengeParameters.USER_ID_FOR_SRP'`

# Cognitoが生成する一意の秘密キーを使用して、MFAのセットアップを開始
Secret_parameters=$(aws cognito-idp associate-software-token \
--session ${Session} \
--output text) && echo ${Secret_parameters}

# 応答から必要な情報を格納
Session=`echo ${Secret_parameters} | jq -r '.Session'`
SecretCode=`echo ${Secret_parameters} | jq -r '.SecretCode'`
```

## 2-3. qrencodeでMFA登録用のQRコードを生成

- QRコード生成コマンドを実行
    - `MyAWSAccountCognito`の部分は任意の文字列に変更してOKです
        - 詳細は割愛しますが、上記が**Google Authenticator上の表示名**になるのでわかりやすい文字列を設定するのがおすすめです。

```bash
qrencode -t ansi "otpauth://totp/MyAWSAccountCognito:${USER_EMAIL}?secret=${SecretCode}&issuer=MyAWSAccountCognito"
open output.png
```

## 2-4. QRコードを読み込んでGoogle Authenticatorに登録

CognitoをGoogle Authenticatorに紐づけます。

- 登録に成功すると、上記で設定した表示名で登録されます。(画像の2段目)
    - 登録でエラーが出る場合、QRエンコードがうまくいっていないか、必要なCognitoのセットアップ情報が取得できていない可能性があります。
![Untitled 9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/ae1de6f2-503e-5a22-d653-2b520489aa62.png)


## 2-5. 登録済みMFA情報でCognitoのMFA検証を実施

- 下記コマンドを実行します

```bash
# Google Authenticatorで発行された認証コードを入力
MFA_CODE=xxxxxx

# MFA検証を実施
aws cognito-idp verify-software-token \
--user-code ${MFA_CODE} \
--session ${Session}
```

## Tips: MFA検証の完了までにセッション切れになる場合

認証フローセッションのデフォルトの有効期限は3分です。コマンドになれるまでは、QRコードを発行〜MFA検証完了までを終える前に、認証フローの有効期限が切れることがあります。

トラブルシューティングが長引いているなどでセッションの有効期限を長くしたい場合は、アプリケーションクライアント設定の「認証フローセッション」で変更できます。ちょっとストレスを緩和できるかもしれません。

今回の方法であれば初回だけなので一時的に短くする分にはありだと思います。
![Untitled 10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2631819/85359a86-2aba-61ed-f24f-5c588df37528.png)


# 3. 作成したCognitoユーザーでログインし、AWS一時Credential取得

上記までが完了したら、一時Credentialを取得できる状態になります。

**ここから先の手順は、一時Credentialを取得する際に毎回行うことになります。**

また、下記の操作は**該当AWSアカウントのIAM権限なしで実行できる**操作になります。引き続き、その事自体がリスクにもなりうるので注意が必要となります。

## 3-1. 必要なCognito情報を控えておく

下記3つのIDが必要になりますので、作成済みのリソースの情報を控えておきます。

- Cognito　ユーザプールのID
- Cognito　ユーザープール　アプリケーションクライアントID
- Cognito　IDプールのID

## 3-2. コマンドでCognitoへログインし、AWS一時Credential取得

```bash
# 変数設定
REGION=ap-northeast-1
USER_POOL_ID=<CognitoユーザプールIDを入力>
CLIENT_ID=<Cognitoユーザプール アプリケーションクライアントIDを入力>
USER_EMAIL="istio@example.com"
PASSWORD="P@ssw0rdO123"
IDENTITY_POOL_ID=<CognitoIDプールのIDを入力>
COGNITO_USER_POOL=cognito-idp.${REGION}.amazonaws.com/${USER_POOL_ID}

# Cognito認証フローを開始
Parameters=$(aws cognito-idp initiate-auth \
  --client-id ${CLIENT_ID} \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters "USERNAME=${USER_EMAIL},PASSWORD=${PASSWORD}" \
  --output json) && echo ${Parameters}

# 必要なパラメータを格納
Session=`echo ${Parameters} | jq -r '.Session'`
USER_ID_FOR_SRP=`echo ${Parameters} | jq -r '.ChallengeParameters.USER_ID_FOR_SRP'`

# Google AuthenticatorのMFAコードを入力
MFA_CODE=xxxxxx

# あとは順番に実行
ID_TOKEN=$(aws cognito-idp respond-to-auth-challenge \
 --client-id ${CLIENT_ID} \
 --challenge-name SOFTWARE_TOKEN_MFA \
 --challenge-responses USERNAME=${USER_ID_FOR_SRP},SOFTWARE_TOKEN_MFA_CODE=${MFA_CODE} \
 --session ${Session} \
  --query "AuthenticationResult.IdToken" \
  --output text) && echo ${ID_TOKEN}

IDENTITY_ID=$(aws cognito-identity get-id \
  --identity-pool-id ${IDENTITY_POOL_ID} \
  --logins "${COGNITO_USER_POOL}=${ID_TOKEN}" \
  --query "IdentityId" \
  --output text) && echo ${IDENTITY_ID}

OUTPUT=$(aws cognito-identity get-credentials-for-identity \
  --identity-id ${IDENTITY_ID} \
  --logins "${COGNITO_USER_POOL}=${ID_TOKEN}") && echo ${OUTPUT}
```

上記が成功すると、変数OUTPUTに一時Credential情報が格納されます。
あとはCredential情報を各種ツールで利用できる状態にすればOKです。
- 下記に、`Credentialを環境変数に設定してAWS CLIを使用する場合`のコマンドを例示しておきます。

```bash
api_key=`echo ${OUTPUT} | jq -r '.Credentials.AccessKeyId'`
api_secret=`echo ${OUTPUT} | jq -r '.Credentials.SecretKey'`
security_token=`echo ${OUTPUT} | jq -r '.Credentials.SessionToken'`

# 環境変数に設定
export AWS_ACCESS_KEY_ID=$api_key
export AWS_SECRET_ACCESS_KEY=$api_secret
export AWS_SECURITY_TOKEN=$security_token

# Credentialが有効かどうかを確認
aws sts get-caller-identity
```

# おわりに

IAMユーザーを発行しない場合アクセスキー管理を考える必要がなくなるので、使い所がなくはないかもな?と思いました。
個人的には、この苦闘(?)を通してAmazon Cognitoの認証フローを理解することができたことが大きかったです。

ちなみに今回はローカルのシェルでCognitoの認証フローを実行することにこだわってみましたが、これらを`各プログラム言語のSDKを利用したアプリケーションコードで実装するサンプル`は、AWSドキュメントで公開されています。

[AWS SDK を使用した Amazon Cognito のコード例。 \- Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/service_code_examples.html)

# 参考文献

[MFAを有効化したCognitoユーザープールよりCLIでToken取得 \| クラウド・AWSのIT技術者向けブログ SKYARCH BROADCASTING](https://www.skyarch.net/blog/mfa%E3%82%92%E6%9C%89%E5%8A%B9%E5%8C%96%E3%81%97%E3%81%9Fcognito%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%97%E3%83%BC%E3%83%AB%E3%82%88%E3%82%8Acli%E3%81%A7token%E5%8F%96%E5%BE%97/)
[AWS SDK を使用した Amazon Cognito のコード例。 \- Amazon Cognito](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/service_code_examples.html)