firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.63k stars 3.95k forks source link

[firebase_app_check]: The new Firebase App Check token is not being updated in Firestore and other services that have App Check enforced. #12799

Closed eli1stark closed 3 months ago

eli1stark commented 4 months ago

Is there an existing issue for this?

Which plugins are affected?

App Check

Which platforms are affected?

Android, iOS

Disclaimers

  1. I was able to reproduce the issue on Android, but a similar issue exists on iOS. I just have a hard time reproducing it locally (Crashlytics shows a lot of errors with get, update, set, and listen operations, resulting in permission denied for many users on both iOS and Android). 2% of our user base is impacted by this issue and similar ones related to App Check in one way or another.

  2. This issue is reproduced in debug mode due to the nature of App Check and how difficult it is to reproduce it in a release build.

Scenario

Imagine the following scenario:

  1. I installed the brand new app (the app requires Firebase Auth to proceed further).

  2. I made a successful sign-in to the app (the app requires making a get call to Firestore to fetch the user to proceed further).

  3. I tried to fetch the user, but I was hit with the error: [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.

  4. I started digging and thinking, "Okay, maybe it's the security rules." However, the rules were simple: allow read if auth != null, and I had just made a successful sign-in before requesting data from Firestore, so that wasn't the issue. I began wondering what else could be causing the problem, and the only other reason it could fail was because of issues with the App Check token.

  5. I thought, "What if the App Check token wasn't fetched properly before making the call to Firestore? This is plausible, maybe it's some kind of race condition." So, I decided to ensure that the token was 100% valid by fetching it beforehand. It sounded like a good idea. I made a fetch and successfully fetched the token. I then made the call to Firestore, thinking that now everything should be rock solid, but no, it threw: [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.

  6. I started thinking again, "WHAT? Okay, I know what it is. Firestore is not picking up the newly fetched token, but why not? Isn't it how it's supposed to work?" I wondered if there was a way to pass it manually to Firestore, but it seemed like there was no way.

  7. So, here's where the issue lies: even if I ensure app attestation by fetching the token, Firestore, Cloud Functions, and Realtime Database will still fail because they won't pick up the new token until the app is restarted.

  8. Yes, you heard me right. If I hot restart the app in debug mode, it picks up the token (which is equivalent to killing the app and opening it again), but this is not a great solution because no user will do this. They will just churn and delete the app, so this is a serious issue here.

Prerequisites

if (kDebugMode) {
  await FirebaseAppCheck.instance.activate(
    appleProvider: AppleProvider.debug,
    androidProvider: AndroidProvider.debug,
  );
} else {
  await FirebaseAppCheck.instance.activate(
    appleProvider: AppleProvider.appAttest,
  );
}

Even though it's part of the story, authentication is not needed here to simplify the case.

Firestore

Make sure your Firestore rules look like this to get them out of the way:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

Now, enforce App Check on your Firestore in Firebase.

App Setup

Have 2 buttons ready:

  1. One that makes a get request to Firestore like this:
    try {
      await FirebaseFirestore.instance.collection('path').doc('id').get(
            const GetOptions(
              source: Source.server,
            ),
          );
    } catch (e) {
      print(e);
    }
  2. Another that fetches the token like this:
    try {
      final token = await FirebaseAppCheck.instance.getToken(true);
      print(token); // ensure token is fetched properly
    } catch (e) {
      print(e);
    }

Steps to Reproduce

  1. Build the app for Android in debug mode on a real Android device, making sure you activated App Check in main.dart. Copy the App Check debug token from the debug console, you will need it later.

  2. Press the 1st button (ensure that the call fails due to App Check).

  3. Press the 2nd button (ensure it fails with a 403 error - app attestation failed).

  4. Since this is debug mode, to emulate App Check behavior, take the debug token you copied in the 1st step and add it to App Check debug tokens for Android (in Firebase).

  5. Once you've added the debug token, press the 2nd button again. The token should now be printed, indicating that you were attested by Firebase without issues.

  6. Now, press the 1st button again, and it should throw cloud_firestore/permission-denied.

  7. Hot restart the app.

  8. Press the 1st button again, it should fetch without issues.

The issue here is that it should have worked on the 6th step.

To test this multiple times, delete the app and reinstall it again so a new debug token can be generated.

Firebase Core version

2.30.1

Flutter Version

3.22.0

Relevant Log Output

No response

Flutter dependencies

Expand Flutter dependencies snippet
```yaml firebase_core: 2.30.1 firebase_crashlytics: 3.5.4 firebase_analytics: 10.10.4 firebase_dynamic_links: 5.5.4 firebase_app_check: 0.2.2+4 firebase_auth: 4.19.4 firebase_database: 10.5.4 firebase_performance: 0.9.4+4 firebase_remote_config: 4.4.4 cloud_firestore: 4.17.2 cloud_functions: 4.7.3 ```

Additional context and comments

Maybe related to:

  1. https://github.com/firebase/firebase-android-sdk/issues/5101
  2. https://github.com/firebase/firebase-android-sdk/issues/5235
russellwheatley commented 4 months ago

@eli1stark - I created a pure android implementation of the same setup:https://github.com/russellwheatley/firebase-android-project

It took some time (around 5 mins) for the firestore request to be successful. This was also the case when I tested the flutter app check example app. Might be worth asking Firebase support if this is intended behaviour as it appears that this is how it works.

eli1stark commented 4 months ago

@russellwheatley Thank you for looking into it! If that's how it works by design, then it's a big issue for apps that use App Check, because applications could potentially cut functionality for 2-3% of their user base without even realizing it. And these are legit iOS and Android devices. This is especially concerning now when Google is heavily pushing App Check with Gemini AI. For now, the only feasible option is to disable App Check and rely only on security rules. Once I disabled App Check, 95% of all permission related issues in Crashlytics went away.

What's the best way to reach out to them, is this a good link?

russellwheatley commented 4 months ago

@russellwheatley Thank you for looking into it! If that's how it works by design, then it's a big issue for apps that use App Check, because applications could potentially cut functionality for 2-3% of their user base without even realizing it. And these are legit iOS and Android devices. This is especially concerning now when Google is heavily pushing App Check with Gemini AI. For now, the only feasible option is to disable App Check and rely only on security rules. Once I disabled App Check, 95% of all permission related issues in Crashlytics went away.

What's the best way to reach out to them, is this a good link?

It might be worth asking on Stack Overflow as you could get advise on how to circumvent it perhaps 🤔

google-oss-bot commented 3 months ago

Hey @eli1stark. We need more information to resolve this issue but there hasn't been an update in 7 weekdays. I'm marking the issue as stale and if there are no new updates in the next 7 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

google-oss-bot commented 3 months ago

Since there haven't been any recent updates here, I am going to close this issue.

@eli1stark if you're still experiencing this problem and want to continue the discussion just leave a comment here and we are happy to re-open this.