Repository navigation
feat(appcheck): support seamless local CLI debug token discovery - #16994
AustinBenoit wants to merge 1 commit into
Conversation
Resolve App Check debug tokens from FIREBASE_APPCHECK_DEBUG_TOKEN, the local Firebase CLI configuration (~/.config/configstore/firebase-appcheck.json) on simulators and macOS, and build-time injection via FirebaseAppCheckDebugTokenPlugin prior to falling back to NSUserDefaults or generating a new UUID. Log token source attribution and actionable CLI recovery hints on HTTP 403 server rejections.
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. |
There was a problem hiding this comment.
Thanks for putting this together. Reading the token from the CLI config on simulators and macOS is a nice improvement to the debug workflow. I put fixes for the comments below in #17003, which is based on this branch, so it can be merged into it or cherry-picked.
The main issues:
- The build plugin doesn't work for clients. It never finds its script, so it always generates an empty stub. Rotated tokens aren't picked up, and the script embeds every token in the CLI config.
- CI tokens can leak into logs when a token exchange fails.
- The
setenvhandoff to AppCheckCore isn't thread-safe. #17003 narrows it, but AppCheckCore should accept the token directly. - CI failures. The Catalyst jobs fail because of unguarded config-file tests and leftover
GULUserDefaultsstate.checkmost likely fails on swiftformat line length.
One thing that isn't visible in the diff: the currentDebugToken priority list in FIRAppCheckDebugProvider.h is now out of date. #17003 updates it.
| let scriptURL = context.package.directoryURL | ||
| .appending(path: "FirebaseAppCheck/Scripts/inject_debug_token.sh") |
There was a problem hiding this comment.
context.package is the package of the target the plugin is applied to. For clients, that's their own package (and xcodeProject.directoryURL on L106 is their project), so this script is never found outside this repo and the inline fallback below always generates an empty stub.
I confirmed this with a minimal client package: tokens=[] even with a valid CLI config. Copying the script into the client's FirebaseAppCheck/Scripts/ made it work. #17003 embeds the generator in the plugin instead.
|
|
||
| var plistURL: URL? | ||
| if let sourceTarget = target as? SourceModuleTarget { | ||
| for file in sourceTarget.sourceFiles where file.url.lastPathComponent == "GoogleService-Info.plist" { |
There was a problem hiding this comment.
This line and L115 are over swiftformat's 100-column limit, which is likely why the check job fails.
| inputFiles: inputFiles, | ||
| outputFiles: [outputURL] |
There was a problem hiding this comment.
The CLI config file isn't an input, so after a token is rotated (for example, after the 403 hint's --force) the build keeps the old token until a clean build. I reproduced this. #17003 uses a prebuild command that runs on every build and rewrites the file only when it changes. With xcodebuild, a rotated, removed, or newly created token is picked up by the next incremental build.
| if [ -n "${CONFIGURATION:-}" ]; then | ||
| case "$CONFIGURATION" in | ||
| *Debug* | *debug* | *DEBUG*) | ||
| ;; | ||
| *) | ||
| emit_empty_stub | ||
| exit 0 | ||
| ;; | ||
| esac | ||
| fi |
There was a problem hiding this comment.
Build tool plugin commands don't get Xcode build settings like CONFIGURATION, so this check never runs under the plugin. Once the script path issue is fixed, a -c release build still generates a file containing the token. The #if DEBUG in the generated file is what keeps the token out of Release binaries; I confirmed the release binary contains no token strings. This check is still valid in Run Script phases.
| for k in sorted(tokens_map.keys()): | ||
| lines.append(f' "{swift_escape(k)}": "{swift_escape(tokens_map[k])}",') | ||
| lines.append(" ]") |
There was a problem hiding this comment.
This writes every token in the CLI config into the app, including tokens for other projects' apps. Debug builds do get shared (ad hoc, App Distribution), so it should embed only the token for this target's GOOGLE_APP_ID. #17003 does that and replaces the python3 dependency with plutil.
| #if TARGET_OS_OSX && !TARGET_OS_SIMULATOR | ||
| NSString *homeDir = NSHomeDirectory(); | ||
| if (homeDir.length > 0) { | ||
| NSString *macConfigPath = [[homeDir stringByAppendingPathComponent:@".config/configstore"] | ||
| stringByAppendingPathComponent:@"firebase-appcheck.json"]; | ||
| [candidatePaths addObject:macConfigPath]; | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| NSString *xdgConfigHome = environment[kXDGConfigHomeEnvKey]; | ||
| if ([xdgConfigHome isKindOfClass:[NSString class]] && xdgConfigHome.length > 0) { | ||
| NSString *xdgConfigPath = [[xdgConfigHome stringByAppendingPathComponent:@"configstore"] | ||
| stringByAppendingPathComponent:@"firebase-appcheck.json"]; | ||
| if (![candidatePaths containsObject:xdgConfigPath]) { | ||
| [candidatePaths addObject:xdgConfigPath]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Two issues here:
- In sandboxed macOS apps (the default in Xcode's macOS template),
NSHomeDirectory()returns the app container, so the CLI config is never found. - configstore writes to
$XDG_CONFIG_HOME/configstorewheneverXDG_CONFIG_HOMEis set, so that path should be checked first.
#17003 checks XDG first and looks up the actual home directory with getpwuid_r. Sandboxed apps still need a read exception for ~/.config/configstore/.
| NSString *moduleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"]; | ||
| if ([moduleName isKindOfClass:[NSString class]] && moduleName.length > 0) { | ||
| NSString *sanitizedModule = [moduleName stringByReplacingOccurrencesOfString:@"-" | ||
| withString:@"_"]; | ||
| sanitizedModule = [sanitizedModule stringByReplacingOccurrencesOfString:@" " withString:@"_"]; | ||
| NSString *qualifiedName = | ||
| [NSString stringWithFormat:@"%@.%@", sanitizedModule, kInjectedDebugTokenClassName]; | ||
| injectedClass = NSClassFromString(qualifiedName); | ||
| } |
There was a problem hiding this comment.
Nit: the generated class uses @objc(FIRAppCheckInjectedDebugToken), so its runtime name isn't module-qualified and NSClassFromString finds it directly. I confirmed this in the client package test. The module-qualified fallback isn't needed, and CFBundleExecutable isn't always the module name anyway.
| if (error.code == 403) { | ||
| return YES; | ||
| } | ||
|
|
||
| NSString *failureReason = | ||
| error.localizedFailureReason ?: error.userInfo[NSLocalizedFailureReasonErrorKey]; | ||
| if ([failureReason isKindOfClass:[NSString class]] && | ||
| ([failureReason containsString:@"status code: 403"] || | ||
| [failureReason containsString:@"403"])) { | ||
| return YES; | ||
| } | ||
|
|
||
| NSString *description = error.localizedDescription; | ||
| if ([description isKindOfClass:[NSString class]] && | ||
| ([description containsString:@"status code: 403"] || [description containsString:@"403"])) { | ||
| return YES; | ||
| } |
There was a problem hiding this comment.
GACAppCheckHTTPError's failure reason includes the request URL (with the project and app IDs) and the response body. So containsString:@"403" flags a 500 or 503 as a rejected token for any app whose IDs contain "403". HTTP errors also have code == 0, so the code check never matches them.
I'd check only httpResponse.statusCode, walking NSUnderlyingErrorKey. #17003 does that and adds tests for these cases. A 403 can also mean the App Check API is disabled or the API key is restricted, so the hint should hedge a bit.
| void FIRAppCheckLogDebugTokenSource(NSString *source) { | ||
| FIRLogInfo(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeDebugTokenSource, | ||
| @"[FirebaseAppCheck] Debug token loaded from %@.", source); | ||
| } | ||
|
|
||
| void FIRAppCheckLogDebugTokenRejected(void) { | ||
| FIRLogWarning(kFIRLoggerAppCheck, kFIRLoggerAppCheckMessageCodeDebugTokenRejected, | ||
| @"[FirebaseAppCheck] Debug token was rejected by server. Run 'firebase " | ||
| @"appcheck:debugtokens:create --force' to provision a new token."); |
There was a problem hiding this comment.
Nits:
kFIRLoggerAppCheckis@"[FirebaseAppCheck]", which the logger already prints, so the prefix is duplicated.- Info-level messages aren't shown by default. The init no longer prints the token for external sources, so Notice may be more useful for the source line.
- Adding
--projectand--appto the 403 hint would make it copy-pasteable. Can you also confirm that--forcematches the CLI?
| [[GULUserDefaults standardUserDefaults] setObject:@"stored_user_defaults_token_555" | ||
| forKey:@"GACAppCheckDebugToken"]; |
There was a problem hiding this comment.
This value is never removed, so it leaks into later tests. It's the stored_user_defaults_token_555 that shows up in the Catalyst failures of testResolveTokenFromSimulatorHostHomeLocalConfigFile. I'd clear GACAppCheckDebugToken in setUp and tearDown.
Discussion
Implements the Apple / iOS SDK portion of SDK/CLI Seamless Debug Tokens (
go/firebase-api-sdk-cli-seamless-debug-tokens):FIRAppCheckDebugProvider:FIREBASE_APPCHECK_DEBUG_TOKENenvironment variableAppCheckDebugToken(and legacyFIRAAppCheckDebugToken) environment variable~/.config/configstore/firebase-appcheck.jsonviaSIMULATOR_HOST_HOMEon Simulator orNSHomeDirectory()on macOS, withXDG_CONFIG_HOMEfallback) keyed byapp.options.googleAppIDFIRAppCheckInjectedDebugTokenclass (+tokens[googleAppID]/+token), guarded by a Release build (!DEBUG) security check that logs an error and ignores the tokenGULUserDefaults(NSUserDefaults) cached tokenFirebaseAppCheckDebugTokenPlugin(BuildToolPlugin&XcodeBuildToolPlugin) andFirebaseAppCheck/Scripts/inject_debug_token.shto generateAppCheckDebugToken.generated.swift(#if DEBUG,@objc(FIRAppCheckInjectedDebugToken)) for physical device Debug builds.I-FAA004003:[FirebaseAppCheck] Debug token loaded from <source>.)I-FAA004004:[FirebaseAppCheck] Debug token was rejected by server. Run 'firebase appcheck:debugtokens:create --force' to provision a new token.)Testing
FIRAppCheckDebugProviderTests.mandFIRAppCheckLoggerTests.mcovering all 6 resolution steps, precedence rules, malformed/unmatched configstore JSON resilience, dynamicFIRAppCheckInjectedDebugTokenlookup, Release build safeguard, and HTTP 403 error detection.xcodebuild test -scheme FirebaseAppCheckUniton both macOS (arm64) and iOS Simulator (iPhone 17 Pro).API Changes
FirebaseAppCheckDebugTokenPlugin) inPackage.swift.