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 99aab95ea256..a4d7031c3fd0 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 @@ -53,6 +53,7 @@ class FirebaseAppCheckPlugin : FlutterFirebasePlugin, FlutterPlugin, FirebaseApp 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 4cd7a39bc1b4..b76562cc227b 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 @@ -17,6 +17,11 @@ 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) } @@ -31,6 +36,150 @@ private object GeneratedAndroidFirebaseAppCheckPigeonUtils { "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) } } + + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } + 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) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true + } + if (a is Array<*> && b is Array<*>) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true + } + if (a is List<*> && b is List<*>) { + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true + } + if (a is Map<*, *> && b is Map<*, *>) { + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false + } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) + } + return a == b + } + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } } /** @@ -46,13 +195,78 @@ class FlutterError( val details: Any? = null ) : RuntimeException() +/** + * 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 == null || other.javaClass != javaClass) { + return false + } + if (this === other) { + return true + } + val other = other as CustomAppCheckToken + return GeneratedAndroidFirebaseAppCheckPigeonUtils.deepEquals(this.token, other.token) && + GeneratedAndroidFirebaseAppCheckPigeonUtils.deepEquals( + this.expireTimeMillis, other.expireTimeMillis) + } + + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + GeneratedAndroidFirebaseAppCheckPigeonUtils.deepHash(this.token) + result = + 31 * result + GeneratedAndroidFirebaseAppCheckPigeonUtils.deepHash(this.expireTimeMillis) + return result + } +} + 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) + } } } @@ -63,6 +277,7 @@ interface FirebaseAppCheckHostApi { androidProvider: String?, appleProvider: String?, debugToken: String?, + windowsProvider: String?, callback: (Result) -> Unit ) @@ -106,15 +321,20 @@ 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 error = result.exceptionOrNull() - if (error != null) { - reply.reply(GeneratedAndroidFirebaseAppCheckPigeonUtils.wrapError(error)) - } else { - reply.reply(GeneratedAndroidFirebaseAppCheckPigeonUtils.wrapResult(null)) - } - } + 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)) + } else { + reply.reply(GeneratedAndroidFirebaseAppCheckPigeonUtils.wrapResult(null)) + } + } } } else { channel.setMessageHandler(null) @@ -221,3 +441,50 @@ 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 d8f3a100c89b..a40c1d435ba3 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 @@ -27,12 +27,13 @@ final class PigeonError: Error { } var localizedDescription: String { - "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" } } private func wrapResult(_ result: Any?) -> [Any?] { - [result] + return [result] } private func wrapError(_ error: Any) -> [Any?] { @@ -57,8 +58,12 @@ private func wrapError(_ error: Any) -> [Any?] { ] } +private func createConnectionError(withChannelName channelName: String) -> PigeonError { + return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") +} + private func isNullish(_ value: Any?) -> Bool { - value is NSNull || value == nil + return value is NSNull || value == nil } private func nilOrValue(_ value: Any?) -> T? { @@ -66,73 +71,224 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } -private class FirebaseAppCheckMessagesPigeonCodecReader: FlutterStandardReader {} +private func doubleEqualsFirebaseAppCheckMessages(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashFirebaseAppCheckMessages(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8000000000000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + +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 (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: + return true + + case is (Void, Void): + return true + + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsFirebaseAppCheckMessages(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsFirebaseAppCheckMessages(element, rhsArray[index]) { + return false + } + } + return true + + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsFirebaseAppCheckMessages(lhsKey, rhsKey) { + if deepEqualsFirebaseAppCheckMessages(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEqualsFirebaseAppCheckMessages(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + + default: + return false + } +} + +func deepHashFirebaseAppCheckMessages(value: Any?, hasher: inout Hasher) { + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashFirebaseAppCheckMessages(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashFirebaseAppCheckMessages(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashFirebaseAppCheckMessages(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashFirebaseAppCheckMessages(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashFirebaseAppCheckMessages(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) + } + } else { + hasher.combine(0) + } +} + + +/// 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 + -private class FirebaseAppCheckMessagesPigeonCodecWriter: FlutterStandardWriter {} + // 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?] { + return [ + token, + expireTimeMillis, + ] + } + static func == (lhs: CustomAppCheckToken, rhs: CustomAppCheckToken) -> Bool { + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsFirebaseAppCheckMessages(lhs.token, rhs.token) && deepEqualsFirebaseAppCheckMessages(lhs.expireTimeMillis, rhs.expireTimeMillis) + } + + func hash(into hasher: inout Hasher) { + hasher.combine("CustomAppCheckToken") + deepHashFirebaseAppCheckMessages(value: token, hasher: &hasher) + deepHashFirebaseAppCheckMessages(value: expireTimeMillis, hasher: &hasher) + } +} + +private class FirebaseAppCheckMessagesPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + return CustomAppCheckToken.fromList(self.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 { - FirebaseAppCheckMessagesPigeonCodecReader(data: data) + return FirebaseAppCheckMessagesPigeonCodecReader(data: data) } override func writer(with data: NSMutableData) -> FlutterStandardWriter { - FirebaseAppCheckMessagesPigeonCodecWriter(data: data) + return FirebaseAppCheckMessagesPigeonCodecWriter(data: data) } } class FirebaseAppCheckMessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = FirebaseAppCheckMessagesPigeonCodec( - readerWriter: FirebaseAppCheckMessagesPigeonCodecReaderWriter() - ) + static let shared = FirebaseAppCheckMessagesPigeonCodec(readerWriter: FirebaseAppCheckMessagesPigeonCodecReaderWriter()) } + /// 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) - func getToken( - appName: String, forceRefresh: Bool, - completion: @escaping (Result) -> Void) - func setTokenAutoRefreshEnabled( - appName: String, isTokenAutoRefreshEnabled: Bool, - completion: @escaping (Result) -> Void) + func activate(appName: String, androidProvider: String?, appleProvider: String?, debugToken: String?, windowsProvider: String?, completion: @escaping (Result) -> Void) + func getToken(appName: String, forceRefresh: Bool, completion: @escaping (Result) -> Void) + func setTokenAutoRefreshEnabled(appName: String, isTokenAutoRefreshEnabled: Bool, completion: @escaping (Result) -> Void) func registerTokenListener(appName: String, completion: @escaping (Result) -> Void) - func getLimitedUseAppCheckToken( - appName: String, - completion: @escaping (Result) -> Void) + func getLimitedUseAppCheckToken(appName: String, completion: @escaping (Result) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. class FirebaseAppCheckHostApiSetup { - static var codec: FlutterStandardMessageCodec { - FirebaseAppCheckMessagesPigeonCodec.shared - } - - /// Sets up an instance of `FirebaseAppCheckHostApi` to handle messages through the - /// `binaryMessenger`. - static func setUp( - binaryMessenger: FlutterBinaryMessenger, api: FirebaseAppCheckHostApi?, - messageChannelSuffix: String = "" - ) { + static var codec: FlutterStandardMessageCodec { FirebaseAppCheckMessagesPigeonCodec.shared } + /// Sets up an instance of `FirebaseAppCheckHostApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: FirebaseAppCheckHostApi?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let activateChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec - ) - if let api { + let activateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { activateChannel.setMessageHandler { message, reply in let args = message as! [Any?] let appNameArg = args[0] as! String let androidProviderArg: String? = nilOrValue(args[1]) let appleProviderArg: String? = nilOrValue(args[2]) let debugTokenArg: String? = nilOrValue(args[3]) - api.activate( - appName: appNameArg, androidProvider: androidProviderArg, appleProvider: appleProviderArg, - debugToken: debugTokenArg - ) { result in + let windowsProviderArg: String? = nilOrValue(args[4]) + api.activate(appName: appNameArg, androidProvider: androidProviderArg, appleProvider: appleProviderArg, debugToken: debugTokenArg, windowsProvider: windowsProviderArg) { result in switch result { case .success: reply(wrapResult(nil)) @@ -144,12 +300,8 @@ class FirebaseAppCheckHostApiSetup { } else { activateChannel.setMessageHandler(nil) } - let getTokenChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getToken\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec - ) - if let api { + let getTokenChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getToken\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { getTokenChannel.setMessageHandler { message, reply in let args = message as! [Any?] let appNameArg = args[0] as! String @@ -166,19 +318,13 @@ class FirebaseAppCheckHostApiSetup { } else { getTokenChannel.setMessageHandler(nil) } - let setTokenAutoRefreshEnabledChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.setTokenAutoRefreshEnabled\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec - ) - if let api { + let setTokenAutoRefreshEnabledChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.setTokenAutoRefreshEnabled\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { setTokenAutoRefreshEnabledChannel.setMessageHandler { message, reply in let args = message as! [Any?] let appNameArg = args[0] as! String let isTokenAutoRefreshEnabledArg = args[1] as! Bool - api.setTokenAutoRefreshEnabled( - appName: appNameArg, isTokenAutoRefreshEnabled: isTokenAutoRefreshEnabledArg - ) { result in + api.setTokenAutoRefreshEnabled(appName: appNameArg, isTokenAutoRefreshEnabled: isTokenAutoRefreshEnabledArg) { result in switch result { case .success: reply(wrapResult(nil)) @@ -190,12 +336,8 @@ class FirebaseAppCheckHostApiSetup { } else { setTokenAutoRefreshEnabledChannel.setMessageHandler(nil) } - let registerTokenListenerChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.registerTokenListener\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec - ) - if let api { + let registerTokenListenerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.registerTokenListener\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { registerTokenListenerChannel.setMessageHandler { message, reply in let args = message as! [Any?] let appNameArg = args[0] as! String @@ -211,12 +353,8 @@ class FirebaseAppCheckHostApiSetup { } else { registerTokenListenerChannel.setMessageHandler(nil) } - let getLimitedUseAppCheckTokenChannel = FlutterBasicMessageChannel( - name: - "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getLimitedUseAppCheckToken\(channelSuffix)", - binaryMessenger: binaryMessenger, codec: codec - ) - if let api { + let getLimitedUseAppCheckTokenChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.getLimitedUseAppCheckToken\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { getLimitedUseAppCheckTokenChannel.setMessageHandler { message, reply in let args = message as! [Any?] let appNameArg = args[0] as! String @@ -234,3 +372,46 @@ 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 { + return FirebaseAppCheckMessagesPigeonCodec.shared + } + func getCustomToken(completion: @escaping (Result) -> Void) { + let channelName: String = "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 2d63a7652e18..3ebd48856730 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 @@ -64,7 +64,7 @@ public class FirebaseAppCheckPlugin: NSObject, FlutterPlugin, 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 { 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 5e6a8cc98b81..f1ea29fb63cd 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 @@ -27,7 +27,9 @@ export 'package:firebase_app_check_platform_interface/firebase_app_check_platfor WebProvider, WebReCaptchaProvider, WindowsAppCheckProvider, - WindowsDebugProvider; + WindowsDebugProvider, + WindowsCustomProvider, + CustomAppCheckToken; export 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' show FirebaseException; diff --git a/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart index 0553fa116e03..ae49b95050b2 100644 --- a/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check/lib/src/firebase_app_check.dart @@ -73,12 +73,12 @@ class FirebaseAppCheck extends FirebasePlugin implements FirebaseService { /// "app attest with fallback to device check" via `AppleAppCheckProvider`. /// Note: App Attest is only available on iOS 14.0+ and macOS 14.0+. /// - /// **Windows**: Only the debug provider is supported. You **must** supply a - /// debug token — the desktop C++ SDK does not auto-generate one. Either pass - /// it via `providerWindows: WindowsDebugProvider(debugToken: 'your-token')` - /// or set the `APP_CHECK_DEBUG_TOKEN` environment variable. The token must - /// first be registered in the Firebase Console under - /// *App Check → Apps → Manage debug tokens*. + /// **Windows**: Use `providerWindows` to configure either + /// [WindowsCustomProvider] for production token minting or + /// [WindowsDebugProvider] for development. The desktop C++ SDK does not + /// auto-generate debug tokens. Either pass a registered token via + /// `providerWindows: WindowsDebugProvider(debugToken: 'your-token')` or set + /// the `APP_CHECK_DEBUG_TOKEN` environment variable. /// /// ## Migration Notice /// 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..e4371030efe0 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,54 @@ 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, const std::string& app_name) + : flutter_api_(std::make_unique( + binary_messenger, app_name)) {} + +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) { + const std::string app_name = app == nullptr ? "" : app->name(); + auto& provider = providers_[app_name]; + if (!provider) { + provider = std::make_unique( + binary_messenger_, app_name); + } + 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 +214,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); } @@ -230,7 +283,6 @@ void FirebaseAppCheckPlugin::GetLimitedUseAppCheckToken( const std::string& app_name, std::function reply)> result) { AppCheck* app_check = GetAppCheckFromPigeon(app_name); - Future future = app_check->GetLimitedUseAppCheckToken(); future.OnCompletion([result](const Future& completed_future) { if (completed_future.error() != 0) { 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..7677a3527581 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,38 @@ 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, const std::string& app_name); + 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::map> + providers_; +}; + class FirebaseAppCheckPlugin : public flutter::Plugin, public FirebaseAppCheckHostApi { friend class TokenStreamHandler; @@ -43,6 +76,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 +92,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 0da3e3c1ded5..9eea265bc7f7 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 @@ -239,16 +239,87 @@ size_t PigeonInternalDeepHash(const ::flutter::EncodableValue& v) { } } // namespace +// 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; +} + +bool CustomAppCheckToken::operator==(const CustomAppCheckToken& other) const { + return PigeonInternalDeepEquals(token_, other.token_) && + PigeonInternalDeepEquals(expire_time_millis_, + other.expire_time_millis_); +} + +bool CustomAppCheckToken::operator!=(const CustomAppCheckToken& other) const { + return !(*this == other); +} + +size_t CustomAppCheckToken::Hash() const { + size_t result = 1; + result = result * 31 + PigeonInternalDeepHash(token_); + result = result * 31 + PigeonInternalDeepHash(expire_time_millis_); + return result; +} + +size_t PigeonInternalDeepHash(const CustomAppCheckToken& v) { return v.Hash(); } 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); } @@ -302,8 +373,12 @@ void FirebaseAppCheckHostApi::SetUp( 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())); @@ -514,4 +589,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 50ae482963dc..450a16da3663 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,53 @@ 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); + + bool operator==(const CustomAppCheckToken& other) const; + bool operator!=(const CustomAppCheckToken& other) const; + /// Returns a hash code value for the object. This method is supported for the + /// benefit of hash tables. + size_t Hash() const; + + 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: @@ -85,6 +126,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, @@ -115,5 +157,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/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 b7f2d035d212..f3c796a477bf 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 @@ -10,14 +10,46 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../../firebase_app_check_platform_interface.dart'; -import '../pigeon/messages.pigeon.dart'; +import '../pigeon/messages.pigeon.dart' as pigeon; import 'utils/exception.dart'; import 'utils/provider_to_string.dart'; +class _WindowsCustomProviderFlutterApi + extends pigeon.FirebaseAppCheckFlutterApi { + _WindowsCustomProviderFlutterApi(this.appName); + + final String appName; + + @override + Future getCustomToken() async { + final provider = + MethodChannelFirebaseAppCheck._windowsCustomProviders[appName]; + if (provider == null) { + throw StateError( + 'No WindowsCustomProvider has been activated for app $appName.', + ); + } + + final token = await provider.fetchToken(); + return pigeon.CustomAppCheckToken( + token: token.token, + expireTimeMillis: token.expireTimeMillis, + ); + } +} + class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { /// Create an instance of [MethodChannelFirebaseAppCheck]. MethodChannelFirebaseAppCheck({required FirebaseApp app}) : super(appInstance: app) { + final flutterApi = _windowsCustomProviderFlutterApis.putIfAbsent( + app.name, + () => _WindowsCustomProviderFlutterApi(app.name), + ); + pigeon.FirebaseAppCheckFlutterApi.setUp( + flutterApi, + messageChannelSuffix: app.name, + ); _tokenChangesListeners[app.name] = StreamController.broadcast(); _listenerRegistration = _registerTokenListener(app); } @@ -55,7 +87,11 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { {}; /// The Pigeon API used for platform communication. - final FirebaseAppCheckHostApi _pigeonApi = FirebaseAppCheckHostApi(); + final pigeon.FirebaseAppCheckHostApi _pigeonApi = + pigeon.FirebaseAppCheckHostApi(); + static final Map + _windowsCustomProviderFlutterApis = {}; + static final Map _windowsCustomProviders = {}; late final Future _listenerRegistration; StreamSubscription? _subscription; bool _isDisposed = false; @@ -86,6 +122,13 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { await _subscription?.cancel(); _subscription = null; await _tokenChangesListeners.remove(app.name)?.close(); + _windowsCustomProviders.remove(app.name); + if (_windowsCustomProviderFlutterApis.remove(app.name) != null) { + pigeon.FirebaseAppCheckFlutterApi.setUp( + null, + messageChannelSuffix: app.name, + ); + } _methodChannelFirebaseAppCheckInstances.remove(app.name); } @@ -112,6 +155,7 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { WindowsAppCheckProvider? providerWindows, }) async { try { + _setWindowsCustomProvider(providerWindows); await _pigeonApi.activate( app.name, defaultTargetPlatform == TargetPlatform.android || kDebugMode @@ -133,12 +177,23 @@ class MethodChannelFirebaseAppCheck extends FirebaseAppCheckPlatform { providerApple: providerApple, providerWindows: providerWindows, ), + _getWindowsProvider(providerWindows), ); } on PlatformException catch (e, s) { convertPlatformException(e, s); } } + void _setWindowsCustomProvider( + WindowsAppCheckProvider? providerWindows, + ) { + if (providerWindows is WindowsCustomProvider) { + _windowsCustomProviders[app.name] = providerWindows; + } else { + _windowsCustomProviders.remove(app.name); + } + } + @override Future getToken(bool forceRefresh) async { try { @@ -201,3 +256,11 @@ String? _getDebugToken({ return null; } } + +String? _getWindowsProvider(WindowsAppCheckProvider? providerWindows) { + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) { + return providerWindows?.type; + } + + return null; +} 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 ca154bea1029..458f8cc2fe71 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 @@ -37,6 +37,135 @@ Object? _extractReplyValueOrThrow( return replyList.firstOrNull; } +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 (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == 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) { + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; + } + return a == b; +} + +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + +/// 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(token, other.token) && + _deepEquals(expireTimeMillis, other.expireTimeMillis); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => _deepHash([runtimeType, ..._toList()]); +} + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -44,6 +173,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); } @@ -52,6 +184,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); } @@ -73,8 +207,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 pigeonVar_channelName = 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( @@ -83,7 +221,13 @@ class FirebaseAppCheckHostApi { binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel - .send([appName, androidProvider, appleProvider, debugToken]); + .send([ + appName, + androidProvider, + appleProvider, + debugToken, + windowsProvider + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; _extractReplyValueOrThrow( @@ -173,3 +317,45 @@ class FirebaseAppCheckHostApi { return pigeonVar_replyValue! as String; } } + +/// 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 pigeonVar_channel = BasicMessageChannel( + '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/platform_interface/platform_interface_firebase_app_check.dart b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/platform_interface/platform_interface_firebase_app_check.dart index 3346544a3bed..08789b066fce 100644 --- a/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/platform_interface/platform_interface_firebase_app_check.dart +++ b/packages/firebase_app_check/firebase_app_check_platform_interface/lib/src/platform_interface/platform_interface_firebase_app_check.dart @@ -67,12 +67,12 @@ abstract class FirebaseAppCheckPlatform extends PlatformInterface { /// "app attest with fallback to device check" via `AppleAppCheckProvider`. /// Note: App Attest is only available on iOS 14.0+ and macOS 14.0+. /// - /// **Windows**: Only the debug provider is supported. You **must** supply a - /// debug token — the desktop C++ SDK does not auto-generate one. Either pass - /// it via `providerWindows: WindowsDebugProvider(debugToken: 'your-token')` - /// or set the `APP_CHECK_DEBUG_TOKEN` environment variable. The token must - /// first be registered in the Firebase Console under - /// *App Check → Apps → Manage debug tokens*. + /// **Windows**: Use `providerWindows` to configure either + /// [WindowsCustomProvider] for production token minting or + /// [WindowsDebugProvider] for development. The desktop C++ SDK does not + /// auto-generate debug tokens. Either pass a registered token via + /// `providerWindows: WindowsDebugProvider(debugToken: 'your-token')` or set + /// the `APP_CHECK_DEBUG_TOKEN` environment variable. /// /// ## Migration Notice /// 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..903cc5e2c5e1 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,69 @@ /// 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); } +/// Carries a minted App Check token and its expiry. +class CustomAppCheckToken { + /// Creates a custom App Check token result. + const 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). + final int expireTimeMillis; +} + +/// Custom provider for Windows production builds. +/// +/// When activated, the Windows C++ plugin registers a custom +/// `AppCheckProvider` that calls [fetchToken] each time the Firebase SDK needs +/// a fresh App Check token. The callback 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 callback before any Firebase operations that require App Check: +/// +/// ```dart +/// await FirebaseAppCheck.instance.activate( +/// providerWindows: WindowsCustomProvider( +/// fetchToken: () 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 { + /// Creates a Windows custom provider. + const WindowsCustomProvider({ + required this.fetchToken, + }) : super('custom'); + + /// Callback invoked when the native Firebase SDK needs a fresh token. + final Future Function() fetchToken; +} + /// 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 7c60c253ff3d..25cd1c683fac 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,11 +3,12 @@ // 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_app_check_platform_interface/src/pigeon/messages.pigeon.dart' + as pigeon; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; import '../mock.dart'; @@ -15,6 +16,9 @@ void main() { setupFirebaseAppCheckMocks(); late FirebaseApp secondaryApp; + const activateChannelName = + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate'; + group('$MethodChannelFirebaseAppCheck', () { setUpAll(() async { await Firebase.initializeApp(); @@ -33,7 +37,7 @@ void main() { debugDefaultTargetPlatformOverride = null; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMessageHandler( - 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate', + activateChannelName, null, ); }); @@ -63,13 +67,14 @@ void main() { final calls = >[]; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMessageHandler( - 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate', + activateChannelName, (ByteData? message) async { calls.add( - FirebaseAppCheckHostApi.pigeonChannelCodec.decodeMessage(message)! - as List, + pigeon.FirebaseAppCheckHostApi.pigeonChannelCodec + .decodeMessage(message)! as List, ); - return FirebaseAppCheckHostApi.pigeonChannelCodec.encodeMessage( + return pigeon.FirebaseAppCheckHostApi.pigeonChannelCodec + .encodeMessage( [], ); }, @@ -95,13 +100,14 @@ void main() { final calls = >[]; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMessageHandler( - 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckHostApi.activate', + activateChannelName, (ByteData? message) async { calls.add( - FirebaseAppCheckHostApi.pigeonChannelCodec.decodeMessage(message)! - as List, + pigeon.FirebaseAppCheckHostApi.pigeonChannelCodec + .decodeMessage(message)! as List, ); - return FirebaseAppCheckHostApi.pigeonChannelCodec.encodeMessage( + return pigeon.FirebaseAppCheckHostApi.pigeonChannelCodec + .encodeMessage( [], ); }, @@ -121,6 +127,253 @@ void main() { expect(calls, hasLength(1)); expect(calls.single[3], 'android-debug-token'); }); + + group('on Windows', () { + late BasicMessageChannel activateChannel; + late List activateMessages; + + setUp(() { + debugDefaultTargetPlatformOverride = TargetPlatform.windows; + activateChannel = const BasicMessageChannel( + activateChannelName, + pigeon.FirebaseAppCheckHostApi.pigeonChannelCodec, + ); + activateMessages = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(activateChannel, + (Object? message) async { + activateMessages.add(message); + return []; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockDecodedMessageHandler(activateChannel, null); + }); + + test('forwards WindowsCustomProvider over Pigeon', () async { + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + await appCheck.activate( + providerWindows: WindowsCustomProvider( + fetchToken: () async => const CustomAppCheckToken( + token: 'app-check-token', + expireTimeMillis: 1735689600000, + ), + ), + ); + + // 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('Windows custom token callback', () { + BasicMessageChannel flutterApiChannelFor(String appName) { + return BasicMessageChannel( + 'dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken.$appName', + pigeon.FirebaseAppCheckFlutterApi.pigeonChannelCodec, + ); + } + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler( + activateChannelName, + (ByteData? message) async { + return pigeon.FirebaseAppCheckHostApi.pigeonChannelCodec + .encodeMessage( + [], + ); + }, + ); + }); + + tearDown(() { + pigeon.FirebaseAppCheckFlutterApi.setUp( + null, + messageChannelSuffix: Firebase.app().name, + ); + pigeon.FirebaseAppCheckFlutterApi.setUp( + null, + messageChannelSuffix: secondaryApp.name, + ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMessageHandler( + activateChannelName, + null, + ); + }); + + test('returns tokens from the active WindowsCustomProvider fetchToken', + () async { + const token = CustomAppCheckToken( + token: 'app-check-token', + expireTimeMillis: 1735689600000, + ); + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + await appCheck.activate( + providerWindows: WindowsCustomProvider( + fetchToken: () async => token, + ), + ); + + final flutterApiChannel = flutterApiChannelFor(secondaryApp.name); + final replyData = await TestDefaultBinaryMessengerBinding + .instance.defaultBinaryMessenger + .handlePlatformMessage( + flutterApiChannel.name, + flutterApiChannel.codec.encodeMessage(null), + null, + ); + final reply = + flutterApiChannel.codec.decodeMessage(replyData) as List?; + + final customToken = reply!.single! as pigeon.CustomAppCheckToken; + expect(customToken.token, 'app-check-token'); + expect(customToken.expireTimeMillis, 1735689600000); + }); + + test('uses the provider registered for the requested app', () async { + final defaultAppCheck = + MethodChannelFirebaseAppCheck(app: Firebase.app()); + final secondaryAppCheck = + MethodChannelFirebaseAppCheck(app: secondaryApp); + + await defaultAppCheck.activate( + providerWindows: WindowsCustomProvider( + fetchToken: () async => const CustomAppCheckToken( + token: 'default-app-token', + expireTimeMillis: 1735689600000, + ), + ), + ); + await secondaryAppCheck.activate( + providerWindows: WindowsCustomProvider( + fetchToken: () async => const CustomAppCheckToken( + token: 'secondary-app-token', + expireTimeMillis: 1735689700000, + ), + ), + ); + + final defaultChannel = flutterApiChannelFor(Firebase.app().name); + final secondaryChannel = flutterApiChannelFor(secondaryApp.name); + + final defaultReplyData = await TestDefaultBinaryMessengerBinding + .instance.defaultBinaryMessenger + .handlePlatformMessage( + defaultChannel.name, + defaultChannel.codec.encodeMessage(null), + null, + ); + final secondaryReplyData = await TestDefaultBinaryMessengerBinding + .instance.defaultBinaryMessenger + .handlePlatformMessage( + secondaryChannel.name, + secondaryChannel.codec.encodeMessage(null), + null, + ); + + final defaultReply = defaultChannel.codec.decodeMessage(defaultReplyData) + as List?; + final secondaryReply = secondaryChannel.codec + .decodeMessage(secondaryReplyData) as List?; + + final defaultToken = defaultReply!.single! as pigeon.CustomAppCheckToken; + final secondaryToken = + secondaryReply!.single! as pigeon.CustomAppCheckToken; + + expect(defaultToken.token, 'default-app-token'); + expect(defaultToken.expireTimeMillis, 1735689600000); + expect(secondaryToken.token, 'secondary-app-token'); + expect(secondaryToken.expireTimeMillis, 1735689700000); + }); + + test('returns a PlatformException envelope when fetchToken throws', + () async { + final appCheck = MethodChannelFirebaseAppCheck(app: secondaryApp); + + await appCheck.activate( + providerWindows: WindowsCustomProvider( + fetchToken: () async { + throw PlatformException( + code: 'token-error', + message: 'Failed to mint App Check token', + details: {'source': 'test'}, + ); + }, + ), + ); + + final flutterApiChannel = flutterApiChannelFor(secondaryApp.name); + 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'}, + ]); }); group('activate() with Recaptcha', () { 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()