every Tech Blog

株式会社エブリーのTech Blogです。

Flutter アプリに AWS Cognito 認証を導入した話

エブリーで小売業界に向き合いの開発を行っている @kosukeohmura です。

今回は、Flutter アプリケーションに AWS Cognito を使った認証機能を導入したプロセスについて紹介します。

バックエンドで Cognito をラップするか、アプリから直接 Cognito に接続するか

Cognito を IdP として採用し認証機構を新たに開発するにあたり、バックエンドを Cognito をラップする実装方針と、アプリから直接 Cognito に接続する方式を検討しました。結論としては今回は Flutter アプリから直接 Cognito に接続する方式を選択しました。

バックエンドで Cognito をラップする方式では、バックエンドで Cognito Admin API を用いた認証処理を自由に書くことが可能ですが、自由に処理を書ける反面、基本的には Cognito を呼び出すだけの独自のエンドポイント群を実装する必要があり、またそのエンドポイントを利用するアプリ側の画面の実装も必要になります。

一方、アプリから Amplify を利用し直接 Cognito に接続する方式では、独自のエンドポイント群を実装することなく認証機構を実装でき、加えて Amplify ですでに用意された認証画面の UI を使用すればアプリのログイン画面などの実装が不要となります。バックエンドでは認証が必要なエンドポイントそれぞれに対してアプリが Cognito から取得したユーザートークンが適正かを判別するミドルウェアを挟む形です。この方式だと、実装する対象が大きく減る一方で、Amplify の機能から外れたことを行おうとすると工夫が必要となるかと思います。

今回私達は ID/PW での認証認可さえできればよく、認証認可前後に独自の処理を挟み込んだりする必要はありませんでした。アプリの画面も一般的なサインイン画面が存在すればよかったので、Amplify のデフォルトの UI コンポーネントを少し改変すれば十分事足りそうでした。こうした背景からアプリから直接 Cognito へ接続する方式のデメリットが問題にならなそうなため、実験的に実装を行い有用性を確かめた後に、その方式を選択することとしました。

導入した主要な変更

公式の Quickstart - AWS Amplify Gen 2 Documentation を参照しながら導入を行います。

1. Amplify Flutter パッケージの追加

まず最初に必要な依存関係を追加します。これにより、Flutter アプリで Amplify の認証機能を使用できるようになります。

flutter pub add amplify_flutter amplify_auth_cognito amplify_authenticator

それぞれのパッケージの関係性がよくわかりませんでしたが、QuickStart に記載されている説明が端的でわかりやすいです。

  • amplify_flutter to connect your application with the Amplify resources.
  • amplify_auth_cognito to connect your application with the Amplify Cognito resources.
  • amplify_authenticator to use the Amplify UI components.

2. Flavor に応じて dart_define ファイルを生成・使用する

環境ごとに Cognito のユーザープールを切り替えるため、まず Flavor に応じて環境変数をアプリへ注入できるようにします。Flavor 環境変数をアプリへ注入するために dart_define ファイルを使用しますが、それをテンプレート化しておきます。

dart_define.template.json

{
  "awsRegion": "$AWS_REGION",
  "cognitoUserPoolId": "$COGNITO_USER_POOL_ID",
  "cognitoUserPoolClientId": "$COGNITO_USER_POOL_CLIENT_ID"
}

あらかじめ direnv などを使い環境変数を設定しておき、それを元に実際の dart_define ファイルを生成し利用します。以下それを行う Makefile 例です。

FLAVOR ?= development

generate-dart-define:
    cat dart_define.template.json | envsubst > dart_define_$$FLAVOR.json

run-development:
    $(FLUTTER) run --debug --flavor development --dart-define-from-file=dart_define_development.json

この仕組みにより、環境ごとに異なる環境変数を使用できます。

3. Amplify 設定ファイルの管理

GitHub で公開されているスキーマ amplify-backend_packages_client-config_src_client-config-schema_schema_v1.json at main · aws-amplify_amplify-backend に従って、Amplify 設定ファイルを構築します。AWS 上で設定するユーザープールの設定値と被る部分は合わせておきます。一部の設定値はプレースホルダとしておき、アプリ実行時に環境変数で置換します。

lib/amplify_outputs.json

{
  "version": "1.1",
  "auth": {
    "aws_region": "<Will be replaced when building>",
    "user_pool_id": "<Will be replaced when building>",
    "user_pool_client_id": "<Will be replaced when building>",
    "password_policy": {
      "min_length": 12,
      "require_lowercase": false,
      "require_uppercase": false,
      "require_numbers": false,
      "require_symbols": false
    },
    "username_attributes": ["email"],
    "user_verification_types": [],
    "unauthenticated_identities_enabled": false,
    "mfa_configuration": "NONE"
  }
}

この内容をアプリ実行時に環境変数で上書きすることで、各環境に適した設定が適用されます。

4. アプリ起動後に Amplify をセットアップする

準備した Amplify 設定ファイルと環境変数を使用し、アプリ起動後に Amplify プラグインの初期化を行います。それを行うための AmplifyService を実装しました:

lib/services/amplify.dart

class AmplifyService {
  final Ref _ref;
  final BundleRepository _bundleRepository;

  @override
  Future<Result<void, MyAppException>> setupAmplify() async {
    try {
      await Amplify.addPlugin(AmplifyAuthCognito());
      final amplifyConfigResult = await _buildAmplifyConfig();
      if (amplifyConfigResult.isError) throw amplifyConfigResult.error;

      await Amplify.configure(amplifyConfigResult.okValue);
      return Result.ok(null);
    } catch (e, s) {
      return Result.error(MyAppException(e, s));
    }
  }

  // lib/amplify_outputs.json ファイルを取得し Map として返却
  Future<Result<String, MyAppException>> _buildAmplifyConfig() async {
    final amplifyConfigString = await _bundleRepository.readBundle(BundlePath.amplifyConfig);
    final amplifyConfig = jsonDecode(amplifyConfigString) as Map<String, dynamic>;
    amplifyConfig['auth']['aws_region'] =          /* 環境変数から注入 */ ;
    amplifyConfig['auth']['user_pool_id'] =        /* 環境変数から注入 */ ;
    amplifyConfig['auth']['user_pool_client_id'] = /* 環境変数から注入 */ ;

    return Result.ok(jsonEncode(amplifyConfig));
  }
}

あとは、アプリケーションのエントリーポイントで AmplifyService を使用します:

Future<void> main() async {
  // ... 既存の初期化処理

  final amplifySetupResult = await container.read(amplifyServiceProvider).setupAmplify();
  if (amplifySetupResult.isError) {
    logger.fatalException('Error occurred when configuring Amplify', amplifySetupResult.error);
  }

  runApp(UncontrolledProviderScope(container: container, child: const MyApp()));
}

これで Amplify のセットアップが完了しました。加えて QuickStart どおりに UI コンポーネントを記述すればサインイン/サインアップ画面が表示されますが、特段難しいことはなく省略します。

まとめ

今回の実装では、Amplify UI を使用することで比較的スムーズに認証機能を導入できました。バックエンド経由ではなく Flutter から直接 Cognito に接続することで、開発速度を重視しながらもセキュリティを保つことができました。

また、環境間で可変な値を環境変数で管理し、テンプレートファイルを使用することで、異なる環境(開発・本番)へ向けて安全にアプリを実行できるようになりました。Amplify セットアップ Service クラスに煩雑な処理を押し込むことで、アプリケーションのエントリーポイントに持たせる責務を最小限に抑えることができました。

Flutter での認証実装を検討されている方の参考になれば幸いです。

参考資料