mirror of
https://github.com/google/app-check
synced 2026-11-09 22:05:52 +05:45
The debug provider stores a per-service "registered" flag so it stops logging the local debug token's registration instructions once that token has been exchanged successfully. Exchanges using the AppCheckDebugToken or FIRAAppCheckDebugToken environment variable also updated the flag, even though the local token wasn't used: - On success, the local token was marked as registered, so after the variable was removed the SDK fell back to an unregistered token without logging how to register it. - On failure, the flag was cleared, so an already registered local token logged its registration instructions again. Only set or clear the flag when the local token was the one exchanged; serverUnreachable failures still leave it untouched. There are no public API changes. Also document all parameters of the public initializer, and fix the test tearDown, which only removed the registered key's prefix and let the flag leak between tests.
267 lines
11 KiB
Swift
267 lines
11 KiB
Swift
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import Foundation
|
|
|
|
#if COCOAPODS
|
|
import GoogleUtilities
|
|
#else
|
|
import GoogleUtilities_UserDefaults
|
|
#endif
|
|
|
|
private let kDebugTokenEnvKey = "AppCheckDebugToken"
|
|
private let kFirebaseDebugTokenEnvKey = "FIRAAppCheckDebugToken"
|
|
|
|
@objc(GACAppCheckDebugProvider)
|
|
public class AppCheckCoreDebugProvider: NSObject, AppCheckCoreProvider {
|
|
/// Storage key for the debug token in UserDefaults.
|
|
/// Internal scope exists for testing purposes.
|
|
/// Do not rename: retains the "GAC" prefix for compatibility with existing stored data from v11
|
|
/// or lower.
|
|
static let debugTokenUserDefaultsKey = "GACAppCheckDebugToken"
|
|
|
|
/// Storage key prefix for registered debug tokens in UserDefaults.
|
|
/// Internal scope exists for testing purposes.
|
|
/// Do not rename: retains the "GAC" prefix for compatibility with existing stored data from v11
|
|
/// or lower.
|
|
static let debugTokenRegisteredUserDefaultsKeyPrefix = "GACAppCheckDebugTokenRegistered"
|
|
|
|
private let apiService: AppCheckCoreDebugProviderAPIServiceProtocol
|
|
private let debugTokenEnvValue: String?
|
|
private let registeredUserDefaultsKey: String
|
|
|
|
// Internal initializer
|
|
init(apiService: AppCheckCoreDebugProviderAPIServiceProtocol,
|
|
serviceName: String,
|
|
resourceName: String,
|
|
environment: [String: String]) {
|
|
self.apiService = apiService
|
|
let registeredUserDefaultsKey = Self.registeredUserDefaultsKey(
|
|
forServiceName: serviceName,
|
|
resourceName: resourceName
|
|
)
|
|
self.registeredUserDefaultsKey = registeredUserDefaultsKey
|
|
let debugTokenEnvValue = Self.environmentVariableDebugToken(environment: environment)
|
|
self.debugTokenEnvValue = debugTokenEnvValue
|
|
super.init()
|
|
|
|
if debugTokenEnvValue == nil {
|
|
Self.logLocalDebugTokenIfNotRegistered(registeredUserDefaultsKey: registeredUserDefaultsKey)
|
|
}
|
|
}
|
|
|
|
/// Creates a debug provider that uses the environment variable, stored, or newly generated
|
|
/// debug token.
|
|
///
|
|
/// - Parameters:
|
|
/// - serviceName: The name of the service (e.g. the Firebase app name) using the provider.
|
|
/// - resourceName: The App Check resource name, `projects/{project}/apps/{app}`.
|
|
/// - baseURL: The App Check API base URL, or `nil` for the default.
|
|
/// - apiKey: The API key used to call the App Check API.
|
|
/// - requestHooks: Hooks invoked on each outgoing request. From Swift, pass
|
|
/// `[AppCheckCoreAPIRequestHook]`. From Objective-C, pass an `NSArray` of blocks with the
|
|
/// signature `void (^)(NSMutableURLRequest *)`; the signature is not checked at compile
|
|
/// time and a mismatch will crash when the hook is invoked.
|
|
///
|
|
/// Typed `[Any]?` rather than `[AppCheckCoreAPIRequestHook]?` deliberately: Swift cannot
|
|
/// bridge an `NSArray` into a Swift `Array` whose element is a function type, so the typed
|
|
/// signature traps at runtime for any non-nil array passed from Objective-C. Do not
|
|
/// "simplify" this type — see PR #111.
|
|
@objc(initWithServiceName:resourceName:baseURL:APIKey:requestHooks:)
|
|
public convenience init(serviceName: String,
|
|
resourceName: String,
|
|
baseURL: String?,
|
|
apiKey: String,
|
|
requestHooks: [Any]?) {
|
|
self.init(serviceName: serviceName,
|
|
resourceName: resourceName,
|
|
baseURL: baseURL,
|
|
apiKey: apiKey,
|
|
requestHooks: requestHooks,
|
|
environment: ProcessInfo.processInfo.environment)
|
|
}
|
|
|
|
// Additional internal initializer for testing
|
|
convenience init(serviceName: String,
|
|
resourceName: String,
|
|
baseURL: String?,
|
|
apiKey: String,
|
|
requestHooks: [Any]?,
|
|
environment: [String: String]) {
|
|
let urlSession = URLSession(configuration: .ephemeral)
|
|
let coreAPIService = AppCheckCoreAPIService(urlSession: urlSession,
|
|
baseURL: baseURL,
|
|
apiKey: apiKey,
|
|
requestHooks: requestHooks,
|
|
environment: environment)
|
|
let debugAPIService = AppCheckCoreDebugProviderAPIService(apiService: coreAPIService,
|
|
resourceName: resourceName)
|
|
self.init(apiService: debugAPIService,
|
|
serviceName: serviceName,
|
|
resourceName: resourceName,
|
|
environment: environment)
|
|
}
|
|
|
|
@objc public func localDebugToken() -> String {
|
|
return Self.localDebugToken()
|
|
}
|
|
|
|
@objc public func currentDebugToken() -> String {
|
|
return debugTokenEnvValue ?? Self.localDebugToken()
|
|
}
|
|
|
|
/// Whether `currentDebugToken()` falls through to the locally stored or generated token.
|
|
private var usesLocalDebugToken: Bool {
|
|
return debugTokenEnvValue == nil
|
|
}
|
|
|
|
// MARK: - AppCheckCoreProvider
|
|
|
|
public func getToken() async throws -> AppCheckCoreToken {
|
|
return try await getToken(limitedUse: false)
|
|
}
|
|
|
|
public func getLimitedUseToken() async throws -> AppCheckCoreToken {
|
|
return try await getToken(limitedUse: true)
|
|
}
|
|
|
|
@objc(getTokenWithCompletion:)
|
|
public func getToken(completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) {
|
|
getToken(limitedUse: false, completion: handler)
|
|
}
|
|
|
|
@objc(getLimitedUseTokenWithCompletion:)
|
|
public func getLimitedUseToken(completion handler: @escaping (AppCheckCoreToken?, Error?)
|
|
-> Void) {
|
|
getToken(limitedUse: true, completion: handler)
|
|
}
|
|
|
|
// MARK: - Internal
|
|
|
|
private func getToken(limitedUse: Bool) async throws -> AppCheckCoreToken {
|
|
do {
|
|
let token = try await apiService.appCheckToken(
|
|
debugToken: currentDebugToken(),
|
|
limitedUse: limitedUse
|
|
)
|
|
// Only the locally generated token is tracked as registered: if an environment variable
|
|
// debug token is later removed, the local token's registration instructions must still be
|
|
// logged.
|
|
if usesLocalDebugToken {
|
|
GULUserDefaults.standard().setObject(true, forKey: registeredUserDefaultsKey)
|
|
}
|
|
return token
|
|
} catch {
|
|
let logMessage = "Failed to exchange debug token to app check token: \(error)"
|
|
AppCheckCoreLogger.log(
|
|
code: .debugProviderFailedExchange,
|
|
logLevel: .debug,
|
|
message: logMessage
|
|
)
|
|
|
|
let nsError = error as NSError
|
|
let isServerReachable = nsError.domain != AppCheckCoreErrorDomain ||
|
|
nsError.code != AppCheckCoreErrorCode.serverUnreachable.rawValue
|
|
// As on success, only the local token's registration is tracked: a rejected environment
|
|
// variable token says nothing about whether the local token is registered.
|
|
if usesLocalDebugToken, isServerReachable {
|
|
GULUserDefaults.standard().removeObject(forKey: registeredUserDefaultsKey)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
private func getToken(limitedUse: Bool,
|
|
completion handler: @escaping (AppCheckCoreToken?, Error?) -> Void) {
|
|
Task {
|
|
do {
|
|
let token = try await getToken(limitedUse: limitedUse)
|
|
AppCheckCore.deliverOnMainQueue(token, error: nil as Error?, to: handler)
|
|
} catch {
|
|
AppCheckCore.deliverOnMainQueue(nil, error: error, to: handler)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func localDebugToken() -> String {
|
|
if let token = GULUserDefaults.standard().string(forKey: debugTokenUserDefaultsKey) {
|
|
return token
|
|
} else {
|
|
let token = UUID().uuidString
|
|
GULUserDefaults.standard().setObject(token, forKey: debugTokenUserDefaultsKey)
|
|
return token
|
|
}
|
|
}
|
|
|
|
/// Internal scope exists for testing purposes.
|
|
static func registeredUserDefaultsKey(forServiceName serviceName: String,
|
|
resourceName: String) -> String {
|
|
let safeServiceName = serviceName.isEmpty ? "default" : serviceName
|
|
var safeResourceName = resourceName.replacingOccurrences(of: "/", with: "_")
|
|
if safeResourceName.isEmpty {
|
|
safeResourceName = "default"
|
|
}
|
|
return "\(debugTokenRegisteredUserDefaultsKeyPrefix)_\(safeServiceName)_\(safeResourceName)"
|
|
}
|
|
|
|
private static func environmentVariableDebugToken(environment: [String: String]) -> String? {
|
|
let envVariableValue = environment[kDebugTokenEnvKey]?
|
|
.isEmpty == false ? environment[kDebugTokenEnvKey] : nil
|
|
let firebaseEnvVariableValue = environment[kFirebaseDebugTokenEnvKey]?
|
|
.isEmpty == false ? environment[kFirebaseDebugTokenEnvKey] : nil
|
|
|
|
if let env = envVariableValue, let _ = firebaseEnvVariableValue {
|
|
let message =
|
|
"The environment variables \(kDebugTokenEnvKey) and \(kFirebaseDebugTokenEnvKey) are both set; using the debug token specified in \(kDebugTokenEnvKey) and ignoring the value of \(kFirebaseDebugTokenEnvKey)."
|
|
AppCheckCoreLogger.log(
|
|
code: .debugProviderFirebaseEnvironmentVariable,
|
|
logLevel: .warning,
|
|
message: message
|
|
)
|
|
return env
|
|
} else if let env = envVariableValue {
|
|
let message =
|
|
"Using the debug token specified in the environment variable \(kDebugTokenEnvKey)."
|
|
AppCheckCoreLogger.log(
|
|
code: .environmentVariableDebugToken,
|
|
logLevel: .debug,
|
|
message: message
|
|
)
|
|
return env
|
|
} else if let firebaseEnv = firebaseEnvVariableValue {
|
|
let message =
|
|
"Using the debug token specified in the environment variable \(kFirebaseDebugTokenEnvKey)."
|
|
AppCheckCoreLogger.log(
|
|
code: .debugProviderFirebaseEnvironmentVariable,
|
|
logLevel: .debug,
|
|
message: message
|
|
)
|
|
return firebaseEnv
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Logs the locally stored debug token unless it has previously been exchanged successfully.
|
|
///
|
|
/// As a side effect, generates and persists the local token if none is stored yet.
|
|
private static func logLocalDebugTokenIfNotRegistered(registeredUserDefaultsKey: String) {
|
|
let isRegistered = GULUserDefaults.standard().bool(forKey: registeredUserDefaultsKey)
|
|
if !isRegistered {
|
|
let message = "App Check debug token: '\(Self.localDebugToken())'."
|
|
AppCheckCoreLogger.log(code: .localDebugToken, logLevel: .warning, message: message)
|
|
}
|
|
}
|
|
}
|