| name | firebase-appcheck-manager |
| description | Use when managing Firebase App Check via CLI. Covers enabling the API, registering Android/iOS attestation providers (Play Integrity, App Attest), managing SHA-256 fingerprints, enforcing services, and managing debug tokens. Triggers on keywords like "App Check", "Play Integrity", "App Attest", "debug token", "attestation", "enforce Firebase". |
Firebase App Check Manager
โ ๏ธ #1 ROOT CAUSE of "App attestation failed": Missing Team ID or API not enabled
Two things MUST be in place for iOS App Check to work:
firebaseappcheck.googleapis.com API must be ENABLED โ even if you've configured providers in Console, if this API isn't enabled, all token exchanges fail silently.
- iOS App must have
teamId set โ Without Team ID, App Attest cannot verify with Apple's servers.
Team ID can be set via REST API (no Console UI needed):
curl -X PATCH -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d '{"teamId":"YOUR_TEAM_ID"}' \
"https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/iosApps/${IOS_APP_ID}?updateMask=teamId"
If Team ID is already set and API is enabled, verify app registration through Firebase Console โ App Check โ ๆ็จ็จๅผ tab.
Required IAM role: roles/editor or roles/owner (not just roles/firebase.viewer or roles/firebaseappcheck.admin).
Manage Firebase App Check configuration entirely via CLI (gcloud + REST API). No Firebase Console UI needed for most steps, except app registration (see Step 0).
Concepts: How App Check Works
App Check is a server-side gatekeeper that protects your Firebase backend services from abuse. It works via a 3-step process:
โโโโโโโโโโโโโโโ โ Attestation โโโโโโโโโโโโโโโโโโโโโโ
โ Your App โ โโโโโโโโโโโโโโโโโโโโโโโ โ Attestation โ
โ (Flutter) โ โ Provider โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โ (Play Integrity / โ
โ โ โก Attestation Token โ App Attest) โ
โ โ โโโโโโโโโโโโโโโโโโโโโโ
โ โ โข Exchange for
โ โ App Check Token โโโโโโโโโโโโโโโโโโโโโโ
โ โ โโโโโโโโโโโโโโโโโโโโโโโ โ Firebase โ
โ โ โ App Check Server โ
โ โ โโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โฃ App Check Token โโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ โ โค API call with โโโโโโโโโโโโโโโโโโโโโโ
โ โ App Check Token โ Firebase Service โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโ โ (AI, Firestore, โ
โ โ โ Storage, etc.) โ
โ โ โฅ Response โโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโ (or 403 if invalid)
Critical understanding:
- The
activate() call in Flutter just tells the SDK to start sending tokens
- Without enforcement on the server side, those tokens are ignored โ ANY request is accepted
- Enforcement = server rejects requests without valid App Check tokens
- You must enable enforcement per Firebase service independently
Android: Three Signing Keys
Android apps have three different signing keys. All three produce different SHA-256 fingerprints. All three must be registered in Firebase for App Check to work across dev, testing, and production:
| Key | When Used | Where to Find |
|---|
| Debug Key | Local flutter run / flutter build --debug | cd android && ./gradlew signingReport |
| Upload Key | Your local .jks keystore used for flutter build appbundle --release | Same signingReport or keytool -list -keystore your.jks |
| App Signing Key | Google Play Console re-signs your AAB with this key before distributing to users | Google Play Console โ Release โ Setup โ App signing |
โ ๏ธ The #1 mistake: Registering only the debug or upload key. Production users receive the app signed with the App Signing Key which is different โ their App Check attestation will fail silently.
iOS: App Attest vs DeviceCheck
| Provider | iOS Version | Strength | Recommendation |
|---|
AppleAppAttestProvider | iOS 14.0+ | Strong (hardware-backed) | โ ๏ธ ~25% failure rate in production (FlutterFire #10683) |
AppleDeviceCheckProvider | iOS 11.0+ | Medium | Standalone fallback only |
AppleAppAttestProviderWithDeviceCheckFallback | All | Adaptive | โ
RECOMMENDED for production |
โ ๏ธ CRITICAL: Always use appAttestWithDeviceCheckFallback in production, NOT appAttest alone.
App Attest depends on Apple's attestation server. Even on iOS 14+ real devices, ExchangeAppAttestAttestation has a ~25% failure rate due to:
- Apple attestation server transient failures
- First-time attestation timing issues on fresh installs
- Network conditions during attestation handshake
- Device state edge cases (not jailbreak related)
appAttestWithDeviceCheckFallback tries App Attest first, and automatically falls back to DeviceCheck when App Attest fails. Both are Apple official attestation mechanisms with equivalent security for App Check purposes. This ensures near-100% token acquisition rate.
Firebase Console must have both App Attest AND DeviceCheck providers registered for the iOS app.
iOS entitlement required: You MUST add com.apple.developer.devicecheck.appattest-environment to your iOS entitlements for App Attest to work in production builds.
Prerequisites
gcloud CLI authenticated with the correct account (gcloud auth list / gcloud config set account EMAIL)
- Owner/Editor role on the Firebase project
- Know your Firebase Project ID and App IDs (
firebase apps:list --project=PROJECT_ID)
Common Header Pattern
All REST API calls need these headers:
ACCESS_TOKEN=$(gcloud auth print-access-token)
PROJECT_ID="your-project-id"
ANDROID_APP_ID="1:xxxx:android:xxxx"
IOS_APP_ID="1:xxxx:ios:xxxx"
PROJECT_NUMBER="123456789"
-H "Authorization: Bearer $ACCESS_TOKEN"
-H "x-goog-user-project: ${PROJECT_ID}"
-H "Content-Type: application/json"
Important: The x-goog-user-project header is required for quota billing. Without it you get SERVICE_DISABLED errors.
Quick Audit Script (One-Shot Health Check)
Run this script to get a full status of App Check for any Firebase project. Copy/paste the entire block:
#!/bin/bash
ACCESS_TOKEN=$(gcloud auth print-access-token)
PROJECT_ID="your-project-id"
PROJECT_NUMBER="123456789"
ANDROID_APP_ID="1:xxxx:android:xxxx"
IOS_APP_ID="1:xxxx:ios:xxxx"
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
echo "โ Firebase App Check Audit: $PROJECT_ID โ"
echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"
echo ""
echo "โโ 1. API Enablement โโ"
AC=$(gcloud services list --enabled --filter="config.name:firebaseappcheck" --project=$PROJECT_ID --format="value(config.name)" 2>/dev/null)
PI=$(gcloud services list --enabled --filter="config.name:playintegrity" --project=$PROJECT_ID --format="value(config.name)" 2>/dev/null)
[ -n "$AC" ] && echo " โ
firebaseappcheck.googleapis.com ENABLED" || echo " โ firebaseappcheck.googleapis.com NOT ENABLED"
[ -n "$PI" ] && echo " โ
playintegrity.googleapis.com ENABLED" || echo " โ playintegrity.googleapis.com NOT ENABLED"
echo ""
echo "โโ 2. Services Enforcement โโ"
SERVICES=$(curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/services")
if echo "$SERVICES" | python3 -c "import sys,json; d=json.load(sys.stdin); svcs=d.get('services',[]); [print(f' {s[\"name\"].split(\"/\")[-1]}: {s.get(\"enforcementMode\",\"UNENFORCED\")}') for s in svcs]" 2>/dev/null; then
:
else
echo " โ ๏ธ No services configured or empty response"
fi
echo ""
echo "โโ 3. Android Play Integrity Config โโ"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/apps/${ANDROID_APP_ID}/playIntegrityConfig" | python3 -c "
import sys,json
d=json.load(sys.stdin)
ttl=d.get('tokenTtl','?')
lvl=d.get('deviceIntegrity',{}).get('minDeviceRecognitionLevel','?')
print(f' Token TTL: {ttl}')
print(f' Min Device Recognition: {lvl}')
if lvl == 'NO_INTEGRITY':
print(' โ ๏ธ NO_INTEGRITY allows any device (including emulators)')
" 2>/dev/null
echo ""
echo "โโ 4. iOS App Attest Config โโ"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/apps/${IOS_APP_ID}/appAttestConfig" | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(f' Token TTL: {d.get(\"tokenTtl\",\"?\")}')
" 2>/dev/null
echo ""
echo "โโ 4b. iOS App Team ID โโ"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/iosApps/${IOS_APP_ID}" | python3 -c "
import sys,json
d=json.load(sys.stdin)
tid=d.get('teamId','')
if tid:
print(f' โ
Team ID: {tid}')
else:
print(' โ Team ID: NOT SET โ App Attest WILL FAIL!')
print(' Fix: curl -X PATCH ... -d \'{\"teamId\":\"YOUR_TEAM_ID\"}\' .../iosApps/APP_ID?updateMask=teamId')
" 2>/dev/null
echo ""
echo "โโ 5. Android SHA Fingerprints โโ"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/androidApps/${ANDROID_APP_ID}/sha" | python3 -c "
import sys,json
d=json.load(sys.stdin)
certs=d.get('certificates',[])
sha1 = [c for c in certs if c.get('certType')=='SHA_1']
sha256 = [c for c in certs if c.get('certType')=='SHA_256']
print(f' Total: {len(certs)} ({len(sha1)} SHA-1, {len(sha256)} SHA-256)')
for cert in certs:
print(f' - {cert.get(\"certType\",\"?\"):8s} : {cert.get(\"shaHash\",\"?\")}')
if len(sha256) < 3:
print(f' โ ๏ธ Only {len(sha256)} SHA-256 fingerprints. Need 3: debug + upload + app-signing')
print(f' App Check requires SHA-256, not SHA-1')
" 2>/dev/null
echo ""
echo "โโ 6. Debug Tokens โโ"
echo " Android:"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_ID}/apps/${ANDROID_APP_ID}/debugTokens" | python3 -c "
import sys,json
d=json.load(sys.stdin)
tokens=d.get('debugTokens',[])
print(f' Count: {len(tokens)}')
for t in tokens:
print(f' - {t.get(\"displayName\",\"unnamed\")} ({t[\"name\"].split(\"/\")[-1]})')
" 2>/dev/null
echo " iOS:"
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_ID}/apps/${IOS_APP_ID}/debugTokens" | python3 -c "
import sys,json
d=json.load(sys.stdin)
tokens=d.get('debugTokens',[])
print(f' Count: {len(tokens)}')
for t in tokens:
print(f' - {t.get(\"displayName\",\"unnamed\")} ({t[\"name\"].split(\"/\")[-1]})')
" 2>/dev/null
echo ""
echo "โโ Audit Complete โโ"
Step-by-Step Setup
Step 0: Register App for App Check
Option A: Via REST API / CLI (Preferred)
You can now configure App Check entirely via CLI:
1. Set iOS Team ID (required for App Attest):
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/iosApps/${IOS_APP_ID}" | python3 -c "
import sys,json; d=json.load(sys.stdin); print(f'teamId: {d.get(\"teamId\",\"NOT SET\")}')"
curl -X PATCH -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d '{"teamId":"YOUR_APPLE_TEAM_ID"}' \
"https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/iosApps/${IOS_APP_ID}?updateMask=teamId"
2. Configure App Attest (iOS):
curl -X PATCH -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d '{"tokenTtl":"3600s"}' \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/apps/${IOS_APP_ID}/appAttestConfig?updateMask=tokenTtl"
3. Configure DeviceCheck (iOS fallback):
curl -X PATCH -H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d '{"tokenTtl":"3600s"}' \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/apps/${IOS_APP_ID}/deviceCheckConfig?updateMask=tokenTtl"
Option B: Via Firebase Console UI
If the REST API approach doesn't fully register the app (status still shows "ๆช่จปๅ"):
- Go to Firebase Console โ your project โ App Check โ ๆ็จ็จๅผ (Apps) tab
- If the app's status shows "ๆช่จปๅ" (Not registered), click the app row to expand it
- Select the attestation provider:
- iOS: Select App Attest (and optionally DeviceCheck for fallback)
- Android: Select Play Integrity
- For iOS: Fill in your Apple Team ID (e.g.
<YOUR_TEAM_ID> (10-character alphanumeric) โ find it at Apple Developer โ Membership)
- Set the token TTL (default 1 hour is fine for most apps)
- Click Save
- Verify the status changes from "ๆช่จปๅ" to "ๅทฒ่จปๅ" (Registered)
- Repeat for each app (iOS, Android) that needs App Check
IAM Role Requirement: This operation requires roles/editor or roles/owner on the Firebase project. roles/firebase.viewer or even roles/firebaseappcheck.admin is NOT sufficient. Grant the required role with:
gcloud projects add-iam-policy-binding PROJECT_ID --member="user:EMAIL" --role="roles/editor"
Step 1: Enable Required APIs
gcloud services enable firebaseappcheck.googleapis.com --project=${PROJECT_ID}
gcloud services enable playintegrity.googleapis.com --project=${PROJECT_ID}
Step 2: Register Android SHA-256 Fingerprints
You need three SHA-256 fingerprints for Android:
| Key Type | Where to Find |
|---|
| Debug Key | ./gradlew signingReport in android/ |
| Upload Key | Your local release .jks file (also in signingReport) |
| App Signing Key | Google Play Console โ Release โ App Integrity โ App Signing |
List Current Fingerprints
curl -s \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/androidApps/${ANDROID_APP_ID}/sha"
Add a SHA-256 Fingerprint
Convert the colon-separated format to lowercase hex (remove colons):
64:50:0E:EF:... โ 64500eef...
curl -s -X POST \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d '{"shaHash":"LOWERCASE_HEX_NO_COLONS","certType":"SHA_256"}' \
"https://firebase.googleapis.com/v1beta1/projects/${PROJECT_ID}/androidApps/${ANDROID_APP_ID}/sha"
Getting All 3 SHA-256 Fingerprints
โ ๏ธ ./gradlew signingReport prerequisite: This command requires Flutter plugin cache to exist. Run flutter pub get first, otherwise Gradle fails with Plugin directory does not exist.
โ ๏ธ keytool locale issue: On non-English systems (e.g. Chinese locale), keytool -list -v outputs labels in the local language (e.g. ๆ่ญๆ็ด instead of Certificate fingerprints). Use grep SHA or pipe through python instead of grep SHA-256.
Recommended: Direct keytool approach (locale-agnostic, no Gradle needed)
keytool -list -v -keystore ~/.android/debug.keystore -storepass android 2>&1 | head -30
keytool -list -v -keystore android/app/upload-keystore.jks -storepass YOUR_PASSWORD 2>&1 | head -30
Alternative: Gradle signingReport (requires pub get first)
flutter pub get
cd android && ./gradlew signingReport 2>/dev/null | head -60
App Signing key (from Google Play Console)
โ ๏ธ The Google Play Developer REST API does NOT expose the App Signing Key. Endpoints like /appSigningKey and /appIntegrity return 404. You MUST use the Play Console UI or Playwright.
Option A: Playwright (automated)
Navigate to https://play.google.com/console/u/0/developers/{DEVELOPER_ID}/app-list, then:
- Click on your app
- Sidebar: "Test and release" (ๆธฌ่ฉฆ่็ผๅธ) โ "App integrity" (ๆ็จ็จๅผๅฎๆดๆง)
- Click "Play App Signing" (Play ๆ็จ็จๅผ็ฐฝ็ฝฒ) setup
- Find "App signing key certificate" section โ SHA-256 fingerprint
Option B: Play Console UI (manual)
Play Console โ Your App โ Release โ Setup โ App integrity โ App signing key certificate โ SHA-256
Convert and register:
echo "64:50:0E:EF:..." | tr -d ':' | tr 'A-F' 'a-f'
Step 3: iOS App Attest Entitlement
CRITICAL: The iOS app must have the App Attest entitlement. Without it, App Attest silently fails on release builds and all iOS requests are rejected when services are ENFORCED.
Check if entitlement exists:
grep -r "appattest" ios/ --include="*.entitlements" 2>/dev/null || echo "โ No App Attest entitlement found!"
Add the entitlement:
Option A: Create/update entitlements file manually:
Create ios/Runner/Runner.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.devicecheck.appattest-environment</key>
<string>production</string>
</dict>
</plist>
Then add it to Xcode project via xcodeproj gem:
require 'xcodeproj'
project = Xcodeproj::Project.open('ios/Runner.xcodeproj')
target = project.targets.find { |t| t.name == 'Runner' }
target.build_configurations.each do |config|
config.build_settings['CODE_SIGN_ENTITLEMENTS'] = 'Runner/Runner.entitlements'
end
project.save
Option B: Use Xcode โ Runner target โ Signing & Capabilities โ + Capability โ App Attest
Step 3b: iOS DeviceCheck Private Key (REQUIRED for fallback)
CRITICAL: If you use appAttestWithDeviceCheckFallback (recommended), you must upload an Apple DeviceCheck private key (.p8) to Firebase Console. Without this key, when App Attest fails (~25% of the time), the DeviceCheck fallback also fails because Firebase has no way to validate DeviceCheck tokens. The result: you still get the same ~25% failure rate as appAttest alone.
Create the DeviceCheck Key
- Go to Apple Developer Console โ Keys
- Click + to create a new key
- Give it a name (e.g.
Firebase DeviceCheck)
- Check DeviceCheck capability (NOT "App Store Connect API" โ that is a different key type)
- Click Continue โ Register
- Download the
.p8 file (e.g. AuthKey_ABCD1234.p8) โ you can only download it once
- Note the Key ID shown on the confirmation page
Warning: An App Store Connect API key (.p8) does NOT work for DeviceCheck. You must create a key specifically with the DeviceCheck capability checked. They are different key types even though both produce .p8 files.
Upload to Firebase via REST API
ACCESS_TOKEN=$(gcloud auth print-access-token)
PROJECT_ID="your-project-id"
IOS_APP_ID="1:xxxx:ios:xxxx"
PRIVATE_KEY=$(cat path/to/AuthKey_KEYID.p8 | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
curl -s -X PATCH \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d "{\"keyId\":\"YOUR_KEY_ID\",\"privateKey\":${PRIVATE_KEY},\"tokenTtl\":\"3600s\"}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_ID}/apps/${IOS_APP_ID}/deviceCheckConfig?updateMask=keyId,privateKey,tokenTtl"
Replace YOUR_KEY_ID with the Key ID from Apple Developer Console (e.g. ABCD1234).
Notes
Step 4: Check Provider Configs
Android (Play Integrity)
curl -s \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/apps/${ANDROID_APP_ID}/playIntegrityConfig"
iOS (App Attest)
curl -s \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/apps/${IOS_APP_ID}/appAttestConfig"
Step 5: Enforce Services
List All Services and Their Enforcement Status
curl -s \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/services"
Set Service to ENFORCED
curl -s -X PATCH \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d '{"enforcementMode":"ENFORCED"}' \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/services/SERVICE_NAME?updateMask=enforcementMode"
Common Firebase Service Names
| Service | API Name |
|---|
| Firebase AI Logic (Gemini) | firebaseml.googleapis.com |
| Cloud Firestore | firestore.googleapis.com |
| Firebase Storage | firebasestorage.googleapis.com |
| Authentication | identitytoolkit.googleapis.com |
| Data Connect | firebasedataconnect.googleapis.com |
| Realtime Database | firebasedatabase.googleapis.com |
Batch Enforce Multiple Services
for SERVICE in firebaseml.googleapis.com firebasestorage.googleapis.com identitytoolkit.googleapis.com; do
echo "Enforcing $SERVICE..."
curl -s -X PATCH \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d '{"enforcementMode":"ENFORCED"}' \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_NUMBER}/services/${SERVICE}?updateMask=enforcementMode"
echo ""
done
Step 6: Debug Tokens
Debug tokens allow debug/CI builds to pass App Check when services are ENFORCED.
Generate and Register a Debug Token
DEBUG_TOKEN=$(python3 -c "import uuid; print(str(uuid.uuid4()))")
echo "Debug token: $DEBUG_TOKEN"
curl -s -X POST \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d "{\"displayName\":\"Local Dev Mac\",\"token\":\"${DEBUG_TOKEN}\"}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_ID}/apps/${ANDROID_APP_ID}/debugTokens"
curl -s -X POST \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
-H "Content-Type: application/json" \
-d "{\"displayName\":\"Local Dev Mac\",\"token\":\"${DEBUG_TOKEN}\"}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_ID}/apps/${IOS_APP_ID}/debugTokens"
List Debug Tokens
curl -s \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/projects/${PROJECT_ID}/apps/${ANDROID_APP_ID}/debugTokens"
Delete a Debug Token
curl -s -X DELETE \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "x-goog-user-project: ${PROJECT_ID}" \
"https://firebaseappcheck.googleapis.com/v1/TOKEN_RESOURCE_NAME"
Flutter Code Integration
IMPORTANT: Check your firebase_core version first to determine Gen 3 vs Gen 4. The API is different.
Gen 4 (firebase_core ^4.4.0, firebase_app_check ^0.4.x)
dependencies:
firebase_app_check: ^0.4.1+4
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:flutter/foundation.dart';
// After Firebase.initializeApp()
// kDebugMode is a compile-time constant โ Dart tree-shakes the entire
// debug branch from release builds, so the token never leaks into production.
await FirebaseAppCheck.instance.activate(
// Production: hardware-backed attestation
// Debug: debug provider with pre-registered token (named param!)
providerAndroid: kDebugMode
? const AndroidDebugProvider(debugToken: 'your-registered-uuid')
: const AndroidPlayIntegrityProvider(),
providerApple: kDebugMode
? const AppleDebugProvider(debugToken: 'your-registered-uuid')
: const AppleAppAttestWithDeviceCheckFallbackProvider(), // MUST use fallback โ appAttest alone has ~25% failure rate
);
Gen 3 (firebase_core ^3.x, firebase_app_check ^0.3.x)
dependencies:
firebase_app_check: ^0.3.2+10
import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:flutter/foundation.dart';
// After Firebase.initializeApp()
await FirebaseAppCheck.instance.activate(
// Gen 3 uses enum values, not class constructors
appleProvider: kDebugMode
? AppleProvider.debug
: AppleProvider.appAttestWithDeviceCheckFallback, // MUST use fallback โ appAttest alone has ~25% failure rate
androidProvider: kDebugMode
? AndroidProvider.debug
: AndroidProvider.playIntegrity,
);
Sideloaded Release Builds (USE_DEBUG_APPCHECK pattern)
โ ๏ธ PlayIntegrity inherently FAILS on sideloaded release APKs โ it only works for apps installed from Google Play.
This means adb install app-release.apk will always fail PlayIntegrity attestation, even with correct SHA-256 fingerprints registered.
The fix is a --dart-define flag that lets you control the provider independently of kDebugMode:
Flutter code (Gen 3):
// USE_DEBUG_APPCHECK=true in .env โ forces debug provider even on release builds
const useDebugAppCheck = bool.fromEnvironment('USE_DEBUG_APPCHECK');
await FirebaseAppCheck.instance.activate(
androidProvider: (kDebugMode || useDebugAppCheck)
? AndroidProvider.debug
: AndroidProvider.playIntegrity,
appleProvider: (kDebugMode || useDebugAppCheck)
? AppleProvider.debug
: AppleProvider.appAttestWithDeviceCheckFallback,
);
Dual .env file pattern:
| File | Usage | USE_DEBUG_APPCHECK | Build Command |
|---|
.env | Local dev + sideloaded release testing | true | flutter build apk --release --dart-define-from-file=.env |
.env.prod | Play Store / App Store builds | absent (false) | flutter build appbundle --release --dart-define-from-file=.env.prod |
Key: Debug provider still requires a registered debug token. Each fresh app install generates a new token (check logcat). Register it via the Debug Tokens REST API.
Fresh install = new debug token: When you uninstall + reinstall a sideloaded release APK, the debug provider generates a NEW UUID. You must register it again. The old token becomes invalid.
Common Gen 3 mistake: Using AppleProvider.appAttest alone. It has ~25% failure rate in production. Always use AppleProvider.appAttestWithDeviceCheckFallback.
OPTIONAL: Verify token in debug mode (both Gen 3 & 4)
if (kDebugMode) {
FirebaseAppCheck.instance
.getToken(true)
.timeout(const Duration(seconds: 10))
.then((token) {
debugPrint('[AppCheck] getToken ok=${token != null}');
})
.catchError((Object e) {
debugPrint('[AppCheck] getToken failed: $e');
});
}
Firebase AI Integration
Gen 4 (firebase_ai ^3.7.0) โ MUST pass appCheck explicitly
final model = FirebaseAI.googleAI(
appCheck: FirebaseAppCheck.instance, // Required in Gen 4!
).generativeModel(
model: 'gemini-2.5-flash-lite',
// ...
);
Without appCheck: FirebaseAppCheck.instance, the SDK won't attach App Check tokens to requests and firebasevertexai.googleapis.com will reject them.
Gen 3 (firebase_ai ^2.x) โ MUST also pass appCheck explicitly
// Gen 3: FirebaseAI.googleAI() ALSO accepts and REQUIRES appCheck parameter
// Without it, App Check tokens are NOT attached to AI requests
// This causes "Firebase App Check token is invalid" errors when firebaseml is ENFORCED
final model = FirebaseAI.googleAI(
appCheck: FirebaseAppCheck.instance, // REQUIRED! Not auto-attached in Gen 3
).generativeModel(
model: 'gemini-2.5-flash-lite',
// ...
);
โ ๏ธ Critical Gen 3 Gotcha: The appCheck parameter is optional in FirebaseAI.googleAI() โ if omitted, it defaults to null and no App Check token will be attached to AI requests. When firebaseml.googleapis.com is ENFORCED, this causes all requests to fail with "Firebase App Check token is invalid". The getToken() call may succeed locally, but the token is never sent to the server.
NOTE: firebasevertexai.googleapis.com ALWAYS requires valid App Check tokens โ it cannot be unenforced via App Check service configuration (both Gen 3 and Gen 4).
Complete Setup Checklist
Server-Side (Firebase Console / CLI)
Client-Side (Flutter)
Firebase Package Version Compatibility
All Firebase packages must be on the same generation. Mixing versions causes dependency conflicts.
| Generation | firebase_core | firebase_app_check | firebase_ai | firebase_auth | firebase_crashlytics | firebase_analytics |
|---|
| Gen 3 (OLD) | ^3.12.x | ^0.3.x | ^2.x | ^5.x | ^4.x | ^11.x |
| Gen 4 (CURRENT) | ^4.4.0 | ^0.4.x | ^3.7.0 | ^6.x | ^5.x | ^12.x |
If you upgrade ONE Firebase package, you likely need to upgrade ALL of them.
Common Mistakes
| Mistake | Fix |
|---|
403 SERVICE_DISABLED | Add x-goog-user-project header OR enable the API |
| iOS App Attest silently fails | Missing com.apple.developer.devicecheck.appattest-environment in entitlements |
iOS App Attest 403 / com.firebase.appCheck error 0 intermittently | App Attest has ~25% failure rate (FlutterFire #10683). Switch from AppleProvider.appAttest to AppleProvider.appAttestWithDeviceCheckFallback. Ensure both App Attest AND DeviceCheck providers are registered in Firebase Console |
| Wrong gcloud account | gcloud auth list then gcloud config set account EMAIL |
| Play Integrity not working in production | Missing App Signing Key SHA-256 (only debug/upload registered) |
| Play Integrity not working at all | playintegrity.googleapis.com not enabled |
| Debug build rejected | No debug token registered, or token mismatch |
| REST API uses project number | Services endpoint uses projects/PROJECT_NUMBER, not project ID |
| SHA-256 format wrong | Must be lowercase hex without colons |
| "Firebase App Check token is invalid" with Firebase AI | Gen 4: Must pass appCheck: FirebaseAppCheck.instance to FirebaseAI.googleAI() + use ^0.4.x provider API. Gen 3: Ensure activate() is called before any AI request โ tokens auto-attach. Both: firebasevertexai.googleapis.com ALWAYS requires valid App Check |
Old API AndroidProvider.debug not working | Upgrade to firebase_app_check ^0.4.x and use AndroidDebugProvider() instead |
firebase_app_check ^0.4.x won't resolve | Requires firebase_core ^4.4.0 โ upgrade all Firebase packages together |
iOS build fails PhaseScriptExecution after Crashlytics 5.x upgrade | Old [firebase_crashlytics] Upload dSYM script phase uses ${GOOGLE_APP_ID} build setting which Gen 4 no longer injects. Remove the old script phase from project.pbxproj โ the FlutterFire: "flutterfire upload-crashlytics-symbols" script phase replaces it |
activate() called but services still accept unauthenticated requests | Client-side activate() only sends tokens. You must enforce each service server-side via REST API or Firebase Console |
| App works in debug but fails in production on Android | Register the App Signing Key SHA-256 from Play Console (different from your local upload key) |
| SHA-1 registered but Play Integrity still fails | App Check requires SHA-256 fingerprints, not SHA-1. Many projects have SHA-1 from google-services.json setup. Check with audit script โ it shows SHA-1 vs SHA-256 counts separately |
Release APK sideloaded via adb install gets 401/403 | PlayIntegrity ONLY works for Play Store installed apps. Use USE_DEBUG_APPCHECK=true dart-define + debug provider for sideloaded release testing (see Sideloaded Release Builds section) |
| Sideloaded release worked before reinstall, now fails | Each fresh install generates a new debug token. Check logcat for the new UUID and register it via REST API |
| Cloud Function returns 401 but Firestore/Auth work | Cloud Functions with manual admin.appCheck().verifyToken() enforcement reject invalid tokens independently of Firebase service enforcement. Debug provider must produce a valid token (registered in Firebase) |
| Tried Gen 4 API but classes not found | Check pubspec.lock for actual firebase_app_check version. If 0.3.x, you're on Gen 3 โ use AndroidProvider.debug enum, not AndroidDebugProvider() class |
gradlew signingReport fails with "Plugin directory does not exist" | Run flutter pub get first โ Gradle needs the Flutter plugin cache to resolve the project |
keytool SHA-256 grep returns empty on Chinese/Japanese locale | keytool -list -v outputs localized labels (e.g. ๆ่ญๆ็ด not Certificate fingerprints). Use head -30 instead of grep SHA-256, or pipe through python |
Enforcement fails with serviceusage.serviceUsageConsumer permission error | gcloud config get-value account may show a different account than expected. Some API calls (SHA registration, provider config) succeed with any project member, but enforcement requires Owner/Editor. Run gcloud config set account CORRECT_EMAIL before enforcing |
| DeviceCheck fallback still fails after configuring provider | Missing Apple DeviceCheck private key in Firebase Console. Must create a key with DeviceCheck capability in Apple Developer Console โ Keys, download .p8, and upload via REST API. ASC API keys do NOT have DeviceCheck capability. |
| Entitlements file created but Xcode doesn't use it | Must add CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements to all Runner build configs in project.pbxproj. Use Python regex or xcodeproj gem โ search for PRODUCT_BUNDLE_IDENTIFIER = com.your.app; as anchor point |
| "App attestation failed" even though REST API configs exist | App not registered in Firebase Console UI. REST API appAttestConfig/deviceCheckConfig create configs but do NOT register the app. Must go to Firebase Console โ App Check โ ๆ็จ็จๅผ โ expand app โ select provider โ fill Team ID โ Save. Requires roles/editor IAM role. |
Related skills
firebase-flutter-setup โ firebase-appcheck-manager โ complete Firebase setup before configuring App Check.
firebase-ai-logic โ use alongside firebase-appcheck-manager for secure AI Logic deployments; AppCheck attestation protects backend API calls.