AWS IoT + Cognito + IAM + Amplifyでユーザ認証・IoTデバイスと通信するAndroidアプリ

## 目的
IoTデバイス（例えばLEDや各種センサ、それらを接続したRaspberry Piなど）をモバイルアプリから操作したい時、あるユーザが所有しているデバイス以外は操作できないようにデバイスとユーザは一意に紐付けられている必要があります。
上記を実現するためにAWS IoT・Cognito・IAM・Amplifyを使用してユーザ認証機能を実装し、ユーザに紐付けられたデバイスとMQTTメッセージを双方向にやり取りできるAndroidアプリを作成します。

ソースコード→[GitHub](https://github.com/sutoo-the-cat/SignedInPubSub)

## 使用環境

- Windows 10
- Android Studio 4.0.1
- Kotlin 1.4.0
- Amplify CLI 4.27.3
- Pixel 3a（Android 10）

## 前提条件

- AWSアカウント作成済み
- AWS IAMユーザ作成済み
- [Amplify](https://docs.amplify.aws/lib/q/platform/android)導入済み

## AWS IoT
### モノを作成する
ここで作成したモノの名前は、ユーザ名やMQTTクライアントIDとして使用します。
開発者ガイドの下記ページを参考に、AWS IoTコンソールを操作してモノを作成します。

[AWS IoT Coreの開始方法 - AWS IoT](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-gs.html)

例として「testuser」というモノを作成しました。
タイプやグループは設定しなくてもOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/f42b1f30-cb6a-1b50-94f7-291004bf9e9d.png)
なお、本来はここで証明書を作成してモノに割り当て、証明書と秘密鍵をダウンロード、デバイスに保存して使用します。
今回はデバイスの代わりにコンソール内のテストクライアントを使用して動作確認するので説明を省略します。

### ポリシーを作成する 
AWS IoT Coreポリシーをモノに割り当てると、デバイスに対してAWS IoTで実行できる操作を許可・拒否することができます。
例としてポリシー名を「testpolicy」とし、「ステートメントを追加」欄でアドバンストモードにして下記を入力します。
下記のポリシーは認証情報が割り当てられたデバイスだけが「デバイス名/*」トピックにアクセスすることを許可します。
*はワイルドカードなので、例えばモノの名前が「testuser」の場合「testuser/to」にも「testuser/from」にもアクセスできます。
ポリシーの書き方は下記ページを参照してください。

[AWS IoT Core ポリシー変数 - AWS IoT](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-policy-variables.html)

\<your-region\>、\<your-aws-account\>は自分の環境に合わせて変更してください。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect"
      ],
      "Resource": [
        "*"
      ],
      "Condition": {
        "Bool": {
          "iot:Connection.Thing.IsAttached": [
            "true"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "iot:Subscribe",
      "Resource": [
        "arn:aws:iot:<your-region>:<your-aws-account>:topicfilter/${iot:Connection.Thing.ThingName}/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Publish",
        "iot:Receive"
      ],
      "Resource": [
        "arn:aws:iot:<your-region>:<your-aws-account>:topic/${iot:Connection.Thing.ThingName}/*"
      ]
    }
  ]
}
```

### AWS IoT エンドポイント
AWS IoTコンソールの左メニューから「設定」を開き、エンドポイントをメモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/2f47a0fa-68c3-4fd3-fe2f-b34705a881de.png)


## プロジェクトの作成
Android Studioでプロジェクトを作成します。
例としてプロジェクト名は「SignedInPubSub」、パッケージ名は「com.example.signedinpubsub」とします。
テンプレートは「Empty Activity」にしました（あとで適宜Viewを追加します）。
プロジェクトの設定はAmplifyを設定した後で行いますので、一旦ここまでで保存しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/eac4725b-3a56-742c-1ef1-7fe53b2dc4a8.png)

## AWS Amplify
コマンドプロンプトなどで先ほど作成したプロジェクトのルートディレクトリまで移動し、下記コマンドを実行します。

```
>amplify init
```

いくつか質問されますので答えていくとルートディレクトリに開発環境が追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/f3891436-2dfe-167a-eeea-625531cbfb33.png)
続いて、下記コマンドを実行します。

```
>amplify add auth
```

同様に質問に答えます。
domain name prefixはデフォルト値でOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/b8a1fe80-20f7-81b6-e232-1d0706f19329.png)
下記コマンドを実行して設定した環境をアップロードします。

```
>amplify push
```

ブラウザからAWS Amplifyコンソールを開き、作成したアプリ（SignedInPubSub）を選択し、
「Backend environments」->「Authentication」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/44a29372-7a3f-4290-3c80-e52213036a31.png)
「View in Cognito」をクリックしてCognitoユーザープールへ移動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/0fb07dc7-e191-e2dd-bd8c-ed497d30e108.png)

## AWS Cognito
### ユーザープール
ユーザープールIDをメモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/01ecc37d-8ab8-7216-ea92-85a889a7d709.png)
左メニューから「全般設定」->「ポリシー」をクリックし、下図の通り設定して変更を保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/04103a5a-5b4d-f93c-39ef-84ee3d95dce5.png)
左メニューから「全般設定」->「ユーザーとグループ」->「ユーザーの作成」をクリックします。
ユーザ名はAWS IoTで作成したモノの名前（ここではtestuser）と一致させます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/192a39b4-26ca-c051-7a9c-0600c4ac2bbf.png)
左メニューから「全般設定」->「アプリクライアント」をクリックし、末尾に「_app_client」が付いているものを削除します。
また、末尾に「_app_clientWeb」が付いているアプリクライアントIDをメモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/4fb00a7c-91d7-dd0d-618c-ad511ef075b7.png)
「詳細を表示」をクリックし、下記項目を有効にして変更を保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/e25c0365-d743-ca32-8107-f7d8c196fe2d.png)
左メニューから「アプリの統合」->「アプリクライアントの設定」をクリックし、下図の通り設定します。
Amplifyで入力した仮のサインイン/サインアウトURIが入っていますのでそれぞれ「https」を「com.example.signedinpubsub」へ書き換えます。
URLの先頭部分（URLスキーム）はWebブラウザを使ってサインインした後でアプリに戻ってくるために必要になるので、アプリ固有のものを指定します。
ちなみに今回はURLスキームとしてAndroidアプリのパッケージ名を指定しましたが、一般的なもの以外であれば何でもOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/5a5142ff-92c1-99a3-0d0c-71ee5ab52b55.png)
左メニューから「アプリの統合」->「ドメイン名」をクリックし、ドメインのプレフィックスをメモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/56bc1f6a-185a-88f6-fb8c-4bc69486f02d.png)

### IDプール
上メニューから「フェデレーティッドアイデンティティ」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/d994da83-e28a-313c-db7a-0ffb377843e3.png)
Amplifyが自動的にIDプールを作成していますのでクリックします。
このページの通りに作っている場合は、「signedinpubsubccd\*\*\*\*\*\_identitypool\_ccd\*\*\*\*\*\_\_dev」のような名前になっているはずです。
次に右上にある「IDプールの編集」をクリックします。
IDプールのIDをメモしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/686a9a9a-1e8a-cf3e-60f2-cd635ba9983c.png)
「認証フローの設定」を開き、メッセージをクリックします。
また、先ほどユーザープールで削除したアプリクライアントと同じIDが設定されている認証プロバイダを削除します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/2db04927-cf2a-9957-09b9-3cada6d9c3b5.png)

## AWS IAM
AWS IAMコンソールに移動します。
左メニューから「アクセス管理」->「ロール」をクリックします。
Amplifyが作成したロールが表示されているので、authRole、unauthRoleそれぞれにIAMポリシーを割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/a5f6674d-f841-91e8-5d61-538fe458f1f5.png)
まずはunauthRoleをクリックし、「アクセス制限」->「ポリシーをアタッチします」をクリックします。
検索欄に「deny」と入れ、「AWSDenyAll」にチェックを入れて「ポリシーのアタッチ」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/d84343c7-529e-4501-667d-5a00b9755042.png)
続いてauthRoleも同様に、検索欄に「IoTFull」と入れて「AWSIoTFullAccess」にチェックを入れて「ポリシーのアタッチ」をクリックします。
これで認証済みのユーザはAWS IoTでのあらゆる操作が可能になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/bfa8baed-7b2d-515f-7fb1-61c2fd893050.png)
今回はすでに用意されているポリシーを割り当てましたが、実際の運用では必要な操作のみ許可するべきです。
詳しくは下記ページを参照してください。

[IAM ロール - AWS Identity and Access Management](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles.html)

## Androidアプリの作成
再びAndroid Studioでの作業です。
下記ページを参考にAndroidアプリの設定を行います。

[Project Setup - Create your application - Amplify Docs](https://docs.amplify.aws/lib/project-setup/create-application/q/platform/android)

### build.gradle
build.gradle(Project:SignedInPubSub)を開き、repositioriesにmavenCentral()を追記します。

```gradle:build.gradle(Project：SignedInPubSub)
buildscript {
    repositories {
        ...
        mavenCentral()
    }
}

allprojects {
    ...
    repositories {
        ...
        mavenCentral()
    }
    ...
}
```

続いてbuild.gradle(Module:app)を開き、compileOptionsとdependenciesを追記します。

```gradle:build.gradle(Module：app)

android {
    ...
    compileOptions {
        // Support for Java 8 features
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    ...
    implementation 'com.amazonaws:aws-android-sdk-iot:2.17.1'
    implementation 'com.amplifyframework:core:1.1.2'
    implementation 'com.amplifyframework:aws-auth-cognito:1.1.2'
    implementation 'com.auth0.android:jwtdecode:2.0.0'
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
    ...
}

```

エディタ上側に下図の表示が出ます。右にある「Sync Now」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/d15f7bb3-7aa8-9f3e-4b18-5ab06ee89ec7.png)

### amplifyconfiguration.json
Amplifyが自動的に作成しています。デフォルトだと「プロジェクトのルートディレクトリ\app\src\main\res\raw」にあります。
下記のように修正します。
<>で囲まれた部分は上記でメモしておいたIDなどに変更します。

```json:amplifyconfiguration.json
{
    "UserAgent": "aws-amplify-cli/2.0",
    "Version": "1.0",
    "auth": {
        "plugins": {
            "awsCognitoAuthPlugin": {
                "UserAgent": "aws-amplify-cli/0.1.0",
                "Version": "0.1.0",
                "IdentityManager": {
                    "Default": {}
                },
                "CredentialsProvider": {
                    "CognitoIdentity": {
                        "Default": {
                            "PoolId": "<your-identify-pool-id>",
                            "Region": "<your-region>"
                        }
                    }
                },
                "CognitoUserPool": {
                    "Default": {
                        "PoolId": "<your-user-pool-id>",
                        "AppClientId": "<your-app-client-id>",
                        "Region": "<your-region>"
                    }
                },
                "Auth": {
                    "Default": {
                        "OAuth": {
                            "WebDomain": "<your-domain-prefix>.auth.<your-region>.amazoncognito.com",
                            "AppClientId": "<your-app-client-id>",
                            "SignInRedirectURI": "com.example.signedinpubsub://signin/",
                            "SignOutRedirectURI": "com.example.signedinpubsub://signout/",
                            "Scopes": [
                                "openid",
                            ]
                        },
                        "authenticationFlowType": "USER_SRP_AUTH"
                    }
                }
            }
        }
    }
}
```

### awsconfiguration.json
amplifyconfiguration.jsonと同様に修正します。

```json:awsconfiguration.json
{
    "UserAgent": "aws-amplify-cli/0.1.0",
    "Version": "0.1.0",
    "IdentityManager": {
        "Default": {}
    },
    "CredentialsProvider": {
        "CognitoIdentity": {
            "Default": {
                "PoolId": "<your-identify-pool-id>",
                "Region": "<your-region>"
            }
        }
    },
    "CognitoUserPool": {
        "Default": {
            "PoolId": "<your-user-pool-id>",
            "AppClientId": "<your-app-client-id>",
            "Region": "<your-region>"
        }
    },
    "Auth": {
        "Default": {
            "OAuth": {
                "WebDomain": "<your-domain-prefix>.auth.<your-region>.amazoncognito.com",
                "AppClientId": "<your-app-client-id>",
                "SignInRedirectURI": "com.example.signedinpubsub://signin/",
                "SignOutRedirectURI": "com.example.signedinpubsub://signout/",
                "Scopes": [
                    "openid",
                ]
            },
            "authenticationFlowType": "USER_SRP_AUTH"
        }
    }
}
```

### strings.xml
TextView等に表示する文字を定義します。

```xml:strings.xml
<resources>
    <string name="app_name">SignedInPubSub</string>
    <string name="text_connecting">Connecting…</string>
    <string name="text_reconnecting">Reconnecting…</string>
    <string name="text_connected">Connected.</string>
    <string name="text_disconnected">Disconnected.</string>
    <string name="text_connect">Connect</string>
    <string name="text_publish">Publish</string>
    <string name="text_subscribe">Subscribe</string>
    <string name="text_disconnect">Disconnect</string>

</resources>
```

### activity_main.xml
画面のレイアウトを定義します。

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/buttonConnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="28dp"
        android:enabled="false"
        android:text="@string/text_connect"
        app:layout_constraintEnd_toStartOf="@+id/buttonDisconnect"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/buttonDisconnect"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:enabled="false"
        android:text="@string/text_disconnect"
        app:layout_constraintEnd_toStartOf="@+id/buttonPublish"
        app:layout_constraintStart_toEndOf="@+id/buttonConnect"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/buttonPublish"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:enabled="false"
        android:text="@string/text_publish"
        app:layout_constraintEnd_toStartOf="@+id/buttonSubscribe"
        app:layout_constraintStart_toEndOf="@+id/buttonDisconnect"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/buttonSubscribe"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="28dp"
        android:layout_marginEnd="16dp"
        android:enabled="false"
        android:text="@string/text_subscribe"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/buttonPublish"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textViewStatus"
        android:layout_width="345dp"
        android:layout_height="40dp"
        android:layout_marginStart="32dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="32dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/buttonConnect" />

    <TextView
        android:id="@+id/textViewMessage"
        android:layout_width="345dp"
        android:layout_height="540dp"
        android:layout_marginStart="32dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="32dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textViewStatus" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

### AndroidManifest.xml

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.signedinpubsub">
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:name=".Initialization"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity"
            android:launchMode="singleInstance">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="com.example.signedinpubsub" />
            </intent-filter>
        </activity>
    </application>

</manifest>

```

### Constants.kt
プログラム内で使用する定数を記述するクラス「Constants」を作成します。
<>で囲まれた部分は、上記でメモしておいたIDなどに変更します。

```kotlin:Constants.kt
package com.example.signedinpubsub

class Constants {
    companion object{
        const val LOG_TAG = "SignedInPubSub"
        const val AUTH_LOG_TAG = "AuthQuickStart"
        const val CALLBACK_SCHEME = "com.example.signedinpubsub"
        const val AWS_IOT_ENDPOINT = "<your-iot-endpoint>"
        const val AWS_IOT_POLICY = "testpolicy"
        const val AWS_COGNITO_USER_POOL_ID = "cognito-idp.<your-region>.amazonaws.com/<your-user-pool-id>"
        const val AWS_COGNITO_POOL_ID = "<your-identify-pool-id>"
        val REGION = Regions.AP_NORTHEAST_1 // Change to your region
    }
}
```

### Initialization.kt
アプリ起動時にAmplifyプラグインを初期化するクラス「Initialization」を作成します。

```kotlin:Initialization.kt
package com.example.signedinpubsub

import android.app.Application
import android.util.Log
import com.amplifyframework.AmplifyException
import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
import com.amplifyframework.core.Amplify

class Initialization: Application() {
    override fun onCreate() {
        super.onCreate()

        // Amplifyの初期化（アプリ起動時に1回だけ実施）
        try {
            Amplify.addPlugin(AWSCognitoAuthPlugin())
            Amplify.configure(applicationContext)
            Log.i(Constants.LOG_TAG, "Initialized Amplify")
        } catch (error: AmplifyException) {
            Log.e(Constants.LOG_TAG, "Could not initialize Amplify", error)
        }
    }
}
```

### MainActivity.kt
メイン画面での処理です。

```kotlin:MainActivity.kt
package com.example.signedinpubsub

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.amazonaws.auth.CognitoCachingCredentialsProvider
import com.amazonaws.mobileconnectors.iot.AWSIotMqttClientStatusCallback
import com.amazonaws.mobileconnectors.iot.AWSIotMqttManager
import com.amazonaws.mobileconnectors.iot.AWSIotMqttQos
import com.amazonaws.regions.Region
import com.amazonaws.services.cognitoidentity.AmazonCognitoIdentityClient
import com.amazonaws.services.cognitoidentity.model.GetIdRequest
import com.amazonaws.services.iot.AWSIotClient
import com.amazonaws.services.iot.model.*
import com.amplifyframework.auth.AuthSession
import com.amplifyframework.auth.cognito.AWSCognitoAuthSession
import com.amplifyframework.auth.result.AuthSessionResult
import com.amplifyframework.core.Amplify
import com.auth0.android.jwt.DecodeException
import com.auth0.android.jwt.JWT
import java.io.UnsupportedEncodingException
import kotlin.collections.HashMap


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ユーザ認証の有効期限切れまたは未認証の場合はWebブラウザを開いて認証
        Amplify.Auth.signInWithWebUI(
            this,
            { result ->
                // サインイン成功
                Log.i(Constants.AUTH_LOG_TAG, result.toString())
                fetchSession()
            },
            { error ->
                // サインイン失敗
                Log.i(Constants.AUTH_LOG_TAG, error.toString())
            }
        )
    }

    // Webブラウザで認証した後戻ってきた時の処理
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)

        if(intent?.scheme != null && Constants.CALLBACK_SCHEME == intent.scheme) {
            Amplify.Auth.handleWebUISignInResponse(intent)
        }
    }


    // セッションを取得
    private fun fetchSession() {
        Amplify.Auth.fetchAuthSession(
            { session ->
                // ユーザ名とユーザIDトークンのペアを取得
                val pair = getUserNameAndIdToken(session)

                if(pair != null){
                    val thingName = pair.first
                    val idToken = pair.second

                    // 認証情報プロバイダの初期化
                    val credentialsProvider = CognitoCachingCredentialsProvider(
                        applicationContext,
                        Constants.AWS_COGNITO_POOL_ID,
                        Constants.REGION
                    )

                    // 認証情報プロバイダにユーザプールIDと認証済みユーザをマッピング
                    val logins: MutableMap<String, String> = HashMap()
                    logins[Constants.AWS_COGNITO_USER_POOL_ID] = idToken
                    credentialsProvider.logins = logins

                    Thread {
                        // AWS IoTクライアントの作成
                        val client = AWSIotClient(credentialsProvider)
                        client.setRegion(Region.getRegion(Constants.REGION))

                        // モノが作成されているか確認
                        val listThingsReq = ListThingsRequest()
                        val listThings = client.listThings(listThingsReq)
                        var hasAdded = false
                        for (i in listThings.things) {
                            if (i.thingName == thingName) {
                                hasAdded = true
                            }
                        }

                        // モノが作成されていない場合はユーザ名で作成する
                        if (!hasAdded) {
                            try{
                                val thingReq = CreateThingRequest()
                                thingReq.thingName = thingName
                                client.createThing(thingReq)
                            }catch(error: Exception){
                                Log.e(Constants.LOG_TAG, "Could not create the thing.", error)
                            }
                        }

                        // Cognito IDの取得
                        val getIdReq = GetIdRequest()
                        getIdReq.logins = credentialsProvider.logins
                        getIdReq.identityPoolId = Constants.AWS_COGNITO_POOL_ID
                        val cognitoIdentity = AmazonCognitoIdentityClient(credentialsProvider)
                        cognitoIdentity.setRegion(Region.getRegion(Constants.REGION))
                        val getIdRes = cognitoIdentity.getId(getIdReq)

                        // プリンシパルがモノに割り当てられているか確認
                        val listPrincipalThingsReq = ListPrincipalThingsRequest()
                        listPrincipalThingsReq.principal = getIdRes.identityId
                        val listPrincipalThings = client.listPrincipalThings(listPrincipalThingsReq)
                        var hasAttached = false
                        for (i in listPrincipalThings.things) {
                            if (i == thingName) {
                                hasAttached = true
                            }
                        }

                        // プリンシパルが割り当てられていない場合はモノにプリンシパルを割り当てる
                        if (!hasAttached){
                            try{
                                val policyReq = AttachPolicyRequest()
                                policyReq.policyName = Constants.AWS_IOT_POLICY // AWS IoTにアクセスするためのポリシー
                                policyReq.target = getIdRes.identityId // Cognito ID
                                client.attachPolicy(policyReq) // ポリシーをCognito IDに割り当てる

                                val principalReq = AttachThingPrincipalRequest()
                                principalReq.principal = getIdRes.identityId // プリンシパル（Cognito ID）
                                principalReq.thingName = thingName // モノの名前
                                client.attachThingPrincipal(principalReq) // プリンシパルをモノに割り当てる
                            }catch(error: Exception){
                                Log.e(Constants.LOG_TAG, "Could not attach the principal to the thing.", error)
                            }
                        }

                        // MQTTクライアントを作成
                        val mqttManager = AWSIotMqttManager(thingName, Constants.AWS_IOT_ENDPOINT)
                        mqttManager.keepAlive = 10

                        val buttonConnect: Button = findViewById(R.id.buttonConnect)
                        val buttonDisconnect: Button = findViewById(R.id.buttonDisconnect)
                        val buttonSubscribe: Button = findViewById(R.id.buttonSubscribe)
                        val buttonPublish: Button = findViewById(R.id.buttonPublish)
                        val textViewStatus: TextView = findViewById(R.id.textViewStatus)
                        val textViewMessage: TextView = findViewById(R.id.textViewMessage)

                        runOnUiThread {
                            buttonConnect.isEnabled = true // 接続可能になった時、Connectボタンを有効にする

                            // Connectボタンがクリックされた時の処理
                            buttonConnect.setOnClickListener {
                                buttonConnect.isEnabled = false
                                try {
                                    mqttManager.connect(credentialsProvider) { status, throwable ->
                                        Log.d(Constants.LOG_TAG, "Status = $status")

                                        runOnUiThread {
                                            when (status) {
                                                AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Connecting -> {
                                                    textViewStatus.text =
                                                        getString(R.string.text_connecting)
                                                }
                                                AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Connected -> {
                                                    textViewStatus.text =
                                                        getString(R.string.text_connected)
                                                    buttonDisconnect.isEnabled = true
                                                    buttonPublish.isEnabled = true
                                                    buttonSubscribe.isEnabled = true
                                                }
                                                AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.Reconnecting -> {
                                                    if (throwable != null) {
                                                        Log.e(Constants.LOG_TAG, "Connection error.", throwable)
                                                    }
                                                    textViewStatus.text =
                                                        getString(R.string.text_reconnecting)
                                                }
                                                AWSIotMqttClientStatusCallback.AWSIotMqttClientStatus.ConnectionLost -> {
                                                    if (throwable != null) {
                                                        Log.e(Constants.LOG_TAG, "Connection error.", throwable)
                                                    }
                                                    textViewStatus.text =
                                                        getString(R.string.text_disconnected)
                                                    buttonConnect.isEnabled = true
                                                }
                                                else -> {
                                                    textViewStatus.text =
                                                        getString(R.string.text_disconnected)
                                                    buttonConnect.isEnabled = true
                                                }
                                            }
                                        }
                                    }
                                } catch (error: Exception) {
                                    Log.e(Constants.LOG_TAG, "Subscription error.", error)
                                }
                            }

                            // Disconnectボタンがクリックされた時の処理
                            buttonDisconnect.setOnClickListener {
                                buttonDisconnect.isEnabled = false
                                buttonPublish.isEnabled = false
                                buttonSubscribe.isEnabled = false
                                buttonConnect.isEnabled = true
                                try {
                                    mqttManager.disconnect()
                                } catch (error: Exception) {
                                    Log.e(Constants.LOG_TAG, "Disconnect error.", error)
                                }
                            }

                            // Publishボタンがクリックされた時の処理
                            buttonPublish.setOnClickListener {
                                try {
                                    mqttManager.publishString("{\"message\":\"Test.\"}", "$thingName/to", AWSIotMqttQos.QOS1)
                                } catch (error: Exception) {
                                    Log.e(Constants.LOG_TAG, "Publish error.", error)
                                }
                            }

                            // Subscribeボタンがクリックされた時の処理
                            buttonSubscribe.setOnClickListener {

                                buttonSubscribe.isEnabled = false

                                try {
                                    mqttManager.subscribeToTopic(
                                        "$thingName/from", AWSIotMqttQos.QOS1
                                    ) { topic, data ->
                                        runOnUiThread { // トピックにメッセージが発行された時のみ実行
                                            try {
                                                val message = String(data)
                                                textViewMessage.text = message

                                            } catch (error: UnsupportedEncodingException) {
                                                Log.e(Constants.LOG_TAG, "Message encoding error.", error)
                                            }
                                        }
                                    }
                                } catch (error: Exception) {
                                    Log.e(Constants.LOG_TAG, "Subscription error.", error)
                                }
                            }
                        }
                    }.start()
                }
            },
            { error -> Log.e(Constants.AUTH_LOG_TAG, error.toString()) }
        )
    }

    // ユーザ名とユーザIDトークンの取得
    private fun getUserNameAndIdToken(session: AuthSession): Pair<String, String>? {
        val cognitoAuthSession = session as AWSCognitoAuthSession

        // セッション取得
        when (cognitoAuthSession.identityId.type) {
            // セッション取得が成功した場合はユーザ名とユーザIDトークンのペアを返す
            AuthSessionResult.Type.SUCCESS -> {
                val tokens = cognitoAuthSession.userPoolTokens.value
                if(tokens != null){
                    val idToken = tokens.idToken // ユーザIDトークン
                    try{
                        val jwt = JWT(idToken)
                        val token = jwt.toString()
                        val name = jwt.getClaim("cognito:username").asString()
                        if(name != null) return Pair(name, token)
                    }catch(error: DecodeException){
                        Log.e(Constants.LOG_TAG, "Could not decode tokens.", error)
                    }
                }
            }

            // セッション取得に失敗した場合はnullを返す
            AuthSessionResult.Type.FAILURE -> {
                Log.i(Constants.AUTH_LOG_TAG, "IdentityId not present because: " + cognitoAuthSession.identityId.error.toString())
            }
        }

        return null
    }
}
```

## 動作確認
AWS IoTコンソールの左メニューから「テスト」をクリックします。
トピックのサブスクリプション欄に「testuser/to」と入力し、「トピックへのサブスクライブ」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/65564b71-8412-24f2-64a4-86881108477e.png)
アプリを起動するとWebブラウザが開きサインインを要求されるので、あらかじめ作成しておいたユーザ名「testuser」でサインインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/e3d47e38-1d2e-8f6f-3a69-bf4137ecced5.png)
ユーザ認証に成功するとConnectボタンが押せるようになります（少し時間がかかります）。
Connectボタンを押すとMQTTクライアントへ接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/17e71c96-0bcd-9a9b-9d79-db1b451fc679.png)
MQTTクライアントへの接続が成功すると、Disconnectボタン、Publishボタン、Subscribeボタンが有効になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/d40d1a2c-6a27-c8cb-00d8-a8ecb67afe00.png)
Publishボタンを押すとトピック「testuser/to」へメッセージが送られ、AWS IoTコンソール上にメッセージが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/9083b7b8-5194-6d20-aa06-ed3bb5e80bb5.png)
Subscribeボタンを押すとトピック「testuser/from」からのメッセージ待ち状態になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/e996e2d5-2cbe-a5e2-60c0-a94c7d94f187.png)
トピックを「testuser/from」に書き換えて、「トピックに発行」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/e328ed26-f31a-9795-c11b-d22918637b2f.png)
アプリにメッセージが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305999/d6364504-b4f0-1683-03ca-9fe3da935560.png)

Amplifyが内部で大部分の処理をやってくれているので、ほとんどドキュメント通りに進めていくことでサインイン機能を実装できました。

## 参考にしたWebサイト
- [Amplify Libraries - Amplify Docs](https://docs.amplify.aws/lib/q/platform/android)
- [AWS IoT 開発者ガイド](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/what-is-aws-iot.html)
- [Amazon Cognito 開発者ガイド](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/what-is-amazon-cognito.html)
- [IAM ユーザーガイド](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/introduction.html)
- [AWSアカウントを取得したら速攻でやっておくべき初期設定まとめ](https://qiita.com/tmknom/items/303db2d1d928db720888)
- [java - Amazon Cognito connection cannot find IdentityPool - Stack Overflow](https://stackoverflow.com/questions/27137299/amazon-cognito-connection-cannot-find-identitypool)
- [[AWS IoT] ポリシー変数で複数のモノをセキュアに管理してみた | Developers.IO](https://dev.classmethod.jp/articles/aws-iot-core-policy/)