目的
IoTデバイス(例えばLEDや各種センサ、それらを接続したRaspberry Piなど)をモバイルアプリから操作したい時、あるユーザが所有しているデバイス以外は操作できないようにデバイスとユーザは一意に紐付けられている必要があります。
上記を実現するためにAWS IoT・Cognito・IAM・Amplifyを使用してユーザ認証機能を実装し、ユーザに紐付けられたデバイスとMQTTメッセージを双方向にやり取りできるAndroidアプリを作成します。
ソースコード→GitHub
使用環境
- Windows 10
- Android Studio 4.0.1
- Kotlin 1.4.0
- Amplify CLI 4.27.3
- Pixel 3a(Android 10)
前提条件
- AWSアカウント作成済み
- AWS IAMユーザ作成済み
- Amplify導入済み
AWS IoT
モノを作成する
ここで作成したモノの名前は、ユーザ名やMQTTクライアントIDとして使用します。
開発者ガイドの下記ページを参考に、AWS IoTコンソールを操作してモノを作成します。
例として「testuser」というモノを作成しました。
タイプやグループは設定しなくてもOKです。
なお、本来はここで証明書を作成してモノに割り当て、証明書と秘密鍵をダウンロード、デバイスに保存して使用します。
今回はデバイスの代わりにコンソール内のテストクライアントを使用して動作確認するので説明を省略します。
ポリシーを作成する
AWS IoT Coreポリシーをモノに割り当てると、デバイスに対してAWS IoTで実行できる操作を許可・拒否することができます。
例としてポリシー名を「testpolicy」とし、「ステートメントを追加」欄でアドバンストモードにして下記を入力します。
下記のポリシーは認証情報が割り当てられたデバイスだけが「デバイス名/*」トピックにアクセスすることを許可します。
*はワイルドカードなので、例えばモノの名前が「testuser」の場合「testuser/to」にも「testuser/from」にもアクセスできます。
ポリシーの書き方は下記ページを参照してください。
<your-region>、<your-aws-account>は自分の環境に合わせて変更してください。
{
"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コンソールの左メニューから「設定」を開き、エンドポイントをメモしておきます。
プロジェクトの作成
Android Studioでプロジェクトを作成します。
例としてプロジェクト名は「SignedInPubSub」、パッケージ名は「com.example.signedinpubsub」とします。
テンプレートは「Empty Activity」にしました(あとで適宜Viewを追加します)。
プロジェクトの設定はAmplifyを設定した後で行いますので、一旦ここまでで保存しておきます。
AWS Amplify
コマンドプロンプトなどで先ほど作成したプロジェクトのルートディレクトリまで移動し、下記コマンドを実行します。
>amplify init
いくつか質問されますので答えていくとルートディレクトリに開発環境が追加されます。
続いて、下記コマンドを実行します。
>amplify add auth
同様に質問に答えます。
domain name prefixはデフォルト値でOKです。
下記コマンドを実行して設定した環境をアップロードします。
>amplify push
ブラウザからAWS Amplifyコンソールを開き、作成したアプリ(SignedInPubSub)を選択し、
「Backend environments」->「Authentication」をクリックします。
「View in Cognito」をクリックしてCognitoユーザープールへ移動します。
AWS Cognito
ユーザープール
ユーザープールIDをメモしておきます。
左メニューから「全般設定」->「ポリシー」をクリックし、下図の通り設定して変更を保存します。
左メニューから「全般設定」->「ユーザーとグループ」->「ユーザーの作成」をクリックします。
ユーザ名はAWS IoTで作成したモノの名前(ここではtestuser)と一致させます。
左メニューから「全般設定」->「アプリクライアント」をクリックし、末尾に「_app_client」が付いているものを削除します。
また、末尾に「_app_clientWeb」が付いているアプリクライアントIDをメモしておきます。
「詳細を表示」をクリックし、下記項目を有効にして変更を保存します。
左メニューから「アプリの統合」->「アプリクライアントの設定」をクリックし、下図の通り設定します。
Amplifyで入力した仮のサインイン/サインアウトURIが入っていますのでそれぞれ「https」を「com.example.signedinpubsub」へ書き換えます。
URLの先頭部分(URLスキーム)はWebブラウザを使ってサインインした後でアプリに戻ってくるために必要になるので、アプリ固有のものを指定します。
ちなみに今回はURLスキームとしてAndroidアプリのパッケージ名を指定しましたが、一般的なもの以外であれば何でもOKです。
左メニューから「アプリの統合」->「ドメイン名」をクリックし、ドメインのプレフィックスをメモしておきます。
IDプール
上メニューから「フェデレーティッドアイデンティティ」をクリックします。
Amplifyが自動的にIDプールを作成していますのでクリックします。
このページの通りに作っている場合は、「signedinpubsubccd*****_identitypool_ccd*****__dev」のような名前になっているはずです。
次に右上にある「IDプールの編集」をクリックします。
IDプールのIDをメモしておきます。
「認証フローの設定」を開き、メッセージをクリックします。
また、先ほどユーザープールで削除したアプリクライアントと同じIDが設定されている認証プロバイダを削除します。
AWS IAM
AWS IAMコンソールに移動します。
左メニューから「アクセス管理」->「ロール」をクリックします。
Amplifyが作成したロールが表示されているので、authRole、unauthRoleそれぞれにIAMポリシーを割り当てます。
まずはunauthRoleをクリックし、「アクセス制限」->「ポリシーをアタッチします」をクリックします。
検索欄に「deny」と入れ、「AWSDenyAll」にチェックを入れて「ポリシーのアタッチ」をクリックします。
続いてauthRoleも同様に、検索欄に「IoTFull」と入れて「AWSIoTFullAccess」にチェックを入れて「ポリシーのアタッチ」をクリックします。
これで認証済みのユーザはAWS IoTでのあらゆる操作が可能になります。
今回はすでに用意されているポリシーを割り当てましたが、実際の運用では必要な操作のみ許可するべきです。
詳しくは下記ページを参照してください。
IAM ロール - AWS Identity and Access Management
Androidアプリの作成
再びAndroid Studioでの作業です。
下記ページを参考にAndroidアプリの設定を行います。
Project Setup - Create your application - Amplify Docs
build.gradle
build.gradle(Project:SignedInPubSub)を開き、repositioriesにmavenCentral()を追記します。
buildscript {
repositories {
...
mavenCentral()
}
}
allprojects {
...
repositories {
...
mavenCentral()
}
...
}
続いてbuild.gradle(Module:app)を開き、compileOptionsとdependenciesを追記します。
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」をクリックします。
amplifyconfiguration.json
Amplifyが自動的に作成しています。デフォルトだと「プロジェクトのルートディレクトリ\app\src\main\res\raw」にあります。
下記のように修正します。
<>で囲まれた部分は上記でメモしておいたIDなどに変更します。
{
"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と同様に修正します。
{
"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等に表示する文字を定義します。
<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 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 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などに変更します。
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」を作成します。
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
メイン画面での処理です。
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」と入力し、「トピックへのサブスクライブ」をクリックします。
アプリを起動するとWebブラウザが開きサインインを要求されるので、あらかじめ作成しておいたユーザ名「testuser」でサインインします。
ユーザ認証に成功するとConnectボタンが押せるようになります(少し時間がかかります)。
Connectボタンを押すとMQTTクライアントへ接続します。
MQTTクライアントへの接続が成功すると、Disconnectボタン、Publishボタン、Subscribeボタンが有効になります。
Publishボタンを押すとトピック「testuser/to」へメッセージが送られ、AWS IoTコンソール上にメッセージが表示されます。
Subscribeボタンを押すとトピック「testuser/from」からのメッセージ待ち状態になります。
トピックを「testuser/from」に書き換えて、「トピックに発行」をクリックします。
アプリにメッセージが表示されます。
Amplifyが内部で大部分の処理をやってくれているので、ほとんどドキュメント通りに進めていくことでサインイン機能を実装できました。