[android java]AWSのAPIgateway + Cognito Userpoolを使用してみる v2

## はじめに
Cognito認証を使用した各種サービスへのアクセスに非常に困惑したので備忘録として情報を記載する
特にAWSMobileClientの仕様が理解できておらず困惑したので書き留めておく．

※2024年1月頃に類似の記事をアップロードしたが，内容に誤りがあったためアーカイブした．こちらは改定版である．

## 何をするか
android環境で，APIgatewayに3つの状況でリクエストを送信する．
- 1.認証・認可なしの場合
    - [前回の記事はこちら](https://qiita.com/sanjolin_chun/items/832fb1c6edfb3ea10ed5)
- 2.AWS cognito userpoolを使用する
    - いまここ
- 3.AWS cognito userpool + idpoolを使用する
    - [記事リンク](https://qiita.com/sanjolin_chun/items/b47b1ccf536b3c4324f0)

本記事では，2のcognito userpoolを使用する場合について解説する．
前提条件として，1の認証・認可なしの場合のセットアップが完了している前提で記述する．

## 環境
各種jdkのバージョンや，ライブラリのバージョンは前回の記事と同じ．

minSdkVersion 28
targetSdkVersion 34
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
``` build.gradle(app)
    implementation 'com.amazonaws:aws-android-sdk-core:2.72.0'
    implementation 'com.amazonaws:aws-android-sdk-apigateway-core:2.72.0'
    implementation 'com.amazonaws:aws-android-sdk-s3:2.72.0'
    implementation 'com.amazonaws:aws-android-sdk-auth-core:2.72.0'
    implementation 'com.amazonaws:aws-android-sdk-auth-ui:2.72.0'
    implementation 'com.amazonaws:aws-android-sdk-auth-userpools:2.72.0'
    implementation 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.72.0'
    implementation 'com.amazonaws:aws-android-sdk-mobile-client:2.72.0'
```
AWSアーキテクトは下記のとおりになる．

![architect_apigateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/109ba41e-441d-6866-bd02-b3cf9f98b1d8.png)

①： userpoolへのリクエスト．このアクセスには認証に必要な情報が含まれる(IDやパス，emailなど)
②： userpoolからのレスポンス．大まかに，アクセストークン，リフレッシュトークン，IDトークンがある．
③： APIgatewayへのリクエスト，CognitoUserpoolと連携して，トークンが有効かたしかめる
④： APIgatewayのレスポンス．内容は前回記事と同じ．

## 手順

### Cognito Userpoolを作成する．
- Cognitoから[userpool]→[userpool]を作成
- userpoolのみを作成．今回はfederationしないので，idpoolは必要ない．
- username,emailを認証に使用
- パスワードポリシーは適宜設定．テスト用なら，カスタムにして最小制限にすればよい．
- MFAはなし．
- 今回はテストのため，ユーザアカウントの復旧も無効とします．
- 自己登録を有効化のチェックを外す
- Cognito アシスト型の検証および確認はどちらでも良い，今回は有効にした．
- メッセージ配信は，Cognito Emailでメールを送信
- userpool名は適当に入力．
- ホストされたUIは無効．
- アプリケーションは「パブリッククライアント」
- アプリ名は適宜
- クライアントシークレットは「生成しない」

![スクリーンショット 2024-01-17 20.22.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/dd3cc2fb-3066-b11c-3ecd-e1c7c435c45b.png)
![スクリーンショット 2024-01-29 19.36.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/244b9731-00e4-302a-9936-29821ca8be1b.png)
![スクリーンショット 2024-01-29 19.36.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/1b11e36e-2952-142c-198b-5557a48d6a60.png)
![スクリーンショット 2024-01-29 19.39.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/e3d18db4-bade-0bbe-3072-736e532531c9.png)
![スクリーンショット 2024-01-29 19.42.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/e5a46b96-678d-2437-519e-2dafb41f8152.png)
![スクリーンショット 2024-01-29 19.42.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/e7b4d26c-535b-0be5-2bfc-dfba2ce0327e.png)
![スクリーンショット 2024-01-29 19.43.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/e77bc41b-4f42-30e9-70a2-334a22158d95.png)
![スクリーンショット 2024-01-29 19.43.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/068104de-a6f7-0732-8b2b-3a04da7a41ce.png)
![スクリーンショット 2024-01-29 19.44.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/ea35f9fc-dd82-c278-bc4c-2f4c1ad42325.png)


### Cognito Userpoolにユーザを作成する．
- 作成したユーザプールを押下
- [ユーザ]→[ユーザを作成]
- サインイン情報のユーザ名，Eメールにチェック
- ユーザ名，Eメールアドレスに適当なアドレスを登録
- Eメールを検証済みにしておく．
- パスワードの設定で適当に初期パスワードを設定しておく

![スクリーンショット 2024-01-29 19.53.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/e4fa2a81-2185-9b49-0432-2ddf7572eabc.png)

ユーザの登録が完了すると，ユーザのパスワードの変更が求められる状態となる．作成したユーザの「確認ステータス」が「パスワードを強制的に変更」になっていることが確認できる．
![スクリーンショット 2024-01-29 19.54.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/430a1028-01e8-c121-2528-5557e6dd8c0f.png)

所有しているメールアドレスを登録することで解決しても良いが，APIの確認のため，ここでは，コマンドでメールアドレスを変更してみる．

Cognitoの認証フローをユーザとして体験した人は知っていると思うが，サインイン成功時にパスワードの変更を求められる．APIでも同様の処理となっており，「サインインを試行」→「パスワードの変更」と実行していく．

今回は，curlで実行していきたい．そのための準備を実施する．Cognitoアプリケーションに```USER_PASSWORD_AUTH```を許可する．今回は簡単のためこちらの認証方法を利用していくが，他の認証方法は，暗号化が施されており，安全性に富んでいるため，実際に利用する場合は，そちらの利用を推奨する．

- [(作成したユーザプール)] → [アプリケーションの統合] → [(作成したアプリケーションクライアント名)] → アプリケーションクライアントに関する情報の[編集]→認証フローに[ALLOW_USER_PASSWORD_AUTH]を追加

![スクリーンショット 2024-01-29 19.49.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/d9ce029d-20cb-ec57-d732-cc4de1f099a4.png)
![スクリーンショット 2024-01-29 19.50.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/7e0c0b20-e045-f0c7-b950-309f51818887.png)



それでは，APIを叩いていく．initiateAuthや他のAPIへのアクションについては[こちらの公式ドキュメントを参照](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html)

#### サインインの試行
```.sh
curl -X POST \
  -H 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' \
  -H 'Content-Type: application/x-amz-json-1.1' \
  --data @testuser.json \
  https://cognito-idp.[リージョン].amazonaws.com/[CognitoIDpool名]

```

```testuser.json
{
  "ClientId": "[アプリケーションクライアントのID]",
  "AuthFlow": "USER_PASSWORD_AUTH",
  "AuthParameters": {
    "USERNAME": "[作成したユーザ名]",
    "PASSWORD": "[ユーザ作成時に設定したパスワード]"
  }
}

```
以下のようなレスポンスが戻る．
``` response.json

{
  "ChallengeName": "NEW_PASSWORD_REQUIRED",
  "ChallengeParameters": {
    "USER_ID_FOR_SRP": "[testuser]",
    "requiredAttributes": "[]",
    "userAttributes": "{\"email_verified\":\"true\",\"email\":\"[test.test@test.co.jp]\"}"
  },
  "Session": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX..."
}
```

このセッションIDを使用して，パスワードの修正リクエストを実行する．
#### パスワードの変更

```.sh
curl -X POST \
  -H 'X-Amz-Target: AWSCognitoIdentityProviderService.RespondToAuthChallenge' \
  -H 'Content-Type: application/x-amz-json-1.1' \
  --data @changePasswordFirst.json \
  https://cognito-idp.[リージョン].amazonaws.com/[CognitoIDプール名]
```

```changePasswordFirst.json
{
    "ClientId": "[アプリケーションクライアントID]",
    "ChallengeName": "NEW_PASSWORD_REQUIRED",
    "ChallengeResponses": {
        "NEW_PASSWORD": "[新しいパスワード]",
        "USERNAME": "[ユーザ名]"
    },
    "Session": "[ここに1つ前のレスポンスにあったセッション文字列をコピペ]"
}
```

レスポンスが戻る

```__response.json
{
    "AuthenticationResult": {
        "AccessToken": "XXXXXXXXXXXXXX.... ",
        "ExpiresIn": 3600,
        "IdToken": "XXXXXXXXXXXXXXXY.... ",
        "RefreshToken": "XXXXXXXXXXXXXXZ.... ",
        "TokenType": "Bearer"
    },
    "ChallengeParameters": {}
}
```

ここまで完了すれば，AWSコンソールのユーザプールから確認できる作成したユーザの「確認ステータス」が，「確認済み」になっているはずである．
![スクリーンショット 2024-01-29 19.59.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/0a68eccd-abfb-8e31-8667-693ab62fe6ab.png)



ここまでで，ユーザプールの作成とユーザプールに紐づいたアプリケーションクライアントの設定と1人分のユーザ登録が完了した．
### APIにCognitoUserpoolをオーソライザーとして登録する
APIgatewayのAPIは，前回に作成したものに追加する形でCognito認証の必須設定を入れる.

- [APIgateway]→[API]→[(作成したAPI)]→[オーソライザー]から認可方法を登録する．
- 今回は，congitoを使用するので，cognitouserpoolを選択する．
- [オーソライザーを作成]から，適当なオーソライザー名と上記で作成したユーザプールを指定する．

![スクリーンショット 2024-01-29 20.03.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/c9cf671c-09b6-bb91-d8f1-d112d58693eb.png)

トークンのソースは```Authorization```を入力すること(これは，Cognito UserpoolとAPIgatewayのAWS間でやりとりされる値のキー名だと思う．つまり，APIgatewayがリクエストヘッダに含まれるAuthorizationパラメータとCognitoユーザプールに存在するToken情報を突合してくれていることになる)．
### APIにCognitoUserpool認可を設定する
APIgateayはメソッドごとに別々のオーソライザーを設定することができる．
- 作成したAPIgatewayのメソッドの画面の「メソッドリクエストを設定」項目から「編集」を押下する．
- 認可から作成した，cognitoユーザプールオーソライザーを選択
- 他の項目は編集することなく，「保存」を押下
- <strong>認可の設定の変更の反映もデプロイが必要です．変更した内容は必ずデプロイしましょう</strong>

![スクリーンショット 2024-01-29 20.07.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/574694/13b78d9e-d20d-66a6-d656-994505c5112a.png)

### curlでCognitoUserpool認可をテストする
上記のCognitoユーザプールオーソライザーが認可情報に設定されたAPIを叩いてみる．
まずは，認可で使うトークンを取得しなければならないので，冒頭で設定したCognitoに対してサインインを行い，各種トークンをもらう

```.sh
curl -X POST \
  -H 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' \
  -H 'Content-Type: application/x-amz-json-1.1' \
  --data @testuser.json \
  https://cognito-idp.[リージョン].amazonaws.com/[CognitoIDpool名]

```


一度目のパスワードの再設定がうまくいっていれば，下記のようなレスポンスが戻るはずである．レスポンスのIdTokenを使用してAPIgatewayにアクセスする．
```__response.json
{
    "AuthenticationResult": {
        "AccessToken": "XXXXXXXXXXXXXX.... ",
        "ExpiresIn": 3600,
        "IdToken": "XXXXXXXXXXXXXXXY.... ",
        "RefreshToken": "XXXXXXXXXXXXXXZ.... ",
        "TokenType": "Bearer"
    },
    "ChallengeParameters": {}
}
```

それでは，APIgatewayで作成したメソッドにアクセスする．例はGETメソッドを対象にしている．

```
curl -X GET \
    -H "Authorization: Bearer [IDTokenをここに] \
    -H "Content-Type: application/json" \
    [作成したAPIメソッドのエンドポイントをここに]
```

> 上記で使用するのは，AccessTokenではなく，IDTokenである．
> - AccessToken...IdPoolにおいて，ログインが成功していることを示す認証情報であり，例えば，[ユーザのパスワードの再設定](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ChangePassword.html)に使用する．
> - Idtokenはユーザがログインした際に生成されるユニーク値で，「ID/Pass」に紐づく一時的な認証情報である．



### Androidデバイスで認可を行うための準備．
さて，ここからandroidに戻る．
AndoridでAWSCongito認証を使用するためには，Cognito Idpoolの設定を教える必要がある．様々な方法がるが，今回はアプリケーションにawsconfigurationファイルを作成して対応する．
- サンプルに沿って```app/src/main/res/raw```配下に``awsconfiguration.json````を作成する．

```awsconfiguration.json
{
  "Version": "1.0",
  "CognitoUserPool": {
    "Default": {
      "PoolId": [プールID],
      "AppClientId": [クライアントアプリケーションID],
      "Region": [リージョン]
    }
  }
}
```
- PoolIDは```[リージョン]_XXXX```のような形式の値．作成したユーザプールのトップに記述されている．
- クライアントアプリケーション名は，作成したユーザプールの[アプリケーションの統合]から確認できる．

### Androidデバイスのアプリケーションから認可を行う
主要な手続きとしては，下記のとおりである．
1. AWSMobileClientを初期化する．具体的には，```awsconfiguration.json```の設定を読み込む
2. AWSMoblieClientを使用して，Cognito Userpoolにログイン試行する．
3. (承認できた場合) IDTokenを使用してLambdaにAuthorizerヘッダをつけてリクエストする．

> 実のところ，LambdaでUserpoolのIDトークンを使用する場合，2と3の接続が非常に悪い．簡単な実装を検討する場合，後述する記事(IDユーザプールの導入)を使用すべきである．少し具体的に記述すると，Lambdaを実行するための```ApiClientFactory```に関して，Authorizationパラメータ認証情報を含める関数がなく，lambdaへのリクエストをほとんど自分で実装する必要がある．一方で，IDプールを使用すると，別のパラメータで認証し，さらに```ApiClientFactory()```でサポートされているため，実装が楽に済む．


- ApinameClient...APIgatewayで生成したクライアント．エンドポイント等の情報を含有
- ApiResponseModel...APIagteayで生成したレスポンスのモデル．

```AWSClient.java

public class AWSClient {

    private static AWSMobileClient awsMobileClient;
    
    public void initializeAwsClient(Context context){
        awsMobileClient = AWSMobileClient.getInstance();
        _initializeAwsClient(context);
    }

    private void _initializeAwsClient(Context app) {
        // awsconfiguration.jsonのリソースIDを取得
        int resourceId = app.getResources().getIdentifier(
            "awsconfiguration", "raw", app.getPackageName());

        // 初期化を実行する．
        awsMobileClient.initialize(app, new AWSConfiguration(app, resourceId), new Callback<UserStateDetails>() {
            @Override
            public void onResult(UserStateDetails details) {
            // ここで得られるUserStateDetailsから状態がわかる．（後述補足 ※1）
                tryLogin();
            }
            @Override
            public void onError(Exception e) {
                // ここが呼ばれるのは，ほぼawsconfigureの設定が間違えているケース
            }
        });
    }
    
    private void tryLogin(){
        String username = ""/*[ユーザネーム]*/;
        String password = ""/*[パスワード]*/;
        awsMobileClient.signIn(username, password, null,new Callback<SignInResult>() {
            @Override
            public void onResult(final SignInResult signInResult) {
                 // signInResult.getSignInState() でログインの詳細がわかる．(後述補足 ※2)
                 getTestApiGateway();
             }
            @Override
            public void onError(Exception e) {}
        });
    }

    
    // APIgatewayで設定したAPIへのアクセス
    // メインスレッドで実行しないでね
    public ApiResponseModel getTestApiGateway() {
        // 前の記事で作成したモデル
        ApiResponseModel model;
        // IDトークン
        String idToken ="";
        try {
            // トークンを取得
            idToken = awsMobileClient.getTokens().getIdToken().getTokenString();
        }catch (Exception e){
            
        }
        // Apiファクトリをインスタンス化する．
        // このクラスにはAuthorizationを含むリクエストを作成できないので，部分的に使用して，新しくリクエストを作成する．
        final ApinameClient client = new ApiClientFactory().build(ApinameClient.class);
        // 新しいリクエストを作成する．
        // APIgatewayのホスト先はApiClientから取得できるが，それ以外はここで記入する必要がある．
        ApiRequest request =
                new ApiRequest(client.getClass().getSimpleName())
                        .withPath("[ここにパスを入力(/test/app/)]")
                        .withHttpMethod(HttpMethodName.valueOf("GET")) 
                        .addHeader("Authorization", "Bearer "+idToken);  //Use JWT token

        ApiResponse response= client.execute(request);
        try {
        // レスポンスのパーサも自分で実装する必要がある(これを回避する方法があれば知りたい)．
            // 一旦stringとして格納
            String jsonString = convertInputStreamToString(response.getContent());
            // Gsonインスタンスを作成
            Gson gson = new GsonBuilder().create();
            // JSONをapigatewyで作成したモデルにパース
            model = gson.fromJson(jsonString, ApiResponseModel.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return model;
    }

     private String convertInputStreamToString(InputStream inputStream) throws IOException {
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            stringBuilder.append(line);
        }
        return stringBuilder.toString();
    }   
}
    
```

※GSONを利用するため，gradleに追記．
```build.gradle
    implementation 'com.google.code.gson:gson:2.8.6'
```


今回は，```initializeAwsClient()```を実行すれば連鎖的にapigatewayのリクエストまで進んでくれるよう記述した．実行して，```ApiResponseModel```が得られていていればOK．


### UserStateとSignInStateについて

- UserState( https://aws-amplify.github.io/aws-sdk-android/docs/reference/com/amazonaws/mobile/client/UserState.html#SIGNED_IN )
    - 上記のソースコードの※1部分でしている．
    - AWSMoblileClientのInitiation時の正常処理系に含まれる値
    - 
恐らく現在のトークンの情報を照合して，ログインが可能な状態か見ている．

    - トークンがタイムアウトしていない場合，もしくはリフレッシュ可能な場合にSIGNED_IN(サインインに必要な情報が揃っている状態)を戻している様子．
    - 
次回の記事で扱うフェデレーショントークンの有効時間もチェックできる．


※【追記】<strong>本記事で扱ったSDKのバージョンでは，コード上で```// TODO enhancement: check if token is expired[AWSMoblieClient:1072]```となっていたため，トークンの生存は見ていない様子．あくまでsharedPreferenceのキャッシュ上にトークンがあるか見ているだけなので，```SIGNED_IN```が戻ってきていてもSigninリクエストはしたほうが良さそう．</strong>

- SignInState ( https://aws-amplify.github.io/aws-sdk-android/docs/reference/com/amazonaws/mobile/client/results/SignInState.html#DONE )
    - 上記のソースコードの※2部分で使用している．
    - AWSMobileClientのサインイン試行時の正常系処理に含まれる値
    - ログイン完了までに必要な手続きを教えてくれる．MFAの設定や，認証に渡す値の暗号化が適切か判断する．
    - ```DONE```になっていれば，ログインの一連の処理が完了している状態．


## エラーハンドリング
```
java.lang.Exception: getTokens does not support retrieving tokens for federated sign-in
```

こちらはほとんどの場合，```awsMobileClient.initialize()```が完了していない状態で```awsMobileClient.getTokens()```を呼び出している．
callbackが多発する状況なので，処理が前後しないように実行してほしい．おそらく上記のコードでは発生しないはず．

<br>

```
com.amazonaws.mobileconnectors.apigateway.ApiClientException: Cognito Identity not configured (Service: null; Status Code: 0; Error Code: null; Request ID: null)
```
こちらはApigateway側の設定に不備がある．私の場合は，設定したオーソライザーがデバック用のオーソライザーでawsconfiguration.jsonで設定した値と異なるオーソライザーを設定していた．

> awsMobileClientを扱うえで，重要な点は，awsconfiguration.jsonの情報とをAWS側の設定を一致させることで，さらにデバックや技術検証目的でもawsconfigration.jsonに使用しない余計な情報を記入しないことである．
例えば，次回の記事で解説する構成情報では，awsconfiguration.jsonにフェデレーションの情報を記入する．このidpool(idフェデレーション情報)が記入されている状態だと，awsMoblileClientも，IDユーザプールの参照と，フェデレーションを一貫して自動的に実行しようと試みてしまう．cognito側でフェデレーションするようidpoolが設定されていなければ，エラーとして処理される．
このコントロールは，(上記のコードであれば，)ライブラリの内側で，awsconfigurationに記述があるかどうかで決定してしまう．なので，awsMobileClientの構成エラーが発生する場合は．awsConfigration.jsonの見直しをおすすめする．

### 参考情報
- [Amazon Cognito ユーザープール API リファレンス](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-userpools-api-reference.html)
- [API ゲートウェイ REST API で、Amazon Cognito ユーザープールをオーソライザーとしてセットアップするにはどうすればよいですか?](https://repost.aws/ja/knowledge-center/api-gateway-cognito-user-pool-authorizer)
- [Amplify gettingstart](https://docs.amplify.aws/android/sdk/auth/getting-started/) (今回はAmplifyは使用していませんが，awsconfiguration.jsonのサンプルがあります．ただし構成がid-federationする前提のため，修正が必要です．)
- [REST API と Amazon Cognito ユーザープールを統合する](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-enable-cognito-user-pool.html)
- [github Issue](https://github.com/aws-amplify/aws-sdk-android/issues/767)(ApifactoryでUserpool認証を使用するには)