Skip to content

[firebase_app_check]: getLimitedUseToken() returns standard 1-hour tokens on iOS/macOS, so replay-protected services reject them #18718

Description

@maxiwoj

Is there an existing issue for this?

  • I have searched the existing issues.

Which plugins are affected?

App Check

Which platforms are affected?

iOS, macOS

Description

On iOS (and macOS), FirebaseAppCheck.instance.getLimitedUseToken() returns a standard App Check token with a 1-hour TTL instead of a limited-use token (5-minute TTL). Every Apple provider goes through the same wrapper (see root cause), so this applies to debug, App Attest, DeviceCheck and reCAPTCHA alike. I reproduced it with the debug provider.

As a result, anything that relies on limited-use tokens fails on Apple platforms once replay protection is enforced. For example, Firebase AI Logic with FirebaseAI.googleAI(useLimitedUseAppCheckTokens: true) rejects every request, and the App Check console reports them as "Unverified: Reused token". The same app on Android works, because Android returns proper 5-minute limited-use tokens.

Root cause. The plugin installs its own provider through FlutterAppCheckProviderFactory → AppCheckProviderWrapper (ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift). The wrapper implements only getToken(completion:). getLimitedUseToken(completion:) is an optional method of AppCheckProvider, added in firebase-ios-sdk #12067. So FIRInternalAppCheckProvider finds that the wrapper doesn't implement it and silently falls back to getToken:

- (void)getLimitedUseTokenWithCompletion:(...)handler {
  if ([self.appCheckProvider respondsToSelector:@selector(getLimitedUseTokenWithCompletion:)]) {
    ...
  } else {
    [self getTokenWithCompletion:handler];
  }
}

The wrapped provider (for example AppCheckDebugProvider, which does send limited_use: true when asked) is therefore never asked for a limited-use token.

History. The wrapper (#10401, Mar 2023) and getLimitedUseToken() (#11091, Jun 2023) both predate the optional protocol method, so neither needed it at the time. react-native-firebase, whose wrapper design the plugin followed, added the forwarding in invertase/react-native-firebase#7424 (Nov 2023). FlutterFire never made the matching change, and the Swift rewrite (#18569) kept the same behaviour. #12468 appears to have hit the same symptom (a 1-hour token from getLimitedUseToken()) but was closed without a diagnosis.

The getLimitedUseToken e2e test didn't catch this because it only checks that an exception is thrown when no debug token is configured; it never looks at the returned token.

Reproducing the issue

  1. Activate App Check with any Apple provider, e.g. FirebaseAppCheck.instance.activate(providerApple: const AppleDebugProvider()), and register the debug token.
  2. Call await FirebaseAppCheck.instance.getLimitedUseToken().
  3. Decode the JWT: exp - iat is 3600. It should be 300.

End to end: use firebase_ai with useLimitedUseAppCheckTokens: true and replay protection enforced for Firebase AI Logic. Every iOS request is rejected ("Unverified: Reused token" in the console), while Android succeeds.

Verified on a physical iPhone (iOS 26.6.2): TTL 3600 s and rejected before the fix, TTL 300 s and HTTP 200 after forwarding the method. Android emulator: TTL 300 s and HTTP 200 unchanged.

Firebase Core version

4.15.0 (firebase_app_check 0.4.8; still present on main)

Flutter Version

3.47.5

Relevant Log Output

Request token claims (iOS, before fix):  exp - iat = 3600s   → HTTP 401 "Firebase App Check token is invalid."
Request token claims (iOS, after fix):   exp - iat = 300s    → HTTP 200
Request token claims (Android):          exp - iat = 300s    → HTTP 200

Flutter dependencies

firebase_core 4.15.0
firebase_app_check 0.4.8
firebase_ai 4.0.0
Firebase Apple SDK 12.19.0 (CocoaPods)

Additional context and comments

I have a fix ready (forward getLimitedUseToken(completion:) in AppCheckProviderWrapper, plus an e2e assertion on the TTL) and will open a PR referencing this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions