Skip to content

πŸ”₯ [iOS] App Check installs the debug provider in release builds β€” config plugin registers the factory before FirebaseApp.configure()Β #9116

Description

@kolezka

Issue

On iOS, release builds using the Expo config plugin exchange App Check debug tokens against firebaseappcheck.googleapis.com, receiving 403 App attestation failed β€” and 429 once the per-project Debug Token exchange requests per minute quota (60/min, not adjustable) saturates.

This is the defect reported in discussion #7518 (open since 2023-12-19) and on StackOverflow 77673428. Every report there assumes a race condition. It is not a race β€” it is deterministic, and it is the line order the config plugin emits.

Android is unaffected.

Root cause

The config plugin injects this into AppDelegate (plugin/src/ios/appDelegate.ts):

RNFBAppCheckModule.sharedInstance()
    FirebaseApp.configure()

The resulting sequence:

  1. +[RNFBAppCheckModule sharedInstance] calls [FIRAppCheck setAppCheckProviderFactory:] with a freshly allocated RNFBAppCheckProviderFactory whose providers dictionary is nil (RNFBAppCheckModule.m:36-45).
  2. FirebaseApp.configure() runs [app.container instantiateEagerComponents] (FIRApp.m).
  3. FIRAppCheckInterop is registered with FIRInstantiationTimingAlwaysEager (FIRAppCheckComponent.m), so its creation block runs inside configure().
  4. [[FIRAppCheck alloc] initWithApp:] finds a non-nil factory and calls createProviderWithApp:. With providers[app.name] == nil, the factory installs providerName:@"debug" debugToken:nil (RNFBAppCheckProviderFactory.m:34-39) β†’ FIRAppCheckDebugProvider.

FIRAppCheckDebugProvider with no registered token generates a random one and exchanges it. Hence exchangeDebugToken traffic from release builds.

The line order is the whole mechanism. -[FIRAppCheck initWithApp:] returns nil when no factory is registered β€” it fails safely and does not crash. Had FirebaseApp.configure() run first, no App Check instance would exist at that point and the JS layer's later configureProvider call would install the correct provider as the first and only one.

No other Firebase product can be the creator on this ordering: a successfully created instance is cached by FIRComponentContainer (*isCacheable = YES), so later consumers (FIRAuth, Messaging, Installations) receive the already-built instance and cannot re-trigger provider selection.

Why it appears intermittent (~20%, not 100%)

The debug provider is installed on every cold start, but an exchange only happens when both hold: the eager auto-refresh reaches the provider before JS configureProvider swaps the delegate, and no live keychain-cached token exists. That timing dependence is why everyone in #7518 concluded "race" β€” the exchange races, but the misconfiguration is deterministic.

This also explains @brianGammon's finding in #7518 that isTokenAutoRefreshEnabled: false drops the 403 graph to ~0: it removes the eager refresh that reaches the wrongly-defaulted provider.

Android is immune by construction

ReactNativeFirebaseAppCheckProviderFactory.create() throws rather than defaulting to debug. Only iOS silently substitutes a debug provider.

Suggested fix

Make the pre-configure default safe outside DEBUG builds:

#if DEBUG
    NSString *defaultProviderName = @"debug";
#else
    NSString *defaultProviderName = @"appAttestWithDeviceCheckFallback";
#endif

This fixes the outcome regardless of who calls the factory or in what order, and keeps debug-token development working locally. appAttestWithDeviceCheckFallback already handles the availability fallback in RNFBAppCheckProvider.m.

A complete fix arguably needs both layers β€” the safe default and corrected ordering in the config plugin so the factory is not registered before FirebaseApp.configure(). I've opened a PR for the factory default only, since changing the plugin's emitted AppDelegate affects every consumer and I can't test that blast radius. Happy to follow up on the plugin if you'd prefer that approach.

The debug default was introduced in ee7df855 ("feat(app-check): add custom factory/provider", 2023-01-19) and is unchanged on main today.


Project Files

Javascript

Click To Expand

package.json:

{
  "dependencies": {
    "@react-native-firebase/app": "22.3.0",
    "@react-native-firebase/app-check": "22.3.0"
  }
}

firebase.json for react-native-firebase v6:

# N/A β€” Expo config plugin used, no firebase.json

iOS

Click To Expand

ios/Podfile:

  • I'm not using Pods
  • I'm using Pods and my Podfile looks like:
# Standard Expo-generated Podfile (expo prebuild), no App Check customisation.

AppDelegate.m:

// AppDelegate.swift, generated by `expo prebuild` β€” plugin-injected block verbatim:
//
// @generated begin @react-native-firebase/app-check - expo prebuild (DO NOT MODIFY)
RNFBAppCheckModule.sharedInstance()
    FirebaseApp.configure()
// @generated end @react-native-firebase/app-check
//
// Note the ordering: sharedInstance() registers the provider factory BEFORE
// FirebaseApp.configure() runs its eager components.


Android

Click To Expand

Have you converted to AndroidX?

  • my application is an AndroidX application?
  • I am using android/gradle.settings jetifier=true for Android compatibility?
  • I am using the NPM package jetifier for react-native compatibility?

android/build.gradle:

// N/A β€” Android is not affected; its factory throws instead of defaulting to debug.

android/app/build.gradle:

// N/A

android/settings.gradle:

// N/A

MainApplication.java:

// N/A

AndroidManifest.xml:

<!-- N/A -->


Environment

Click To Expand

react-native info output:

System:
  OS: macOS 26.5.2
  CPU: (14) arm64 Apple M4 Pro
  Memory: 7.30 GB / 48.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 24.16.0
    path: ~/.nvm/versions/node/v24.16.0/bin/node
  Yarn: Not Found
  npm:
    version: 8.19.4
    path: <project>/node_modules/.bin/npm
  Watchman:
    version: 2026.07.20.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.17.0
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.2
      - iOS 26.2
      - macOS 26.2
      - tvOS 26.2
      - visionOS 26.2
      - watchOS 26.2
  Android SDK:
    API Levels:
      - "35"
      - "36"
      - "36"
    Build Tools:
      - 34.0.0
      - 35.0.0
      - 36.0.0
      - 36.1.0
      - 36.1.0
    System Images:
      - android-35 | Google APIs ARM 64 v8a
      - android-36.1 | Google APIs ARM 64 v8a
      - android-36.1 | Google Play ARM 64 v8a
      - android-36 | Google Play ARM 64 v8a
    Android NDK: Not Found
IDEs:
  Android Studio: 2025.2 AI-252.28238.7.2523.14688667
  Xcode:
    version: 26.2/17C52
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.20
    path: /Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/javac
  Ruby:
    version: 4.0.6
    path: /opt/homebrew/bin/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.0.0
    wanted: 20.0.0
  react:
    installed: 19.1.0
    wanted: 19.1.0
  react-native:
    installed: 0.81.5
    wanted: 0.81.5
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

info React Native v0.86.2 is now available (your project is running on v0.81.5).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.86.2
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.81.5&to=0.86.2
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".
  • Platform that you're experiencing the issue on:
    • iOS
    • Android
    • iOS but have not tested behavior on Android
    • Android but have not tested behavior on iOS
    • Both
  • react-native-firebase version you're using that has this issue:
    • 22.3.0 (also present on main β€” the file is byte-identical)
  • Firebase module(s) you're using that has the issue:
    • App Check
  • Are you using TypeScript?
    • Y & 5.9.3

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions