AWS Amplify+Cognitoでユーザー認証画面をネイティブアプリで作ってみる


AWSの認証はWebUI(Hosted UI)が用意されているのでそちらで組めばサーバーレスの認証は簡単に行えますが、レイアウトを細かく変更したい時にUIをネイティブアプリで作りたくなるなと思い、ネイティブUIから繋げる方法を試してみました。

Cognitoの管理画面でWebページのUIのカスタマイズの設定もありますので、そちらで十分なら必要ないと思います。
あと、後述しますがソーシャルログインも加えたいなら素直にWebUIで実装した方が良さそうです。

Cognitoの設定などはAmplifyを使いました。
(AWSアカウント作成やIAMは事前に作成しています)

準備

AmplifyのCLIをインストールします。

npm install -g @aws-amplify/cli

手元でインストールしたバージョンはこちらです。

$ node -v
v11.12.0

$ amplify -v
4.26.0

podインストール

認証周りの実装で使用するのでAmplifyをpodインストールします。
検証時点の最新バージョンにしています。

pod 'Amplify', '1.1.0'
pod 'AmplifyPlugins/AWSCognitoAuthPlugin', '1.1.0'

podインストールします

$ pod install --repo-update

Universal Linksの設定

今回はメール認証完了後にリダイレクトURLでアプリに戻すので事前にUniversal Linksの対応をしておきます。

後で追加も可能ですが、この後のamplify add authの設定中にリダイレクトURLを記述するところがあるので先に準備しておく方がスムーズだと思います。

リダイレクトURLはLambdaに追加されるCustomMessage内に追加されるので間違えた場合はあとで変更も可能です。

今回はS3とCloudFrontを使って対応しましたが、記述が長くなったので別の記事に纏めました。

AWS S3+CloudFront使ってUniversal Linksでアプリを起動する

ここで作成した https://****.cloudfront.net をリダイレクトURLとして使用していきます。

Amplify アプリケーション追加

ターミナルでプロジェクト直下まで移動し、amplify initを実行します。

$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project {ここはアプリケーションの名前になるので好きなように命名してください}
? Enter a name for the environment dev
? Choose your default editor: None
? Choose the type of app that you're building ios
Using default provider  awscloudformation

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default

プロジェクトに amplify.jsonamplifyconfiguration.json を追加しておきます。

Amplify Auth追加

amplifyでAuthを追加していきます。

Email使ってサインイン、サインアップさせたいのでマニュアルで諸々設定していきます。

Warning: you will not be able to edit these selections. のところは後で変更できないので注意してください。

$ amplify add auth

Do you want to use the default authentication and security configuration? Manual configuration

Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM co
ntrols (Enables per-user Storage features for images or other content, Analytics, and more)

Please provide a friendly name for your resource that will be used to label this category in the project: amplify****

Please enter a name for your identity pool. amplify****_identitypool_****

Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) No

Do you want to enable 3rd party authentication providers in your identity pool? Yes

Select the third party identity providers you want to configure for your identity pool: {選択なし}

Please provide a name for your user pool: amplify****_userpool_****

Warning: you will not be able to edit these selections.
How do you want users to be able to sign in? Email

Do you want to add User Pool Groups? No

Do you want to add an admin queries API? No

Multifactor authentication (MFA) user login options: OFF

Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)

Please specify an email verification subject: Your verification code

Please specify an email verification message: Your verification code is {####}

Do you want to override the default password policy for this User Pool? Yes

Enter the minimum password length for this User Pool: 8

Select the password character requirements for your userpool: Requires Lowercase, Requires Uppercase, Requires Numbers

Warning: you will not be able to edit these selections.
What attributes are required for signing up? Email, Name

Specify the app's refresh token expiration period (in days): 30

Do you want to specify the user attributes this app can read and write? No

Do you want to enable any of the following capabilities? Email Verification Link with Redirect

Do you want to use an OAuth flow? No

? Do you want to configure Lambda Triggers for Cognito? No

? Enter the URL that your users will be redirected to upon account confirmation: {ユニバーサルリンクが起動するURL、同じ手順で進めていれば今回はCloudFrontのURLになります}

? Enter the subject for your custom account confirmation email: アカウント確認

? Enter the body text for your custom account confirmation email (this will appear before the link URL): 下記のURLをクリックすることで、メールアドレスの認証が完了となります。

Successfully added resource amplify**** locally

成功したらマネジメントコンソールに反映させるためにamplify pushしておきましょう。

Universal Linksを動かすために必要な処理

実はこのままだとリダイレクトURLを踏んでも認証は完了しません。

メールで届くURLはS3の****verificationbucket-***を表示して、verify.jsを実行して認証完了後に指定したURLを開こうとしていますが、そのままだと認証時にエラーになるのでローディングが回り続けて終わります。

verify.jsで発生しているエラーはNotAuthorizedException: Unable to verify secret hash for client *****というもので

Cognitoに登録されているアプリにクライアントシークレットが設定されていることが原因です。

対応としてはCognitoのアプリにクライアントシークレットを設定していないアプリを追加して、アプリ側のjsonファイルを書き換えます。

Cognito->UserPoolのアプリクライアントの設定画面で別のアプリクライアントの追加から新規で作成します。

クライアントシークレットを生成の項目のチェックを外して作成します。

合わせてamplifyconfiguration.jsonawsconfiguration.jsonの下記項目を書き換えます。

// PoolIdとRegionは変更の必要無し
// AppClientIdに新しく追加したクライアントIDに書き換え、AppClientSecretは削除する
"CognitoUserPool": {
    "Default": {
        "PoolId": "****",
        "AppClientId": "****",
        "Region": "****"
    }
}

アプリ側の実装

ここからはアプリ側を実装していきます。

今回はAmplify.Authで実装していますが、AWSMobileClient.defaultでも同じように実装は可能です。

Amplify, AmplifyPluginsをインポート

import Amplify
import AmplifyPlugins

Authのセッション確認

ユーザーがCognitoのUserPoolに登録されたあとにSignInすれば、session.isSignedIn == true になります。

_ = Amplify.Auth.fetchAuthSession(listener: { (result) in
    do {
        let session = try result.get()
        print("fetchAuthSession succcess:\(session.isSignedIn)")
    } catch {
        print("fetchAuthSession error:\(error)")
    }
})

Auth API実行時のコールバック受け取り

_ = Amplify.Hub.listen(to: .auth) { (payload) in
    switch payload.eventName {
    case HubPayload.EventName.Auth.signedIn:
        // ログイン済み
    case HubPayload.EventName.Auth.signedOut:
        // ログアウト
    case HubPayload.EventName.Auth.sessionExpired:
        // セッション切れ
    default: break
    }
}

新規登録(サインアップ)

amplify add auth(What attributes are required for signing up?)でEmail, Nameで設定した場合の例になります。

オプションで名前、signUpの第一引数でメールアドレスを設定しないとエラーになりました。

let userAttributes = [AuthUserAttribute(.name, value: userName)]
let options = AuthSignUpRequest.Options(userAttributes: userAttributes)
_ = Amplify.Auth.signUp(username: mailAddress, password: password, options: options) { result in
    switch result {
    case .success(let signUpResult):
        if case let .confirmUser(deliveryDetails, _) = signUpResult.nextStep {
            print("signUp: \(String(describing: deliveryDetails))")
        } else {
            print("signUp: complete")
        }
    case .failure(let error):
        print("signUp error: \(error)")
    }
}

ログイン(サインイン)

amplify add auth(How do you want users to be able to sign in?)でEmailを設定した場合の例になります。

_ = Amplify.Auth.signIn(username: mailAddress, password: password) { result in
    switch result {
    case .success(_):
        print("signIn success")
    case .failure(let error):
        print("signIn error: \(error)")
    }
}

ログアウト(サインアウト)

let options = AuthSignOutRequest.Options(globalSignOut: true)
_ = Amplify.Auth.signOut(options: options) { (result) in
    switch result {
    case .success:
        print("signOut success")
    case .failure(let error):
        print("signOut error: \(error)")
    }
}

処理の流れ

  1. Amplify.Auth.signUpに必要なパラメータをつめて実行する
  2. signUpが成功すれば認証確認メールが届くのでリンクをタップする
  3. 成功していればこの時点でCognitoのUserPoolの登録アカウントが有効になる
  4. リンクからの遷移先ページで下に引っ張るとアプリを開くことができる
  5. ユニバーサルリンクでアプリが開いた時にAmplify.Auth.signInを呼ぶ
  6. その後はアプリ起動時にAmplify.Auth.fetchAuthSessionでセッション確認すれば有効な状態になっていることを確認できます

という流れで処理を行い、確認ができました。

ソーシャルログインの追加について

Appleの開発者ページとマネジメントコンソールでSign In with Appleの設定をすれば成功するかと思いましたが、
それだけでは上手くいきませんでした。

ネイティブアプリ側で取得したSign In with Appleのトークンを
AWSMobileClientのfederatedSignInに渡すとsignedInが返されるのですが、管理画面にはユーザー追加されずその後のCredetialの取得も失敗します。

あと、Amplify 1.1.0のバージョンでAmplify.Hub.listenでイベント監視している時にfederatedSignInを呼ぶと言うクラッシュにも遭遇しました。

こちらはすでにamplify-ios issues #640に挙げられていて、調度修正されたところだったので次のリリースあたりには解消されているかもしれません。

そもそもWebUIでないと正常に動作しない話もあるので、また時間をおいて試してみようと思います。

おわりに

Amplifyでの初期構築はすごく簡単な反面、そこから変更加えようとすると大変でした。

今回触ったことでAWSについてもっと勉強しないとなーと思えたので、いいきっかけになりました。