AWS Cognitoを使ってAndroidでサインアップ・サインインアプリを作ってみた

## はじめに

商用のモバイルアプリではアカウント認証が基本的な機能となっています。そこでクラウドサービスを利用した簡単なサインインアプリを作ってみました。今回はユーザの認証のみを行うためにAmazon Cognitoを利用しました。

## Cognitoについて

Cognitoはモバイルやウェブのユーザ認証やアクセス制御を簡単に実行できるサービスです。主に次の機能があります。
- **ユーザプール**
アカウントの作成・ログイン機能を提供し、ユーザの属性（氏名、メールアドレスなど）の管理を行うことが可能。
- **IDプール**
ユーザに一時的なAWS認証情報を提供。

今回はユーザプールを利用して、Androidアプリからユーザの作成とサインインができるようにしていきます。

## クラウドの設定

まず、ユーザプールを作っていきます。
### ユーザプールの作成
1. **サインエクスペリエンスを設定**
まず、プロバイダーのタイプとサインインオプションを選択します。GoogleやAppleなどの3rd partyのアカウント認証を行うにフェデレーテッドも使う必要がありますが、今回は最小限の構成にするため、Cognitoのユーザプールのみを使用します。サインインオプションもユーザ名にします。<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/be22f3c3-2a07-8aef-182f-61a0f6aa65a0.png" width=500>

1. **セキュリティ要件を設定**
次にセキュリティ要件ですが、ここでは特にリッチな機能は不要なので、MFAやユーザアカウント復旧などは無効にしておきます。<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/03770b62-68ef-4c9b-0679-4e5ef2b45a35.png" width=500>

1. **サインアップエクスペリエンスを設定**
次はサインアップに関する設定です。自己登録を有効にします。また、必須の属性に"given_name"だけを追加しておきました（なんとなくです）。必須の属性ではユーザの氏名や住所、メールアドレスなどを追加できます。<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/fdaf137b-0d5a-4806-a82d-16d667232eec.png" width=500>

1. **メッセージ配信を設定**
ここら辺の機能は今回のアプリでは使わないので適当に設定します。

1. **アプリケーションを統合**
ここではユーザプール名などの設定をしていきます。
![スクリーンショット 2024-08-24 231758.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/30b93fbe-daab-4a0f-ac6f-cad1422dec72.png)

これでCognitoのユーザプールの作成は完了します。作られると以下のようにユーザプール一覧に新しいユーザプール追加されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/ef049405-4f9f-8a65-bfab-e0ea89e5624e.png" width=400>
最後にユーザプールID、クライアントID、リージョンを確認しておきます。この情報はAndroidの実装のほうに必要となります。クライアントIDは**アプリケーションの統合**の欄から確認できます。

## Androidの実装
次にAndroidアプリ側の実装を行っていきます。
### 準備
まずは、AWS SDKをapp下のGradle依存関係に追加します。
```Kotlin
dependencies {
    // AWS Cognito Identity Provider
    implementation("com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.77.0")
}
```
また、必要なパーミッションを`AndroidManifest.xml`に追加します。
```xml
<uses-permission android:name="android.permission.INTERNET" />
```
### UI作成
サインインとサインアップを行うための簡単なUIを作成します。
|サインイン画面|サインアップ画面|
|-|-|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/bdd1525e-9b8c-0c33-7c86-ce4c4f69c3d9.png" width=300>|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/c1113231-ec37-4b26-26af-496c3c13bd88.png" width=300>|

### Repository作成
次にクラウドと接続する部分を作ります。ChatGPTに聞いたらほぼ正解のコードを作ってくれたので少し修正して使っています。日常の業務でもそうですが、こういう時に本当に便利です。
```Kotlin
class CognitoAuthRepositoryImp(context: Context): AuthRepository {
    // Cognitoのユーザプールの設定
    private val userPoolId = ""
    private val clientId = ""
    private val clientSecret = null
    private val region = 
    private val userPool: CognitoUserPool =
        CognitoUserPool(context, userPoolId, clientId, clientSecret, region)

    override fun signUp(username: String, givenName: String, password: String, callback: (Result<Boolean>) -> Unit) {
        val userAttributes = CognitoUserAttributes()
        userAttributes.addAttribute("given_name", givenName)

        userPool.signUpInBackground(username, password, userAttributes, null, object: SignUpHandler {
            override fun onSuccess(user: CognitoUser?, signUpResult: SignUpResult?) {
                if (signUpResult?.isUserConfirmed == true) {
                    callback(Result.success(true)) // 確認済み
                } else {
                    callback(Result.success(false)) // 確認コードの入力が必要
                }
            }

            override fun onFailure(exception: Exception) {
                callback(Result.failure(exception)) // エラーハンドリング
            }
        })
    }

    // サインイン処理の実装
    override fun signIn(username: String, password: String, callback: (Result<String>) -> Unit) {
        val user = userPool.getUser(username)

        user.getSessionInBackground(object: AuthenticationHandler {
            override fun onSuccess(userSession: CognitoUserSession, newDevice: CognitoDevice?) {
                val idToken = userSession.idToken.jwtToken
                callback(Result.success(idToken)) // 認証成功でIDトークンを返す
            }

            override fun onFailure(exception: Exception) {
                callback(Result.failure(exception)) // 認証失敗時のエラーハンドリング
            }

            override fun getAuthenticationDetails(authenticationContinuation: AuthenticationContinuation, userId: String?) {
                val authDetails = AuthenticationDetails(userId, password, null)
                authenticationContinuation.setAuthenticationDetails(authDetails)
                authenticationContinuation.continueTask()
            }

            override fun getMFACode(continuation: MultiFactorAuthenticationContinuation?) {
                TODO("Not yet implemented")
            }

            override fun authenticationChallenge(continuation: ChallengeContinuation) {
                // 必要に応じて追加の認証チャレンジを処理する
                continuation.continueTask()
            }
        })
    }
}
```
最後にRepositoryとUIを繋ぎこめば完成です。

## ユーザ確認についてのLambda実装について
基本的な実装は以上になります。
しかし、Cognitoではユーザを作成した際には**確認**をしなければ、アカウントでサインインすることができません。
一般的なサービスならウェブなどでアカウント作成した後に、メールで認証リンクなどが送られてくると思います。そのリンクを踏むことでユーザ登録は完了します。
Cognitoでは自動的に確認メールを送信する設定などがあります。しかし、今回はメールを利用しないことにしていたので、サインアップ時に自動で承認を行うLambdaトリガーを作成しました。Lambdaトリガーは**ユーザプールのプロパティ**から設定できます。

### 作成したLambda
```python
import json

def lambda_handler(event, context):
    event['response']['autoConfirmUser'] = True
    return event
```
すべてのリクエストの`autoConfirmUser`をTrueにして返します。

以上で、サインアップからサインインまでの一連のフローは完成です。
## 動作
実際の動作を確認していきます。
### サインアップ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/b7ebe1c4-ac09-23de-e5c7-61d188fd97fa.gif" width=400>

処理の完了は下のStatusのところに表示しているだけになります。
AWSのコンソールを確認するとしっかりとユーザが追加されて、確認ステータスも確認済みになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/31aee0af-6665-f098-07b8-6a9ace8fada6.png)

### サインイン
サインアップで作成したアカウントのサインインをしてみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1120135/daa73eed-d8e2-aa09-c946-a31f4d86d869.gif" width=400>
無事、サインインに成功しました。このとき、アクセス用のトークンがクラウドからもらえるのでサインイン後はそのトークンを使って、クラウドにアクセスすることができます。

## まとめ
Cognitoを使って、ユーザのサインアップ・サインインを行う簡単なアプリを作ってみました。クラウドは本当に便利ですね。初めてだったので、Cognitoの設定に苦戦しましたが、慣れたらすぐにちゃんとした機能が作れそうです。今後もAWS周りでいろいろ遊んでいきたいと思います。

## 作成したアプリ
https://github.com/HikaruYam/sample-cognito-android-app/tree/main