diff --git a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt index 0dc0ce26dc7e..7ccb625d0a97 100644 --- a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt +++ b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/FirebaseAppCheckPlugin.kt @@ -55,6 +55,7 @@ class FirebaseAppCheckPlugin : androidProvider: String?, appleProvider: String?, debugToken: String?, + windowsProvider: String?, callback: (Result) -> Unit ) { try { diff --git a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt index 0a046dd5aa47..e2b417cc4b4f 100644 --- a/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt +++ b/packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt @@ -18,6 +18,9 @@ import java.io.ByteArrayOutputStream import java.nio.ByteBuffer private object GeneratedAndroidFirebaseAppCheckPigeonUtils { + fun createConnectionError(channelName: String): FlutterError { + return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } + fun wrapResult(result: Any?): List { return listOf(result) } @@ -37,6 +40,36 @@ private object GeneratedAndroidFirebaseAppCheckPigeonUtils { ) } } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).containsKey(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + } /** @@ -50,19 +83,77 @@ class FlutterError ( override val message: String? = null, val details: Any? = null ) : Throwable() + +/** + * Carries a minted App Check token plus the wall-clock expiry the Firebase + * SDK should associate with it. Returning the expiry alongside the token lets + * backends mint tokens with arbitrary lifetimes (short TTLs for a stricter + * security posture, longer TTLs for fewer round-trips) without the plugin + * hardcoding a refresh window. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class CustomAppCheckToken ( + /** The App Check token string to send with Firebase requests. */ + val token: String, + /** + * Absolute expiry as Unix epoch milliseconds (UTC). The Firebase SDK uses + * this to decide when to refresh; a token returned with an expiry in the + * past is treated as immediately expired. + */ + val expireTimeMillis: Long +) + { + companion object { + fun fromList(pigeonVar_list: List): CustomAppCheckToken { + val token = pigeonVar_list[0] as String + val expireTimeMillis = pigeonVar_list[1] as Long + return CustomAppCheckToken(token, expireTimeMillis) + } + } + fun toList(): List { + return listOf( + token, + expireTimeMillis, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is CustomAppCheckToken) { + return false + } + if (this === other) { + return true + } + return GeneratedAndroidFirebaseAppCheckPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} private open class GeneratedAndroidFirebaseAppCheckPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + CustomAppCheckToken.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) + when (value) { + is CustomAppCheckToken -> { + stream.write(129) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface FirebaseAppCheckHostApi { - fun activate(appName: String, androidProvider: String?, appleProvider: String?, debugToken: String?, callback: (Result) -> Unit) + fun activate(appName: String, androidProvider: String?, appleProvider: String?, debugToken: String?, windowsProvider: String?, callback: (Result) -> Unit) fun getToken(appName: String, forceRefresh: Boolean, callback: (Result) -> Unit) fun setTokenAutoRefreshEnabled(appName: String, isTokenAutoRefreshEnabled: Boolean, callback: (Result) -> Unit) fun registerTokenListener(appName: String, callback: (Result) -> Unit) @@ -86,7 +177,8 @@ interface FirebaseAppCheckHostApi { val androidProviderArg = args[1] as String? val appleProviderArg = args[2] as String? val debugTokenArg = args[3] as String? - api.activate(appNameArg, androidProviderArg, appleProviderArg, debugTokenArg) { result: Result -> + val windowsProviderArg = args[4] as String? + api.activate(appNameArg, androidProviderArg, appleProviderArg, debugTokenArg, windowsProviderArg) { result: Result -> val error = result.exceptionOrNull() if (error != null) { reply.reply(GeneratedAndroidFirebaseAppCheckPigeonUtils.wrapError(error)) @@ -183,3 +275,41 @@ interface FirebaseAppCheckHostApi { } } } +/** + * Dart-side handler invoked by the native plugin when the Firebase SDK needs + * a fresh App Check token. Implementations typically call a backend service + * (for example a Cloud Function with `enforceAppCheck: false`) that mints a + * token using the Firebase Admin SDK. The native side awaits the future, + * then hands the token to the Firebase SDK, which attaches it to subsequent + * Firebase backend requests (Firestore, Functions, Storage, Auth, RTDB). + * + * Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. + */ +class FirebaseAppCheckFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { + companion object { + /** The codec used by FirebaseAppCheckFlutterApi. */ + val codec: MessageCodec by lazy { + GeneratedAndroidFirebaseAppCheckPigeonCodec() + } + } + fun getCustomToken(callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(null) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else if (it[0] == null) { + callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", ""))) + } else { + val output = it[0] as CustomAppCheckToken + callback(Result.success(output)) + } + } else { + callback(Result.failure(GeneratedAndroidFirebaseAppCheckPigeonUtils.createConnectionError(channelName))) + } + } + } +} diff --git a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift index 1842cfe9c240..0db996a4e4aa 100644 --- a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift +++ b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift @@ -57,6 +57,14 @@ private func wrapError(_ error: Any) -> [Any?] { ] } +private func createConnectionError(withChannelName channelName: String) -> PigeonError { + PigeonError( + code: "channel-error", + message: "Unable to establish connection on channel: '\(channelName)'.", + details: "" + ) +} + private func isNullish(_ value: Any?) -> Bool { value is NSNull || value == nil } @@ -66,9 +74,134 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } -private class FirebaseAppCheckMessagesPigeonCodecReader: FlutterStandardReader {} +func deepEqualsFirebaseAppCheckMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsFirebaseAppCheckMessages(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsFirebaseAppCheckMessages(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be + // untrue. + return false + } +} -private class FirebaseAppCheckMessagesPigeonCodecWriter: FlutterStandardWriter {} +func deepHashFirebaseAppCheckMessages(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { + deepHashFirebaseAppCheckMessages(value: item, hasher: &hasher) + } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashFirebaseAppCheckMessages(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + +/// Carries a minted App Check token plus the wall-clock expiry the Firebase +/// SDK should associate with it. Returning the expiry alongside the token lets +/// backends mint tokens with arbitrary lifetimes (short TTLs for a stricter +/// security posture, longer TTLs for fewer round-trips) without the plugin +/// hardcoding a refresh window. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct CustomAppCheckToken: Hashable { + /// The App Check token string to send with Firebase requests. + var token: String + /// Absolute expiry as Unix epoch milliseconds (UTC). The Firebase SDK uses + /// this to decide when to refresh; a token returned with an expiry in the + /// past is treated as immediately expired. + var expireTimeMillis: Int64 + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> CustomAppCheckToken? { + let token = pigeonVar_list[0] as! String + let expireTimeMillis = pigeonVar_list[1] as! Int64 + + return CustomAppCheckToken( + token: token, + expireTimeMillis: expireTimeMillis + ) + } + + func toList() -> [Any?] { + [ + token, + expireTimeMillis, + ] + } + + static func == (lhs: CustomAppCheckToken, rhs: CustomAppCheckToken) -> Bool { + deepEqualsFirebaseAppCheckMessages(lhs.toList(), rhs.toList()) + } + + func hash(into hasher: inout Hasher) { + deepHashFirebaseAppCheckMessages(value: toList(), hasher: &hasher) + } +} + +private class FirebaseAppCheckMessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return CustomAppCheckToken.fromList(readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class FirebaseAppCheckMessagesPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? CustomAppCheckToken { + super.writeByte(129) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} private class FirebaseAppCheckMessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { override func reader(with data: Data) -> FlutterStandardReader { @@ -90,7 +223,8 @@ class FirebaseAppCheckMessagesPigeonCodec: FlutterStandardMessageCodec, @uncheck /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol FirebaseAppCheckHostApi { func activate(appName: String, androidProvider: String?, appleProvider: String?, - debugToken: String?, completion: @escaping (Result) -> Void) + debugToken: String?, windowsProvider: String?, + completion: @escaping (Result) -> Void) func getToken(appName: String, forceRefresh: Bool, completion: @escaping (Result) -> Void) func setTokenAutoRefreshEnabled(appName: String, isTokenAutoRefreshEnabled: Bool, @@ -123,11 +257,13 @@ class FirebaseAppCheckHostApiSetup { let androidProviderArg: String? = nilOrValue(args[1]) let appleProviderArg: String? = nilOrValue(args[2]) let debugTokenArg: String? = nilOrValue(args[3]) + let windowsProviderArg: String? = nilOrValue(args[4]) api.activate( appName: appNameArg, androidProvider: androidProviderArg, appleProvider: appleProviderArg, - debugToken: debugTokenArg + debugToken: debugTokenArg, + windowsProvider: windowsProviderArg ) { result in switch result { case .success: @@ -231,3 +367,58 @@ class FirebaseAppCheckHostApiSetup { } } } + +/// Dart-side handler invoked by the native plugin when the Firebase SDK needs +/// a fresh App Check token. Implementations typically call a backend service +/// (for example a Cloud Function with `enforceAppCheck: false`) that mints a +/// token using the Firebase Admin SDK. The native side awaits the future, +/// then hands the token to the Firebase SDK, which attaches it to subsequent +/// Firebase backend requests (Firestore, Functions, Storage, Auth, RTDB). +/// +/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. +protocol FirebaseAppCheckFlutterApiProtocol { + func getCustomToken(completion: @escaping (Result) -> Void) +} + +class FirebaseAppCheckFlutterApi: FirebaseAppCheckFlutterApiProtocol { + private let binaryMessenger: FlutterBinaryMessenger + private let messageChannelSuffix: String + init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { + self.binaryMessenger = binaryMessenger + self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + } + + var codec: FirebaseAppCheckMessagesPigeonCodec { + FirebaseAppCheckMessagesPigeonCodec.shared + } + + func getCustomToken(completion: @escaping (Result) -> Void) { + let channelName = "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken\(messageChannelSuffix)" + let channel = FlutterBasicMessageChannel( + name: channelName, + binaryMessenger: binaryMessenger, + codec: codec + ) + channel.sendMessage(nil) { response in + guard let listResponse = response as? [Any?] else { + completion(.failure(createConnectionError(withChannelName: channelName))) + return + } + if listResponse.count > 1 { + let code: String = listResponse[0] as! String + let message: String? = nilOrValue(listResponse[1]) + let details: String? = nilOrValue(listResponse[2]) + completion(.failure(PigeonError(code: code, message: message, details: details))) + } else if listResponse[0] == nil { + completion(.failure(PigeonError( + code: "null-error", + message: "Flutter api returned null value for non-null return value.", + details: "" + ))) + } else { + let result = listResponse[0] as! CustomAppCheckToken + completion(.success(result)) + } + } + } +} diff --git a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift index 5aec32b1ad2c..a7efddd615ec 100644 --- a/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift +++ b/packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckPlugin.swift @@ -60,7 +60,7 @@ public class FirebaseAppCheckPlugin: NSObject, FlutterPlugin, private var binaryMessenger: FlutterBinaryMessenger? func activate(appName: String, androidProvider: String?, appleProvider: String?, - debugToken: String?, + debugToken: String?, windowsProvider: String?, completion: @escaping (Result) -> Void) { guard let app = FLTFirebasePlugin.firebaseAppNamed(appName) else { completion(.failure(FlutterError( diff --git a/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart index cd569468c12c..53589fad47db 100644 --- a/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check/lib/firebase_app_check.dart @@ -23,7 +23,10 @@ export 'package:firebase_app_check_platform_interface/firebase_app_check_platfor ReCaptchaV3Provider, WebDebugProvider, WindowsAppCheckProvider, - WindowsDebugProvider; + WindowsDebugProvider, + WindowsCustomProvider, + FirebaseAppCheckFlutterApi, + CustomAppCheckToken; export 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' show FirebaseException; diff --git a/packages/firebase_app_check/firebase_app_check/windows/CMakeLists.txt b/packages/firebase_app_check/firebase_app_check/windows/CMakeLists.txt index 7c40c200c5e9..10fd9a991423 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/CMakeLists.txt +++ b/packages/firebase_app_check/firebase_app_check/windows/CMakeLists.txt @@ -4,6 +4,53 @@ # customers of the plugin. cmake_minimum_required(VERSION 3.14) +set(FIREBASE_SDK_VERSION "13.5.0") + +if(EXISTS $ENV{FIREBASE_CPP_SDK_DIR}/include/firebase/version.h) + file(READ "$ENV{FIREBASE_CPP_SDK_DIR}/include/firebase/version.h" existing_version) + + string(REGEX MATCH "FIREBASE_VERSION_MAJOR ([0-9]*)" _ ${existing_version}) + set(existing_version_major ${CMAKE_MATCH_1}) + + string(REGEX MATCH "FIREBASE_VERSION_MINOR ([0-9]*)" _ ${existing_version}) + set(existing_version_minor ${CMAKE_MATCH_1}) + + string(REGEX MATCH "FIREBASE_VERSION_REVISION ([0-9]*)" _ ${existing_version}) + set(existing_version_revision ${CMAKE_MATCH_1}) + + set(existing_version "${existing_version_major}.${existing_version_minor}.${existing_version_revision}") +endif() + +if(existing_version VERSION_EQUAL FIREBASE_SDK_VERSION) + message(STATUS "Found Firebase SDK version ${existing_version}") + set(FIREBASE_CPP_SDK_DIR $ENV{FIREBASE_CPP_SDK_DIR}) +else() + set(firebase_sdk_url "https://dl.google.com/firebase/sdk/cpp/firebase_cpp_sdk_windows_${FIREBASE_SDK_VERSION}.zip") + set(firebase_sdk_filename "${CMAKE_BINARY_DIR}/firebase_cpp_sdk_windows_${FIREBASE_SDK_VERSION}.zip") + set(extracted_path "${CMAKE_BINARY_DIR}/extracted") + if(NOT EXISTS ${firebase_sdk_filename}) + file(DOWNLOAD ${firebase_sdk_url} ${firebase_sdk_filename} + SHOW_PROGRESS + STATUS download_status + LOG download_log) + list(GET download_status 0 status_code) + if(NOT status_code EQUAL 0) + message(FATAL_ERROR "Download failed: ${download_log}") + endif() + else() + message(STATUS "Using cached Firebase SDK zip file") + endif() + + if(NOT EXISTS ${extracted_path}) + file(MAKE_DIRECTORY ${extracted_path}) + file(ARCHIVE_EXTRACT INPUT ${firebase_sdk_filename} + DESTINATION ${extracted_path}) + else() + message(STATUS "Using cached extracted Firebase SDK") + endif() + set(FIREBASE_CPP_SDK_DIR "${extracted_path}/firebase_cpp_sdk_windows") +endif() + # Project-level configuration. set(PROJECT_NAME "firebase_app_check") project(${PROJECT_NAME} LANGUAGES CXX) @@ -66,6 +113,34 @@ target_compile_definitions(${PLUGIN_NAME} PRIVATE -DINTERNAL_EXPERIMENTAL=1) # dependencies here. set(MSVC_RUNTIME_MODE MD) set(firebase_libs firebase_core_plugin firebase_app_check) + +set(FLUTTERFIRE_FIREBASE_CPP_SDK_BINARY_DIR + "${CMAKE_BINARY_DIR}/firebase_cpp_sdk") +if(NOT TARGET firebase_app) + add_subdirectory(${FIREBASE_CPP_SDK_DIR} + ${FLUTTERFIRE_FIREBASE_CPP_SDK_BINARY_DIR} + EXCLUDE_FROM_ALL) +endif() + +target_include_directories(${PLUGIN_NAME} INTERFACE + "${FIREBASE_CPP_SDK_DIR}/include") + +# firebase_core rewrites the imported Firebase C++ SDK library targets so +# multi-config generators (Visual Studio) use the Release .lib paths when +# building Release/Profile. Without this, firebase_app_check can end up linking +# the Debug firebase_app_check.lib into a Release runner, which pulls in debug +# CRT symbols and fails at link time. +get_target_property(firebase_app_check_debug_path firebase_app_check IMPORTED_LOCATION) +if(firebase_app_check_debug_path) + string(REPLACE "Debug" "Release" firebase_app_check_release_path + ${firebase_app_check_debug_path}) + set_target_properties(firebase_app_check PROPERTIES + IMPORTED_LOCATION_DEBUG "${firebase_app_check_debug_path}" + IMPORTED_LOCATION_RELEASE "${firebase_app_check_release_path}" + IMPORTED_LOCATION_PROFILE "${firebase_app_check_release_path}" + ) +endif() + target_link_libraries(${PLUGIN_NAME} PRIVATE "${firebase_libs}") target_include_directories(${PLUGIN_NAME} INTERFACE diff --git a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp index d9a0a72d7014..789a1dc33d81 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp +++ b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp @@ -98,6 +98,52 @@ class TokenStreamHandler std::unique_ptr listener_; }; +// FlutterCustomAppCheckProvider calls into Dart via the FlutterApi and +// completes the Firebase C++ SDK callback asynchronously when Dart returns a +// token (or an error). The Dart handler returns the token together with its +// expiry, so the C++ SDK can cache for the exact lifetime the backend minted +// rather than a hardcoded refresh window. +FlutterCustomAppCheckProvider::FlutterCustomAppCheckProvider( + flutter::BinaryMessenger* binary_messenger) + : flutter_api_( + std::make_unique(binary_messenger)) {} + +void FlutterCustomAppCheckProvider::GetToken( + std::function + completion_callback) { + auto completion = std::make_shared>( + std::move(completion_callback)); + + flutter_api_->GetCustomToken( + [completion](const CustomAppCheckToken& dart_token) { + firebase::app_check::AppCheckToken result_token; + result_token.token = dart_token.token(); + result_token.expire_time_millis = dart_token.expire_time_millis(); + (*completion)(result_token, firebase::app_check::kAppCheckErrorNone, + ""); + }, + [completion](const FlutterError& error) { + (*completion)(firebase::app_check::AppCheckToken(), + firebase::app_check::kAppCheckErrorUnknown, + error.message().empty() ? "unknown" : error.message()); + }); +} + +FlutterCustomAppCheckProviderFactory::FlutterCustomAppCheckProviderFactory( + flutter::BinaryMessenger* binary_messenger) + : binary_messenger_(binary_messenger) {} + +firebase::app_check::AppCheckProvider* +FlutterCustomAppCheckProviderFactory::CreateProvider(firebase::App* app) { + if (!provider_) { + provider_ = + std::make_unique(binary_messenger_); + } + return provider_.get(); +} + static AppCheck* GetAppCheckFromPigeon(const std::string& app_name) { App* app = App::GetInstance(app_name.c_str()); return AppCheck::GetInstance(app); @@ -166,17 +212,22 @@ FirebaseAppCheckPlugin::~FirebaseAppCheckPlugin() { void FirebaseAppCheckPlugin::Activate( const std::string& app_name, const std::string* android_provider, const std::string* apple_provider, const std::string* debug_token, + const std::string* windows_provider, std::function reply)> result) { - // On Windows/desktop, only the Debug provider is available. - DebugAppCheckProviderFactory* factory = - DebugAppCheckProviderFactory::GetInstance(); + if (windows_provider != nullptr && *windows_provider == "custom") { + custom_provider_factory_ = + std::make_unique(binaryMessenger); + AppCheck::SetAppCheckProviderFactory(custom_provider_factory_.get()); + } else { + DebugAppCheckProviderFactory* factory = + DebugAppCheckProviderFactory::GetInstance(); + + if (debug_token != nullptr && !debug_token->empty()) { + factory->SetDebugToken(*debug_token); + } - if (debug_token != nullptr && !debug_token->empty()) { - factory->SetDebugToken(*debug_token); + AppCheck::SetAppCheckProviderFactory(factory); } - - AppCheck::SetAppCheckProviderFactory(factory); - result(std::nullopt); } @@ -229,9 +280,12 @@ void FirebaseAppCheckPlugin::RegisterTokenListener( void FirebaseAppCheckPlugin::GetLimitedUseAppCheckToken( const std::string& app_name, std::function reply)> result) { + // GetLimitedUseAppCheckToken was added to the Firebase C++ SDK after the + // version currently bundled with this plugin. Fall back to GetAppCheckToken, + // which is functionally equivalent for our custom Windows provider since it + // does not cache — it calls getWindowsAppCheckToken on every invocation. AppCheck* app_check = GetAppCheckFromPigeon(app_name); - - Future future = app_check->GetLimitedUseAppCheckToken(); + Future future = app_check->GetAppCheckToken(false); future.OnCompletion([result](const Future& completed_future) { if (completed_future.error() != 0) { result(ParseError(completed_future)); diff --git a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h index baabf2bd5931..c78375ffbcb4 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h +++ b/packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -24,6 +25,37 @@ namespace firebase_app_check_windows { class TokenStreamHandler; +// Custom App Check provider for Windows. When the Firebase C++ SDK calls +// GetToken(), this provider calls into Dart via FirebaseAppCheckFlutterApi +// to request a server-minted token (from the getWindowsAppCheckToken Cloud +// Function), then completes the SDK callback with the result. +class FlutterCustomAppCheckProvider + : public firebase::app_check::AppCheckProvider { + public: + explicit FlutterCustomAppCheckProvider( + flutter::BinaryMessenger* binary_messenger); + void GetToken(std::function + completion_callback) override; + + private: + std::unique_ptr flutter_api_; +}; + +// Factory that creates FlutterCustomAppCheckProvider instances. +class FlutterCustomAppCheckProviderFactory + : public firebase::app_check::AppCheckProviderFactory { + public: + explicit FlutterCustomAppCheckProviderFactory( + flutter::BinaryMessenger* binary_messenger); + firebase::app_check::AppCheckProvider* CreateProvider( + firebase::App* app) override; + + private: + flutter::BinaryMessenger* binary_messenger_; + std::unique_ptr provider_; +}; + class FirebaseAppCheckPlugin : public flutter::Plugin, public FirebaseAppCheckHostApi { friend class TokenStreamHandler; @@ -43,6 +75,7 @@ class FirebaseAppCheckPlugin : public flutter::Plugin, void Activate( const std::string& app_name, const std::string* android_provider, const std::string* apple_provider, const std::string* debug_token, + const std::string* windows_provider, std::function reply)> result) override; void GetToken(const std::string& app_name, bool force_refresh, std::function> reply)> @@ -58,6 +91,11 @@ class FirebaseAppCheckPlugin : public flutter::Plugin, std::function reply)> result) override; private: + // Holds ownership of the custom provider factory for its lifetime. + // Must outlive the AppCheck instance it was registered with. + std::unique_ptr + custom_provider_factory_; + static flutter::BinaryMessenger* binaryMessenger; static std::map< std::string, diff --git a/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp b/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp index 0343b73ea815..4682f116d498 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp +++ b/packages/firebase_app_check/firebase_app_check/windows/messages.g.cpp @@ -31,15 +31,68 @@ FlutterError CreateConnectionError(const std::string channel_name) { EncodableValue("")); } +// CustomAppCheckToken + +CustomAppCheckToken::CustomAppCheckToken(const std::string& token, + int64_t expire_time_millis) + : token_(token), expire_time_millis_(expire_time_millis) {} + +const std::string& CustomAppCheckToken::token() const { return token_; } + +void CustomAppCheckToken::set_token(std::string_view value_arg) { + token_ = value_arg; +} + +int64_t CustomAppCheckToken::expire_time_millis() const { + return expire_time_millis_; +} + +void CustomAppCheckToken::set_expire_time_millis(int64_t value_arg) { + expire_time_millis_ = value_arg; +} + +EncodableList CustomAppCheckToken::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(token_)); + list.push_back(EncodableValue(expire_time_millis_)); + return list; +} + +CustomAppCheckToken CustomAppCheckToken::FromEncodableList( + const EncodableList& list) { + CustomAppCheckToken decoded(std::get(list[0]), + std::get(list[1])); + return decoded; +} + PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( uint8_t type, flutter::ByteStreamReader* stream) const { - return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + switch (type) { + case 129: { + return CustomEncodableValue(CustomAppCheckToken::FromEncodableList( + std::get(ReadValue(stream)))); + } + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } } void PigeonInternalCodecSerializer::WriteValue( const EncodableValue& value, flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = + std::get_if(&value)) { + if (custom_value->type() == typeid(CustomAppCheckToken)) { + stream->WriteByte(129); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + } flutter::StandardCodecSerializer::WriteValue(value, stream); } @@ -92,8 +145,12 @@ void FirebaseAppCheckHostApi::SetUp(flutter::BinaryMessenger* binary_messenger, const auto& encodable_debug_token_arg = args.at(3); const auto* debug_token_arg = std::get_if(&encodable_debug_token_arg); + const auto& encodable_windows_provider_arg = args.at(4); + const auto* windows_provider_arg = + std::get_if(&encodable_windows_provider_arg); api->Activate(app_name_arg, android_provider_arg, apple_provider_arg, debug_token_arg, + windows_provider_arg, [reply](std::optional&& output) { if (output.has_value()) { reply(WrapError(output.value())); @@ -304,4 +361,58 @@ EncodableValue FirebaseAppCheckHostApi::WrapError(const FlutterError& error) { error.details()}); } +// Generated class from Pigeon that represents Flutter messages that can be +// called from C++. +FirebaseAppCheckFlutterApi::FirebaseAppCheckFlutterApi( + flutter::BinaryMessenger* binary_messenger) + : binary_messenger_(binary_messenger), message_channel_suffix_("") {} + +FirebaseAppCheckFlutterApi::FirebaseAppCheckFlutterApi( + flutter::BinaryMessenger* binary_messenger, + const std::string& message_channel_suffix) + : binary_messenger_(binary_messenger), + message_channel_suffix_(message_channel_suffix.length() > 0 + ? std::string(".") + message_channel_suffix + : "") {} + +const flutter::StandardMessageCodec& FirebaseAppCheckFlutterApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &PigeonInternalCodecSerializer::GetInstance()); +} + +void FirebaseAppCheckFlutterApi::GetCustomToken( + std::function&& on_success, + std::function&& on_error) { + const std::string channel_name = + "dev.flutter.pigeon.firebase_app_check_platform_interface." + "FirebaseAppCheckFlutterApi.getCustomToken" + + message_channel_suffix_; + BasicMessageChannel<> channel(binary_messenger_, channel_name, &GetCodec()); + EncodableValue encoded_api_arguments = EncodableValue(); + channel.Send(encoded_api_arguments, [channel_name, + on_success = std::move(on_success), + on_error = std::move(on_error)]( + const uint8_t* reply, + size_t reply_size) { + std::unique_ptr response = + GetCodec().DecodeMessage(reply, reply_size); + const auto& encodable_return_value = *response; + const auto* list_return_value = + std::get_if(&encodable_return_value); + if (list_return_value) { + if (list_return_value->size() > 1) { + on_error(FlutterError(std::get(list_return_value->at(0)), + std::get(list_return_value->at(1)), + list_return_value->at(2))); + } else { + const auto& return_value = std::any_cast( + std::get(list_return_value->at(0))); + on_success(return_value); + } + } else { + on_error(CreateConnectionError(channel_name)); + } + }); +} + } // namespace firebase_app_check_windows diff --git a/packages/firebase_app_check/firebase_app_check/windows/messages.g.h b/packages/firebase_app_check/firebase_app_check/windows/messages.g.h index c56d71862604..f3ac230526eb 100644 --- a/packages/firebase_app_check/firebase_app_check/windows/messages.g.h +++ b/packages/firebase_app_check/firebase_app_check/windows/messages.g.h @@ -52,12 +52,47 @@ class ErrorOr { private: friend class FirebaseAppCheckHostApi; + friend class FirebaseAppCheckFlutterApi; ErrorOr() = default; T TakeValue() && { return std::get(std::move(v_)); } std::variant v_; }; +// Carries a minted App Check token plus the wall-clock expiry the Firebase +// SDK should associate with it. Returning the expiry alongside the token lets +// backends mint tokens with arbitrary lifetimes (short TTLs for a stricter +// security posture, longer TTLs for fewer round-trips) without the plugin +// hardcoding a refresh window. +// +// Generated class from Pigeon that represents data sent in messages. +class CustomAppCheckToken { + public: + // Constructs an object setting all fields. + explicit CustomAppCheckToken(const std::string& token, + int64_t expire_time_millis); + + // The App Check token string to send with Firebase requests. + const std::string& token() const; + void set_token(std::string_view value_arg); + + // Absolute expiry as Unix epoch milliseconds (UTC). The Firebase SDK uses + // this to decide when to refresh; a token returned with an expiry in the + // past is treated as immediately expired. + int64_t expire_time_millis() const; + void set_expire_time_millis(int64_t value_arg); + + private: + static CustomAppCheckToken FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class FirebaseAppCheckHostApi; + friend class FirebaseAppCheckFlutterApi; + friend class PigeonInternalCodecSerializer; + std::string token_; + int64_t expire_time_millis_; +}; + class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { public: PigeonInternalCodecSerializer(); @@ -84,6 +119,7 @@ class FirebaseAppCheckHostApi { virtual void Activate( const std::string& app_name, const std::string* android_provider, const std::string* apple_provider, const std::string* debug_token, + const std::string* windows_provider, std::function reply)> result) = 0; virtual void GetToken( const std::string& app_name, bool force_refresh, @@ -114,5 +150,29 @@ class FirebaseAppCheckHostApi { protected: FirebaseAppCheckHostApi() = default; }; +// Dart-side handler invoked by the native plugin when the Firebase SDK needs +// a fresh App Check token. Implementations typically call a backend service +// (for example a Cloud Function with `enforceAppCheck: false`) that mints a +// token using the Firebase Admin SDK. The native side awaits the future, +// then hands the token to the Firebase SDK, which attaches it to subsequent +// Firebase backend requests (Firestore, Functions, Storage, Auth, RTDB). +// +// Generated class from Pigeon that represents Flutter messages that can be +// called from C++. +class FirebaseAppCheckFlutterApi { + public: + FirebaseAppCheckFlutterApi(flutter::BinaryMessenger* binary_messenger); + FirebaseAppCheckFlutterApi(flutter::BinaryMessenger* binary_messenger, + const std::string& message_channel_suffix); + static const flutter::StandardMessageCodec& GetCodec(); + void GetCustomToken( + std::function&& on_success, + std::function&& on_error); + + private: + flutter::BinaryMessenger* binary_messenger_; + std::string message_channel_suffix_; +}; + } // namespace firebase_app_check_windows #endif // PIGEON_MESSAGES_G_H_ diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart index 53a285e48729..b376615a7eb9 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/firebase_app_check_platform_interface.dart @@ -11,3 +11,5 @@ export 'src/method_channel/method_channel_firebase_app_check.dart'; export 'src/platform_interface/platform_interface_firebase_app_check.dart'; export 'src/web_providers.dart'; export 'src/windows_providers.dart'; +export 'src/pigeon/messages.pigeon.dart' + show FirebaseAppCheckFlutterApi, CustomAppCheckToken; diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart index 96afb6f2c999..ae9e0f8ab2cc 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/method_channel/method_channel_firebase_app_check.dart @@ -93,6 +93,7 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { }) async { try { String? debugToken; + String? windowsProvider; if (providerAndroid is AndroidDebugProvider && providerAndroid.debugToken != null) { debugToken = providerAndroid.debugToken; @@ -103,6 +104,9 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { providerWindows.debugToken != null) { debugToken = providerWindows.debugToken; } + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) { + windowsProvider = providerWindows?.type; + } await _pigeonApi.activate( app.name, @@ -121,6 +125,7 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { ) : null, debugToken, + windowsProvider, ); } on PlatformException catch (e, s) { convertPlatformException(e, s); diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart index fab0d53008a2..8872f9a95ab5 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/pigeon/messages.pigeon.dart @@ -3,7 +3,7 @@ // BSD-style license that can be found in the LICENSE file. // Autogenerated from Pigeon (v25.3.2), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers, require_trailing_commas +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers, require_trailing_commas, parameter_assignments import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -18,6 +18,87 @@ PlatformException _createConnectionError(String channelName) { ); } +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { + if (empty) { + return []; + } + if (error == null) { + return [result]; + } + return [error.code, error.message, error.details]; +} + +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + } + if (a is Map && b is Map) { + return a.length == b.length && + a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); + } + return a == b; +} + +/// Carries a minted App Check token plus the wall-clock expiry the Firebase +/// SDK should associate with it. Returning the expiry alongside the token lets +/// backends mint tokens with arbitrary lifetimes (short TTLs for a stricter +/// security posture, longer TTLs for fewer round-trips) without the plugin +/// hardcoding a refresh window. +class CustomAppCheckToken { + CustomAppCheckToken({ + required this.token, + required this.expireTimeMillis, + }); + + /// The App Check token string to send with Firebase requests. + String token; + + /// Absolute expiry as Unix epoch milliseconds (UTC). The Firebase SDK uses + /// this to decide when to refresh; a token returned with an expiry in the + /// past is treated as immediately expired. + int expireTimeMillis; + + List _toList() { + return [ + token, + expireTimeMillis, + ]; + } + + Object encode() { + return _toList(); + } + + static CustomAppCheckToken decode(Object result) { + result as List; + return CustomAppCheckToken( + token: result[0]! as String, + expireTimeMillis: result[1]! as int, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! CustomAppCheckToken || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -25,6 +106,9 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); + } else if (value is CustomAppCheckToken) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -33,6 +117,8 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { + case 129: + return CustomAppCheckToken.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -54,8 +140,12 @@ class FirebaseAppCheckHostApi { final String pigeonVar_messageChannelSuffix; - Future activate(String appName, String? androidProvider, - String? appleProvider, String? debugToken) async { + Future activate( + String appName, + String? androidProvider, + String? appleProvider, + String? debugToken, + String? windowsProvider) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -65,7 +155,13 @@ class FirebaseAppCheckHostApi { binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel - .send([appName, androidProvider, appleProvider, debugToken]); + .send([ + appName, + androidProvider, + appleProvider, + debugToken, + windowsProvider + ]); final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { @@ -196,3 +292,47 @@ class FirebaseAppCheckHostApi { } } } + +/// Dart-side handler invoked by the native plugin when the Firebase SDK needs +/// a fresh App Check token. Implementations typically call a backend service +/// (for example a Cloud Function with `enforceAppCheck: false`) that mints a +/// token using the Firebase Admin SDK. The native side awaits the future, +/// then hands the token to the Firebase SDK, which attaches it to subsequent +/// Firebase backend requests (Firestore, Functions, Storage, Auth, RTDB). +abstract class FirebaseAppCheckFlutterApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + Future getCustomToken(); + + static void setUp( + FirebaseAppCheckFlutterApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + try { + final CustomAppCheckToken output = await api.getCustomToken(); + return wrapResponse(result: output); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } + } +} diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/windows_providers.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/windows_providers.dart index b6b09e55b20b..be6f58d980f9 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/windows_providers.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/windows_providers.dart @@ -4,17 +4,50 @@ /// Base class for Windows App Check providers. /// -/// On Windows, only the [WindowsDebugProvider] is supported. The Firebase C++ -/// SDK does not support platform attestation providers (such as Play Integrity -/// or DeviceCheck) on desktop platforms. +/// The Firebase C++ SDK does not ship native platform attestation providers +/// (such as Play Integrity or DeviceCheck) on desktop, so Windows supports +/// [WindowsDebugProvider] for development and [WindowsCustomProvider] for +/// production builds that mint tokens via a backend. abstract class WindowsAppCheckProvider { final String type; const WindowsAppCheckProvider(this.type); } +/// Custom provider for Windows production builds. +/// +/// When activated, the Windows C++ plugin registers a custom +/// `AppCheckProvider` that calls into Dart via a Pigeon `FlutterApi` each time +/// the Firebase SDK needs a fresh App Check token. The Dart handler is +/// expected to call a backend service (typically a Cloud Function with +/// `enforceAppCheck: false`) that mints a valid App Check token using the +/// Firebase Admin SDK, then return both the token and its expiry. +/// +/// Register the Dart token handler before any Firebase operations that require +/// App Check, alongside `FirebaseAppCheck.instance.activate`: +/// +/// ```dart +/// FirebaseAppCheckFlutterApi.setUp(MyWindowsTokenHandler()); +/// +/// class MyWindowsTokenHandler implements FirebaseAppCheckFlutterApi { +/// @override +/// Future getCustomToken() async { +/// // Call your backend, e.g. a callable Cloud Function that uses +/// // admin.appCheck().createToken(windowsAppId). +/// final response = await myBackend.mintAppCheckToken(); +/// return CustomAppCheckToken( +/// token: response.token, +/// expireTimeMillis: response.expireTimeMillis, +/// ); +/// } +/// } +/// ``` +class WindowsCustomProvider extends WindowsAppCheckProvider { + const WindowsCustomProvider() : super('custom'); +} + /// Debug provider for Windows. /// -/// This is the **only** provider available on Windows. Unlike mobile platforms, +/// Intended for development and local testing only. Unlike mobile platforms, /// the desktop C++ SDK does **not** auto-generate a debug token. You must /// supply one explicitly. /// diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart index e84ff78ab5f4..67a24783a5c7 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/pigeons/messages.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: one_member_abstracts + import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( @@ -29,6 +31,7 @@ abstract class FirebaseAppCheckHostApi { String? androidProvider, String? appleProvider, String? debugToken, + String? windowsProvider, ); @async @@ -46,3 +49,35 @@ abstract class FirebaseAppCheckHostApi { @async String getLimitedUseAppCheckToken(String appName); } + +/// Carries a minted App Check token plus the wall-clock expiry the Firebase +/// SDK should associate with it. Returning the expiry alongside the token lets +/// backends mint tokens with arbitrary lifetimes (short TTLs for a stricter +/// security posture, longer TTLs for fewer round-trips) without the plugin +/// hardcoding a refresh window. +class CustomAppCheckToken { + CustomAppCheckToken({ + required this.token, + required this.expireTimeMillis, + }); + + /// The App Check token string to send with Firebase requests. + final String token; + + /// Absolute expiry as Unix epoch milliseconds (UTC). The Firebase SDK uses + /// this to decide when to refresh; a token returned with an expiry in the + /// past is treated as immediately expired. + final int expireTimeMillis; +} + +/// Dart-side handler invoked by the native plugin when the Firebase SDK needs +/// a fresh App Check token. Implementations typically call a backend service +/// (for example a Cloud Function with `enforceAppCheck: false`) that mints a +/// token using the Firebase Admin SDK. The native side awaits the future, +/// then hands the token to the Firebase SDK, which attaches it to subsequent +/// Firebase backend requests (Firestore, Functions, Storage, Auth, RTDB). +@FlutterApi() +abstract class FirebaseAppCheckFlutterApi { + @async + CustomAppCheckToken getCustomToken(); +} diff --git a/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart index 5f215cec19fc..c6ca01b17b09 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/test/method_channel_tests/method_channel_firebase_app_check_test.dart @@ -3,7 +3,10 @@ // found in the LICENSE file. import 'package:firebase_app_check_platform_interface/firebase_app_check_platform_interface.dart'; +import 'package:firebase_app_check_platform_interface/src/pigeon/messages.pigeon.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../mock.dart'; @@ -44,5 +47,168 @@ void main() { expect(appCheck.setInitialValues(), appCheck); }); }); + + group('activate() on Windows', () { + late BasicMessageChannel activateChannel; + late List activateMessages; + + setUp(() { + debugDefaultTargetPlatformOverride = TargetPlatform.windows; + activateChannel = const BasicMessageChannel( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate', + FirebaseAppCheckHostApi.pigeonChannelCodec, + ); + activateMessages = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(activateChannel, + (Object? message) async { + activateMessages.add(message); + return []; + }); + }); + + tearDown(() { + debugDefaultTargetPlatformOverride = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(activateChannel, null); + }); + + test('forwards WindowsCustomProvider over Pigeon', () async { + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + await appCheck.activate( + providerWindows: const WindowsCustomProvider(), + ); + + // Android/Apple slots carry method-channel defaults even on Windows. + expect(activateMessages, hasLength(1)); + expect(activateMessages.single, [ + 'secondaryApp', + 'playIntegrity', + 'deviceCheck', + null, + 'custom', + ]); + }); + + test( + 'forwards WindowsDebugProvider with an explicit debug token over Pigeon', + () async { + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + await appCheck.activate( + providerWindows: const WindowsDebugProvider( + debugToken: 'debug-token', + ), + ); + + expect(activateMessages, hasLength(1)); + expect(activateMessages.single, [ + 'secondaryApp', + 'playIntegrity', + 'deviceCheck', + 'debug-token', + 'debug', + ]); + }); + + test( + 'forwards WindowsDebugProvider with no explicit token as null ' + '(env-var fallback path)', () async { + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + // Null debugToken triggers the native APP_CHECK_DEBUG_TOKEN fallback. + await appCheck.activate( + providerWindows: const WindowsDebugProvider(), + ); + + expect(activateMessages, hasLength(1)); + expect(activateMessages.single, [ + 'secondaryApp', + 'playIntegrity', + 'deviceCheck', + null, + 'debug', + ]); + }); + }); + }); + + group('$FirebaseAppCheckFlutterApi', () { + const BasicMessageChannel flutterApiChannel = + BasicMessageChannel( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken', + FirebaseAppCheckFlutterApi.pigeonChannelCodec, + ); + + tearDown(() { + FirebaseAppCheckFlutterApi.setUp(null); + }); + + test('returns CustomAppCheckToken in a success envelope', () async { + final token = CustomAppCheckToken( + token: 'app-check-token', + expireTimeMillis: 1735689600000, + ); + FirebaseAppCheckFlutterApi.setUp( + _TestFirebaseAppCheckFlutterApi( + onGetCustomToken: () async => token, + ), + ); + + final replyData = await TestDefaultBinaryMessengerBinding + .instance.defaultBinaryMessenger + .handlePlatformMessage( + flutterApiChannel.name, + flutterApiChannel.codec.encodeMessage(null), + null, + ); + final reply = + flutterApiChannel.codec.decodeMessage(replyData) as List?; + + expect(reply, [token]); + }); + + test('returns a PlatformException envelope when the handler throws', + () async { + FirebaseAppCheckFlutterApi.setUp( + _TestFirebaseAppCheckFlutterApi( + onGetCustomToken: () async { + throw PlatformException( + code: 'token-error', + message: 'Failed to mint App Check token', + details: {'source': 'test'}, + ); + }, + ), + ); + + final replyData = await TestDefaultBinaryMessengerBinding + .instance.defaultBinaryMessenger + .handlePlatformMessage( + flutterApiChannel.name, + flutterApiChannel.codec.encodeMessage(null), + null, + ); + final reply = + flutterApiChannel.codec.decodeMessage(replyData) as List?; + + expect(reply, [ + 'token-error', + 'Failed to mint App Check token', + {'source': 'test'}, + ]); + }); }); } + +class _TestFirebaseAppCheckFlutterApi implements FirebaseAppCheckFlutterApi { + _TestFirebaseAppCheckFlutterApi({ + required this.onGetCustomToken, + }); + + final Future Function() onGetCustomToken; + + @override + Future getCustomToken() => onGetCustomToken(); +} diff --git a/packages/firebase_core/firebase_core/windows/CMakeLists.txt b/packages/firebase_core/firebase_core/windows/CMakeLists.txt index 277ea0e10c24..86c39c045600 100644 --- a/packages/firebase_core/firebase_core/windows/CMakeLists.txt +++ b/packages/firebase_core/firebase_core/windows/CMakeLists.txt @@ -118,7 +118,13 @@ if(NOT MSVC_RUNTIME_MODE) set(MSVC_RUNTIME_MODE MD) endif() -add_subdirectory(${FIREBASE_CPP_SDK_DIR} bin/ EXCLUDE_FROM_ALL) +set(FLUTTERFIRE_FIREBASE_CPP_SDK_BINARY_DIR + "${CMAKE_BINARY_DIR}/firebase_cpp_sdk") +if(NOT TARGET firebase_app) + add_subdirectory(${FIREBASE_CPP_SDK_DIR} + ${FLUTTERFIRE_FIREBASE_CPP_SDK_BINARY_DIR} + EXCLUDE_FROM_ALL) +endif() target_include_directories(${PLUGIN_NAME} INTERFACE "${FIREBASE_CPP_SDK_DIR}/include") @@ -129,6 +135,7 @@ foreach(firebase_lib IN ITEMS ${FIREBASE_RELEASE_PATH_LIBS}) set_target_properties(${firebase_lib} PROPERTIES IMPORTED_LOCATION_DEBUG "${firebase_lib_path}" IMPORTED_LOCATION_RELEASE "${firebase_lib_release_path}" + IMPORTED_LOCATION_PROFILE "${firebase_lib_release_path}" ) endforeach()