wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

FlutterでFirebase App Checkに対応(+Firebase App Distributionで配信時の対応)

Firebase App Checkへの対応について、
Firebase App Distributionで配信したアプリにApp Checkを対応させる際、いくつかハマりポイントがあったので記事にた

PR

github.com

github.com

実装

Firebase App Checkを実装

firebase.google.com

Firebase App Checkを実装するとiOSではApp AttestDeviceCheck、AndroidではPlay Integrityで正常に端末からアクセスされているかを判定できる

FlutterでApp Checkを使用するには、まず* firebase_app_check**パッケージを導入する

pub.dev

次にアプリ側に以下のコードを追加

dart/app/lib/main.dart

  final appEnv = dotenv.env['APP_ENV'];

  await FirebaseAppCheck.instance.activate(
    // Androidの場合はApp Distributionを使用する Play Integrityの認証が成功しないのでdebugの方で実装
    androidProvider: appEnv == 'production'
        ? AndroidProvider.playIntegrity
        : AndroidProvider.debug,
    appleProvider:
        kReleaseMode ? AppleProvider.deviceCheck : AppleProvider.debug,
  );

Firebase App Distributionでテスト用のアプリを配信している場合は、aabで配信していても通常のPlay Store経由でインストールされたと認識されず、認証時にエラーが発生してしまうため、AndroidProvider.debugを使用して認証するように実装

今回はアプリで取得したApp Checkトークンによって、認証された端末以外からのAPIアクセスを禁止するようにした。バックエンドでApp Checkトークンの認証を行う方法は以下のとおり

まず、アプリ側でApp Checkのトークンを発行し、APIのHeaderに設定

dart/app/lib/utils/graphql.dart

  // App Checkトークンを設定するAuthLink
  final AuthLink appCheckAuthLink = AuthLink(
    getToken: () async {
      final appCheckToken = await getAppCheckToken();
      return appCheckToken;
    },
    headerKey: 'X-Firebase-AppCheck', // App Check用
  );

  final link = authLink.concat(appCheckAuthLink).concat(httpLink);

getAppCheckToken関数は以下の通り。App Checkトークンは1時間有効なので、キャッシュして利用

dart/app/lib/utils/graphql.dart

Future<String?> getAppCheckToken() async {
  const tokenKey = 'appCheckToken';
  const tokenTimeKey = 'tokenFetchTime';

  try {
    final cachedAppCheckToken = await secureStorage.read(key: tokenKey);
    final tokenFetchTimeString = await secureStorage.read(key: tokenTimeKey);

    if (cachedAppCheckToken != null && tokenFetchTimeString != null) {
      final tokenFetchTime = DateTime.parse(tokenFetchTimeString);
      final currentTime = DateTime.now();
      final difference = currentTime.difference(tokenFetchTime);
      if (difference.inHours < 1) {
        return cachedAppCheckToken;
      }
    }

    final appCheckToken = await FirebaseAppCheck.instance.getToken(); // ←ここでApp Checkのトークンを取得
    if (appCheckToken != null) {
      await secureStorage.write(key: tokenKey, value: appCheckToken);
      await secureStorage.write(
          key: tokenTimeKey, value: DateTime.now().toIso8601String());
      return appCheckToken;
    }
  } catch (e) {
    print('Error fetching App Check token: $e');
  }

  return "";
}

この実装により、APIアクセス時にHeaderのX-Firebase-AppCheckにApp Checkトークンが設定される

バックエンドでは、上記で渡されたX-Firebase-AppCheckデータを以下のコードで認証

typescript/backend/src/common/guards/auth/auth.guard.ts

    if (this.configService.get('NODE_ENV') === 'production') {
      const appCheckToken = request.headers['x-firebase-appcheck']
      if (!appCheckToken) {
        throw new Error('App Check token not found')
      }

      try {
        await getAppCheck().verifyToken(appCheckToken)
      } catch (e) {
        console.log('error', e)
        throw new Error('Unauthorized (App Check)')
      }
    }

この実装により、APIアクセス時にApp Checkトークンが存在しない場合はエラーが表示される

ただし、上記の実装だけでは、実機からのアクセスおよび正式にストアからインストールされたアプリ以外ではAPIが実行できなくなる。開発時にはApp Checkのデバッグトークンを使用して認証を行う。

iOSでデバッグトークンを取得する方法は、Flutter アプリをXcodeで起動
アプリが起動するとXcodeのログにFirebase App Check Debug Token: *****が出力される

これをFirebaseのコンソールから App Checkのデバッグトークンを登録すれば認証が可能

Android + エミュレータ機能の場合もほぼ同じ。 ローカルでアプリが起動するとログにEnter this debug secret into the allow list in the Firebase Console for your project: *****が出力される

これをFirebaseコンソールで登録することで、認証が可能になる。

最後に、Firebase App DistributionでAndroidアプリを配布した場合も同様で、Play Store経由でインストールされていないため、Play Integrityの認証が通らず、デバッグトークンが必要になる

以下の手順で取得できる - 1. Firebase App DistributionでAndroidアプリを実機でインストール - 2. PCと Androidの実機をUSBケーブルで接続 - 3. PCのターミナルで以下のコマンドを実行して、Androidのログを出力させる

$ adb logcat | grep "Firebase Console for your project"

これでエミュレータと同様にEnter this debug secret into the allow list in the Firebase Console for your project: *****の値が出力される。この値をFirebaseのコンソールから App Checkのデバッグトークンとして登録することで認証が可能

これでApp Checkの認証およびデバッグ時の実装が完了した