Problem
After enabling Firebase App Check enforcement on Firestore, Storage, and RTDB, my Flutter iOS debug build fails to obtain an App Check token. All Firestore listeners fail with “Missing or insufficient permissions.” The Xcode console shows:
AppCheck failed: 'The operation couldn't be completed. The server responded with an error:
- URL:
- HTTP status code: 403
- Response body: {
"error": {
"code": 403,
"message": "App attestation failed.",
"status": "PERMISSION_DENIED"
}
}
Setup
- Flutter app using
firebase_app_checkpackage - iOS physical device, debug mode via Xcode
AppleDebugProvider()configured in Dart:
await FirebaseAppCheck.instance.activate(
providerApple: kDebugMode
? const AppleDebugProvider()
: const AppleAppAttestWithDeviceCheckFallbackProvider(),
);
- Firebase initialized programmatically via
firebase_options.dart(not usingGoogleService-Info.plistfor initialization) - Debug token correctly registered in Firebase console under the correct iOS app
- Firebase App Check API enabled in Google Cloud Console
- App Attest registered as attestation provider in Firebase console
- Token propagation waited 30+ minutes
- Android emulator works fine with its own debug token
What I tried (none of these fixed it)
- Verified the debug token UUID matched exactly between Xcode logs and Firebase console
- Confirmed the Firebase App Check API was enabled in GCP
- Waited over an hour for token propagation
- Added the App Attest entitlement to the Xcode project
- Configured DeviceCheck as an additional provider in Firebase console
- Set
AppCheckDebugProviderFactoryin nativeAppDelegate.swift
Root cause
The iOS API key (found in GoogleService-Info.plist under the API_KEY field, or in firebase_options.dart) has an iOS application restriction that only allows requests from a specific bundle ID. Every App Check token exchange request must include an X-Ios-Bundle-Identifier header.
The native Firebase iOS SDK reads the bundle ID from GoogleService-Info.plist in the app bundle. Because I used programmatic initialization via firebase_options.dart, the plist was never added to the Xcode build target. It existed on disk at ios/Runner/GoogleService-Info.plist but wasn’t referenced in project.pbxproj, so it was never copied into the app bundle.
Without the plist in the bundle, the SDK sent requests without a bundle ID. Firebase blocked them but returned the misleading “App attestation failed” error instead of the real reason.
How I confirmed this
I called the exchangeDebugToken endpoint directly with curl:
# Without bundle ID — reveals the REAL error
curl -s -X POST \
"?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"debugToken": "YOUR_DEBUG_TOKEN"}'
# Response:
# "Requests from this iOS client application are blocked."
# reason: API_KEY_IOS_APP_BLOCKED
# WITH bundle ID — succeeds
curl -s -X POST \
"?key=YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Ios-Bundle-Identifier: com.example.yourapp" \
-d '{"debugToken": "YOUR_DEBUG_TOKEN"}'
# Response: valid App Check token returned
Replace YOUR_PROJECT, YOUR_APP_ID, YOUR_API_KEY, and YOUR_DEBUG_TOKEN with values from your Firebase console and Xcode logs. YOUR_APP_ID is the full app ID (e.g., 1:123456789:ios:abcdef1234567890).
Fix
Two changes are needed:
1. Add GoogleService-Info.plist to the Xcode build target
The file likely already exists at ios/Runner/GoogleService-Info.plist — it just isn’t in the Xcode project. In Xcode:
- Drag
GoogleService-Info.plistinto the Runner group in the project navigator - Uncheck “Copy items if needed” (it’s already there)
- Check the Runner target
You can verify it’s missing by searching for it in project.pbxproj:
grep -c "GoogleService-Info.plist" ios/Runner.xcodeproj/project.pbxproj
# If result is 0, it's not in the build
2. Set AppCheckDebugProviderFactory in AppDelegate.swift
With the plist now in the bundle, the native Firebase SDK will auto-configure and default to DeviceCheck (or App Attest) instead of the debug provider. You must set the debug provider factory before Firebase configures:
import FirebaseCore
import FirebaseAppCheck
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let providerFactory = AppCheckDebugProviderFactory()
AppCheck.setAppCheckProviderFactory(providerFactory)
// ... rest of your setup
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Note: You might see examples using
#if DEBUGaround the factory setup. Be aware that the Runner target in Flutter projects may not haveDEBUGin itsSWIFT_ACTIVE_COMPILATION_CONDITIONSbuild setting (only RunnerTests does). Check your build settings before relying on#if DEBUG, or omit the guard — the Dart-sideactivate()overrides the provider for release builds anyway.
After both changes, do a clean build (flutter clean then flutter pub get then cd ios && pod install) and run from Xcode.
TL;DR
If you use programmatic Firebase init in Flutter (firebase_options.dart) and your iOS API key has application restrictions, App Check debug token exchange silently fails with a misleading “App attestation failed” error. The fix: add GoogleService-Info.plist to your Xcode build target so the native SDK can send the bundle ID with requests, and set AppCheckDebugProviderFactory in AppDelegate.swift before plugin registration.