AWS Cognito認証をPythonのみでサクッと動作確認する(Backend)

## 背景
私は普段、マイクロサービス開発しているバックエンドエンジニアです。
この度、フロントエンドの画面ができていない状態で、バックエンド単体で認証・認可周りの動作確認をすることになりました。この記事では、AWSマネジメントコンソールと簡単なPythonスクリプトだけで、Cognito認証の動作確認を行う方法をメモいたします。

#### より詳しく知りたい方はこちらもご覧ください

https://qiita.com/N700A/items/fe00128bd45abbf931d7

## 認証フローのイメージ
こちらは、Cognito認証フローの超ざっくりとしたイメージです。
- 最初にユーザーはConitoに対しEmail/Passwordでログインします
- ログインするとユーザーは、CognitoからIdTokenを発行されます
- ユーザーはトークンをBackend APIに投げます
- Backend APIは、ユーザーからもらったトークンを、Cognitoからもらった公開鍵で復号します
- Backend APIは、復号したJsonから正しいユーザーの値が取得できているか確認します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/27d927d1-fece-7634-bf57-59408ca7dce4.png)

詳しく知りたい方は、下記のOpenID Connectの書籍がオススメ

https://amzn.asia/d/bmXxJOs



## 手順
動作確認手順は下記の通りとなります。
- １. AWSマネジメントコンソールでCognitoのユーザープールを生成する
- ２. ユーザープールのID/パスワードログインを有効にする
- ３. Hosted UIを利用して初回ログインする
- ４. curlコマンドを利用して認証する
- ５. Pythonのサンプルコードに認証結果のIdTokenを貼り付けて、ユーザー情報を復号できることを確認する



## 1. AWSマネジメントコンソールでCognitoのユーザープールを生成する
初めに、AWSマネジメントコンソールからCognitoのユーザープールを新規作成します。このあたりは要点のみ記載していますので、詳しい方法は他記事も参考にしてください。
- Cognito ユーザープールのサインインオプション
  - ユーザー名
  - E メール

- ユーザー名の要件
  - ユーザーが任意のユーザー名でサインインすることを許可
  - ユーザー名の大文字と小文字を区別する
- 多要素認証
  - MFAなし
- ユーザーアカウントの復旧
  - セルフサービスのアカウントの復旧は無効にしてください
  - （今回はテスト検証なので、EmailやSMSの設定が面倒なため）
![多要素認証の設定..png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/9072f71e-2a40-51e2-2122-0551a4f5adbb.png)
- Cognito アシスト型の検証および確認
  - 
メッセージを自動的に送信しない にチェック
- Eメール
  - CognitoでEメールを送信を選択
![Eメールの設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/31a2a685-5ab3-a474-fbd4-41533e16a4d8.png)

- ユーザープール名
  - 適当な名前でOK（例: `test-user-pool`)
- ホストされた認証ページ
  - Cognito のホストされた UI を使用にチェック
![ホストされたUI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/ff47e788-f69f-8561-373b-fdb727f23289.png)
- 最初のアプリケーションクライアント
  - パブリッククライアント
  - アプリケーションのクライアント名
    - 適当な名前でOK（例: test-app) 
  - クライアントのシークレットを生成しない
  - 許可されているコールバック URL
    - ここでは適当に `localhost:8000/` にしておきます 


これにてユーザープールの作成が完了です。

## 2. ユーザープールのID/パスワードログインを有効にする
作成したユーザープールの画面に移動します。今回は、`test-user-pool`という名前で作成されています。
次に、ID/パスワードでログインできるように設定します。
- ユーザープールの画面で、「アプリケーションの統合」タブをクリックし、一番下までスクロールします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/6781070d-6b02-334a-ab38-ad4f1eba0d59.png)

- すると、アプリケーションクライアントのリストに先ほど作成した`test-app`という名前があるのでクリックします。
![ユーザープールアプリケーションクライアントのリスト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/911cb172-ae09-4b53-bd3f-49158fc87a6c.png)
- 
アプリケーションクライアント: test-app の画面の右上にある、「編集」ボタンをクリックします
- 認証フローの「ALLOW_UESR_PASSWORD_AUTH」をクリックして有効化します。
![ユーザープール_認証方法の設定..png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/e1346b6b-15ad-bc32-5480-8c672c6bf5b1.png)
- 「変更を保存」ボタンを押して完了です

## 3. Hosted UIを利用して初回ログインする
### テストユーザーを作成する
ユーザーを作成しログインまでを行います。本来はフロントで実装されるフローですが、今回は、CognitoのHosted UIを利用します。
- 作成したユーザープールの画面に移動します。ユーザータブから、ユーザーを作成ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/0c23d2e9-3681-69e3-acd5-bac66a7eea51.png)
  - ユーザー情報は下記にします
    - サインインに使用されるエイリアス属性
      - Eメールにチェック
    - ユーザー名: test-user
    - Eメールアドレス: test-user@example.com
    - 仮パスワード: User1234!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/8d0426fb-b2b7-cbb3-476d-1fcfe15f8b74.png)
  - 「ユーザーを作成」ボタンを押し、完了です。

### Hosted UIを利用できるように設定する
- ユーザープールの画面で、「アプリケーションの統合」タブをクリックし、ドメインの「アクション」ボタンを押下、Cognitoドメインの作成を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/5e701b53-bddd-4d54-5526-5b23ee1b2c1f.png)
  - ドメイン名は適当な名前でOKです（今回は、`backend-test`という名前にしました）
  - 再度、アプリケーションクライアントのリストに移動し、`test-app`をクリックします。
![ユーザープールアプリケーションクライアントのリスト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/911cb172-ae09-4b53-bd3f-49158fc87a6c.png)
- ホストされたUIの項目で、ステータスが「使用不可」になっていますので、「編集」ボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/6b366407-2a25-4f0c-9e19-94d029394ece.png)
- ホストされたUIを編集
  - URL (適当でOK)
    - http://localhost:8000  
  - ID プロバイダー: Cognitoユーザープール
  - OAuth 2.0 許可タイプ: 認証コード付与
  - OpenID Connect のスコープ: OpenID
- このように設定して、変更を保存します 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/e0147e80-bb9a-24da-7fec-da2eb97e32b6.png)
- そうすると、「ホストされたUIを表示」ボタンが有効になります
### Hosted UIでログインする
- 先程の、AWSマネジメントコンソールの画面で「ホストされたUIを表示」ボタンを押すと、下記のような初回ログイン画面が表示されます。
  - ここにEmail, 仮パスワードを入れます（これらは、先ほどAWSマネジメントコンソールで設定した値です）
    - Username: test-user@example.com
    - Password: User1234!  
    - <img width="420" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/d9189d2b-3381-a197-b88f-40786355bb1b.png"/>

- するとチェンジパスワード画面が出てきますが、今回は同じパスワードを使いまわします（セキュリティ的にアレですが、動作確認なので...）
  - <img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/ca2d7840-7aca-c810-d3fe-71a662a50c38.png">

- 「このサイトにアクセスできません」と表示されれば成功です。
  - localhost:8000に飛んでいますが、フロントの画面は無いので大丈夫。 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/52a32311-4d2d-37dd-4b9b-a7f04e17aa61.png)
これで、`test-user`にログインした状態となりました！

### 4. curlコマンドを利用して認証する
ログイン後、バックエンドで検証するためのトークンをもらわないといけないので、Cognito-APIにcurlでリクエストを飛ばします。

:::note warn
注意) Windowsユーザーは、WSLを使ってください。PowerShell,コマンドプロントだとうまくcurlできないよ
:::


```
curl --location --request POST 'https://cognito-idp.ap-northeast-1.amazonaws.com/' --header 'Content-Type: application/x-amz-json-1.1' --header 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' --data-raw '
{
    "ClientId": "75fcnhmtiimj0stoaso6dribsg",
    "AuthFlow": "USER_PASSWORD_AUTH",
    "AuthParameters":
    {
        "USERNAME": "test-user@example.com",
        "PASSWORD": "User1234!"
    }
}
'
```
- 備考
  - USERNAME, PASSWORDはさっき設定したものを入力 
  - クライアントIDはAWSマネジメントコンソールから拝借します
    - 添付画像のクライアントIDは使用できませんので悪しからず 
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349189/6ad6d205-c17a-2901-eb21-b8c72b317a7d.png)

こんな感じのJSONが返ってきたら成功です！
```
{
    "AuthenticationResult": {
        "AccessToken": "XXXXX",
        "ExpiresIn": 3600,
        "IdToken": "ZZZZZZZZ",
        "RefreshToken": "XXXXX",
        "TokenType": "Bearer"
    },
    "ChallengeParameters": {}
}
```

### 6. Pythonのサンプルコードに認証結果のIdTokenを貼り付けて、ユーザー情報を復号できることを確認する
最後にトークンの復号を試します。下記のサンプルコードをローカルで実行します。
- token, user_pool_id, client_id は各自値を設定してください

```python
import jwt

# 先ほど取得したIDトークンを貼り付ける
token = 'ZZZZZZZZ'

region = 'ap-northeast-1'
user_pool_id = 'ap-northeast-1_D0Ardyf8e' # AWSマネジメントコンソールからコピー
client_id = '75fcnhmtiimj0stoaso6dribsg' # AWSマネジメントコンソールからコピー

issuer = f'https://cognito-idp.{region}.amazonaws.com/{user_pool_id}'
jwks_url = f'{issuer}/.well-known/jwks.json'


jwks_client = jwt.PyJWKClient(jwks_url)

signing_key = jwks_client.get_signing_key_from_jwt(token)

token = jwt.decode(
    token,
    signing_key.key,
    algorithms=["RS256"],
    audience=client_id,
    issuer=issuer
)

if token['token_use'] != 'id':
    raise Exception('Invalid token_use')

print("復号成功！")
print(token)
```

（※ 上記のコードは、こちらの記事を引用しました。）

https://dev.classmethod.jp/articles/verify-cognito-with-pyjwt/#toc-1

### 出力結果
ユーザー情報を復号できていますね :smile: (にっこり）
```shell
復号成功！
{'sub': '87246a48-80e1-7000-fc65-0dc36d3852e7', 'email_verified': True, 'iss': 'https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_D0Ardyf8e', 'cognito:username': 'test-user', 'origin_jti': 'e1b85621-1692-4dbc-9d01-a734e9f94ef5', 'aud': '75fcnhmtiimj0stoaso6dribsg', 'event_id': '08bd6dd6-e030-4be6-9c74-2b67cac0dd2c', 'token_use': 'id', 'auth_time': 1730605969, 'exp': 1730609569, 'iat': 1730605969, 'jti': '26219b34-0993-4435-917e-4b5ddc58d949', 'email': 'test-user@example.com'}
```

## まとめ
今回はAWSマネジメントコンソールを使って、サクッとバックエンドでの認証の動作確認を行いました。FastAPIのアプリにちゃんと組み込む際は、下記の公式ドキュメントを参照すると良さそうですね。

https://fastapi.tiangolo.com/ja/tutorial/security/oauth2-jwt/#_3

Cognitoハンズオンとして、下記の記事も参考になりました

https://zenn.dev/kazu_o/articles/c4e4c7b96122c3