[Flutter]公式に対応されたAmplify Flutter使ってみる ~認証編~

Amplify Flutterの内、認証部分を試しました。
2021.03.07

ウェブやネイティブといったフロントエンドを担当するエンジニアがバックエンドSaaSを採用する際、特にネイティブアプリの場合はFirebaseがデファクトスタンダードといって良いと思います。利用者が多いと情報も多く、気軽に利用しやすいので個人ユースでも採用されるケースが多いものの、Amplifyも精力的にアップデートがされているようで、最近Flutter向けのSDKも公開されました。

この記事では実際にAmplify Flutterの認証に絞って実際に使ってみます。

Amplify Flutterの対象になるユーザーは

  • Flutter エコシステムに投資したものの、今は AWS の力も利用したい

とのことなので、現在提供されている認証や分析、S3の利用などの機能以外にも機能は増えていきそうです。

環境構築

実行環境は以下です。

Flutter 1.22.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 9b2d32b605 (6 weeks ago) • 2021-01-22 14:36:39 -0800
Engine • revision 2f0af37152
Tools • Dart 2.10.5

AWS Amplify は、より迅速なアプリ開発を実現する一連のツールです。ユーザーの認証、ファイルの保存、分析イベントのキャプチャ、その他多数の機能をSDK越しに利用できます。

Amplify Flutterで必要な各種ツールの動作要件

Node.js v10.x or later
npm v5.x or later
git v2.14.1 or later

Install Flutter version 1.20.0 or higher (make sure you are using a stable version of flutter)

Amplify FlutterのFlutter向けのSDKのリポジトリは以下です。

Amplify CLIでの環境構築

npm install -g @aws-amplify/cli
amplify configure

CLIが適宜案内をしてくれるので指示にしたがって認証やユーザーの追加を行います。以下はAmplify CLIが使用するユーザーを作成している部分です。

アクセスキーやシークレットキーの入力もCLI上で行います。

Press Enter to continue

Enter the access key of the newly created user:
? accessKeyId:  ********************
? secretAccessKey:  ****************************************
This would update/create the AWS Profile in your local machine
? Profile Name:  default

Successfully set up the new user.

これらが終わったらpubspec.yamlのdependencyに依存するライブラリを記述してflutter pub getします。

  cupertino_icons: ^1.0.0
  amplify_flutter: '<1.0.0'
  amplify_auth_cognito: '<1.0.0'
  amplify_analytics_pinpoint: '<1.0.0' # 認証のみで良いならanalytics_pinpointは不要
  flutter_login: ^1.0.14 # flutter_loginは直接関係ないが後述する理由で使用するのでこのタイミングで追加

Amplifyをアプリ内で利用する際初期設定が必要になります。使用する機能を追加して設定を行っています。

AmplifyAnalyticsPinpoint analyticsPlugin = AmplifyAnalyticsPinpoint();
AmplifyAuthCognito authPlugin = AmplifyAuthCognito();
Amplify.addPlugins([authPlugin, analyticsPlugin]); // 認証のみを利用するならanalyticsPluginは不要
await Amplify.configure(amplifyconfig);

認証を行うモジュールの追加

amplify add auth
amplify push

addした時の回答は以下です。Emailでの認証をしたいので、2つ目はusernameでなくEmailを選ぶようにします。

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

How do you want users to be able to sign in?
> Email

Do you want to configure advanced settings?
> No, I am done.

pushコマンドでAWSのCognito画面上に新しくユーザープール、IDプールが作成されます。AmplifyのAuthは内部的にCognitoを利用しています。Firebaseと同じようにAmplifyもAWSの各種サービスがアプリケーション開発用にパッケージ化されています。

実装

flutter_loginについて

フォーム周りの実装をパッケージに任せたかったのでflutter_loginというパッケージを使っています。これをベースにAmplifyでの認証に必要な画面を作っていきます。

登録画面

登録した後に認証コードが登録したメールアドレス宛てに送られてきます。それを入力する画面を実装する必要があります。以下のようになります。

登録以外にログインも必要になります。以下のようになります。

認証を行うモジュールの作成

AuthServiceというクラス越しに認証周りの操作を行うように実装します。

認証を行うメソッドは必要な情報をユーザー定義のAuthCredentialsという抽象クラスのインターフェースを実装した具象クラスから受け取るようにします。このユーザー定義の型はAmplify Flutterのチュートリアルを一通りやった時にあった型の実装をそっくり真似ています。

このチュートリアルをやる場合は宣言的なインターフェースでナビゲーションを行うことができるNavgator2.0を利用していて、今までのNavigatorにしか慣れていない人は事前にキャッチアップが必要になります。

abstract class AuthCredentials {
  final String username;
  final String password;

  AuthCredentials({@required this.username, @required this.password})
      : assert(username != null),
        assert(password != null);
}

class LoginCredentials extends AuthCredentials {
  LoginCredentials({String username, String password})
      : super(username: username, password: password);
}

class SignUpCredentials extends AuthCredentials {
  final String email;
  SignUpCredentials({String username, String password, @required this.email})
      : assert(email != null),
        super(username: username, password: password);
}

登録はsignUpというメソッドで行います。登録した後認証コードを入力させる必要があります。

Future<SignUpResult> _signUp(SignUpCredentials credentials) async {
  return await Amplify.Auth.signUp(
      username: credentials.email,
      password: credentials.password,
      options: CognitoSignUpOptions(userAttributes: {
        'email': credentials.email,
      }));
}

Future<String> onSignup(LoginData data) async {
  try {
    await _signUp(SignUpCredentials(
      email: data.name,
      password: data.password,
      username: data.name,
    ));
     this.data = data;
  } on AuthException catch (e) {
    return e.message;
  }
}

認証コードの送信に成功した後、改めてログインする必要があります。そこでこれまで入力してきた情報がもう一度必要になります。

MaterialAppのconstructorの引数onGenerateRouteで認証情報を受け渡すようにします。LoginDataはflutter_formで定義されている認証情報です。

onGenerateRoute: (settings) {
  if (settings.name == '/confirm') {
    return PageRouteBuilder(
    pageBuilder: (_, __, ___) =>
        ConfirmPage(data: settings.arguments as LoginData),
    transitionsBuilder: (_, __, ___, child) => child,
  );
}

遷移させる側のコードは以下です。pushReplacementNamedのargumentsで認証に使った情報を渡しています。

Navigator.of(context).pushReplacementNamed(
  _authService.isSignedIn ? '/dashboard' : '/confirm',
  arguments: _authService.data,
);

認証コードを送信する処理は以下です。AuthServiceのインスタンスメソッドとして定義しています。signUpのフローが終わった後にsignInを改めて行う必要があるのでsignUpに使った認証情報はメモリに保持しておかないと、またユーザーに認証情報を入力してもらわなければなりません。

そのため、AuthServiceのフィールドに認証情報を保持するようにしています。

Future<void> verifyCode(
    LoginData data, String code, VoidCallback onSucceeded) async {
  final res = await Amplify.Auth.confirmSignUp(
    username: data.name,
    confirmationCode: code,
  );
   if (res.isSignUpComplete) {
     // Login user
    final user = await Amplify.Auth.signIn(
       username: data.name, password: data.password);
    if (user.isSignedIn) {
      onSucceeded();
    }
  }
}

ここまでで登録と認証コードの送信ができます。実行結果を動画にしました。

認証コード検証の時にも出てきましたがログインはsignInメソッドを使用することで実装できます。

Future<SignInResult> _signIn(AuthCredentials credentials) async {
  return await Amplify.Auth.signIn(
  username: credentials.username, password: credentials.password);
}

実行結果です。

ログインしているユーザーの情報を取得する場合はAmplify.Auth.getCurrentUser()を使用します。これでログインした後にユーザー情報を中央に表示しています。

static Future<AuthUser> get currentUser async {
  return await Amplify.Auth.getCurrentUser();
}

現在のセッションをチェックしたい場合

Authではサインインするだけで、認証情報を最新の状態に保ち、他のカテゴリに提供するために必要な全ての処理を行います。しかし、直接認証情報にアクセスしたい場合があります。アプリ再起動時などにこれを行わないと現在ログイン中のユーザーが確認できませんでした。fetchAuthSessionというメソッドを使って取得できます。サンプルのアプリでは以下の実装を使って認証状態を確認して、サインイン状態であればユーザー情報を表示する画面を表示するようにしています。

static Future<AuthSession> get authSession async {
  return await Amplify.Auth.fetchAuthSession();
}

// 利用する側
final session = await AuthSession().authSession();
if session.isSignedIn {
  // サインインしている場合の処理
}

最後にAuthService全体のコードを記載します。

import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
// Amplify Flutter Packages
import 'package:amplify_flutter/amplify.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_login/flutter_login.dart';

abstract class AuthCredentials {
  final String username;
  final String password;

  AuthCredentials({@required this.username, @required this.password})
      : assert(username != null),
        assert(password != null);
}

class LoginCredentials extends AuthCredentials {
  LoginCredentials({String username, String password})
      : super(username: username, password: password);
}

class SignUpCredentials extends AuthCredentials {
  final String email;
  SignUpCredentials({String username, String password, @required this.email})
      : assert(email != null),
        super(username: username, password: password);
}

class AuthService {
  bool isSignedIn = false;
  LoginData data;

  static Future<AuthUser> get currentUser async {
    return await Amplify.Auth.getCurrentUser();
  }

  static Future<AuthSession> get authSession async {
    return await Amplify.Auth.fetchAuthSession();
  }

  static Future<void> signOut() async {
    Amplify.Auth.signOut();
  }

  Future<SignInResult> _signIn(AuthCredentials credentials) async {
    return await Amplify.Auth.signIn(
        username: credentials.username, password: credentials.password);
  }

  Future<SignUpResult> _signUp(SignUpCredentials credentials) async {
    return await Amplify.Auth.signUp(
        username: credentials.email,
        password: credentials.password,
        options: CognitoSignUpOptions(userAttributes: {
          'email': credentials.email,
        }));
  }

  Future<String> onLogin(LoginData data) async {
    try {
      final credentials = LoginCredentials(
        username: data.name,
        password: data.password,
      );

      final res = await _signIn(credentials);
      isSignedIn = res.isSignedIn;
    } on AuthException catch (e) {
      return e.message;
    }
  }

  Future<String> onSignup(LoginData data) async {
    try {
      await _signUp(SignUpCredentials(
        email: data.name,
        password: data.password,
        username: data.name,
      ));

      this.data = data;
    } on AuthException catch (e) {
      return e.message;
    }
  }

  Future<String> onRecoverPassword(
      String email, VoidCallback completion) async {
    try {
      final res = await Amplify.Auth.resetPassword(username: email);

      if (res.nextStep.updateStep == 'CONFIRM_RESET_PASSWORD_WITH_CODE') {
        completion();
      }
    } on AuthException catch (e) {
      return e.message;
    }
  }

  Future<void> resetPassword(LoginData data, String code, String password,
      VoidCallback onSucceeded) async {
    final res = await Amplify.Auth.confirmPassword(
      username: data.name,
      newPassword: password,
      confirmationCode: code,
    );
    onSucceeded();
  }

  Future<void> verifyCode(
      LoginData data, String code, VoidCallback onSucceeded) async {
    final res = await Amplify.Auth.confirmSignUp(
      username: data.name,
      confirmationCode: code,
    );

    if (res.isSignUpComplete) {
      // Login user
      final user = await Amplify.Auth.signIn(
          username: data.name, password: data.password);

      if (user.isSignedIn) {
        onSucceeded();
      }
    }
  }

  Future<void> resendCode(LoginData data, VoidCallback onSucceeded) async {
    await Amplify.Auth.resendSignUpCode(username: data.name);
    onSucceeded();
  }
}

まとめ

最小限の実装にしたら全部まとめて説明できるのではと思ったのですが、それだとちょっと長すぎたので主要な機能ごとに書くことにしました。想像していたよるずっと簡単に利用できたので今後のアップデート次第ではFirebase以外の選択肢の1つになるかも知れないなと思いました。Flutterの学習・開発は始めたばかりで説明やその裏にある認識に誤りがあるかもしれません。なにかお気づきの際はコメントやTwitterなどでお知らせください。