Skip to content

Commit 0964913

Browse files
committed
refactor(app_check,windows): return token and expiry from Dart custom provider, clarify provider docs
The Dart FlutterApi now returns CustomAppCheckToken, a class carrying both the minted token and its wall-clock expiry in Unix epoch milliseconds, instead of just a string. This lets backends mint tokens with arbitrary lifetimes without the plugin hardcoding a refresh window, and it removes a 55-minute constant from the C++ plugin that assumed every backend mints ~1-hour tokens. Short-lived tokens for stricter posture and longer-lived tokens for fewer round-trips are both supported by the same API. The Windows plugin's GetToken now pipes the expiry from Dart straight through to AppCheckToken.expire_time_millis, so the Firebase C++ SDK caches for the exact lifetime the backend chose. Also updates stale provider docs on windows_providers.dart (the base and debug-provider docs previously described debug as the only supported Windows provider, which was already out of date once custom support landed) and expands WindowsCustomProvider's docs with a usage example showing a FirebaseAppCheckFlutterApi implementation. Regenerates all Pigeon bindings (Dart, Kotlin, Swift, C++) from the updated source.
1 parent 976a491 commit 0964913

File tree

8 files changed

+473
-45
lines changed

8 files changed

+473
-45
lines changed

packages/firebase_app_check/firebase_app_check/android/src/main/kotlin/io/flutter/plugins/firebase/appcheck/GeneratedAndroidFirebaseAppCheck.g.kt

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,36 @@ private object GeneratedAndroidFirebaseAppCheckPigeonUtils {
4040
)
4141
}
4242
}
43+
fun deepEquals(a: Any?, b: Any?): Boolean {
44+
if (a is ByteArray && b is ByteArray) {
45+
return a.contentEquals(b)
46+
}
47+
if (a is IntArray && b is IntArray) {
48+
return a.contentEquals(b)
49+
}
50+
if (a is LongArray && b is LongArray) {
51+
return a.contentEquals(b)
52+
}
53+
if (a is DoubleArray && b is DoubleArray) {
54+
return a.contentEquals(b)
55+
}
56+
if (a is Array<*> && b is Array<*>) {
57+
return a.size == b.size &&
58+
a.indices.all{ deepEquals(a[it], b[it]) }
59+
}
60+
if (a is List<*> && b is List<*>) {
61+
return a.size == b.size &&
62+
a.indices.all{ deepEquals(a[it], b[it]) }
63+
}
64+
if (a is Map<*, *> && b is Map<*, *>) {
65+
return a.size == b.size && a.all {
66+
(b as Map<Any?, Any?>).containsKey(it.key) &&
67+
deepEquals(it.value, b[it.key])
68+
}
69+
}
70+
return a == b
71+
}
72+
4373
}
4474

4575
/**
@@ -53,12 +83,70 @@ class FlutterError (
5383
override val message: String? = null,
5484
val details: Any? = null
5585
) : Throwable()
86+
87+
/**
88+
* Carries a minted App Check token plus the wall-clock expiry the Firebase
89+
* SDK should associate with it. Returning the expiry alongside the token lets
90+
* backends mint tokens with arbitrary lifetimes (short TTLs for a stricter
91+
* security posture, longer TTLs for fewer round-trips) without the plugin
92+
* hardcoding a refresh window.
93+
*
94+
* Generated class from Pigeon that represents data sent in messages.
95+
*/
96+
data class CustomAppCheckToken (
97+
/** The App Check token string to send with Firebase requests. */
98+
val token: String,
99+
/**
100+
* Absolute expiry as Unix epoch milliseconds (UTC). The Firebase SDK uses
101+
* this to decide when to refresh; a token returned with an expiry in the
102+
* past is treated as immediately expired.
103+
*/
104+
val expireTimeMillis: Long
105+
)
106+
{
107+
companion object {
108+
fun fromList(pigeonVar_list: List<Any?>): CustomAppCheckToken {
109+
val token = pigeonVar_list[0] as String
110+
val expireTimeMillis = pigeonVar_list[1] as Long
111+
return CustomAppCheckToken(token, expireTimeMillis)
112+
}
113+
}
114+
fun toList(): List<Any?> {
115+
return listOf(
116+
token,
117+
expireTimeMillis,
118+
)
119+
}
120+
override fun equals(other: Any?): Boolean {
121+
if (other !is CustomAppCheckToken) {
122+
return false
123+
}
124+
if (this === other) {
125+
return true
126+
}
127+
return GeneratedAndroidFirebaseAppCheckPigeonUtils.deepEquals(toList(), other.toList()) }
128+
129+
override fun hashCode(): Int = toList().hashCode()
130+
}
56131
private open class GeneratedAndroidFirebaseAppCheckPigeonCodec : StandardMessageCodec() {
57132
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
58-
return super.readValueOfType(type, buffer)
133+
return when (type) {
134+
129.toByte() -> {
135+
return (readValue(buffer) as? List<Any?>)?.let {
136+
CustomAppCheckToken.fromList(it)
137+
}
138+
}
139+
else -> super.readValueOfType(type, buffer)
140+
}
59141
}
60142
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
61-
super.writeValue(stream, value)
143+
when (value) {
144+
is CustomAppCheckToken -> {
145+
stream.write(129)
146+
writeValue(stream, value.toList())
147+
}
148+
else -> super.writeValue(stream, value)
149+
}
62150
}
63151
}
64152

@@ -187,15 +275,24 @@ interface FirebaseAppCheckHostApi {
187275
}
188276
}
189277
}
190-
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
278+
/**
279+
* Dart-side handler invoked by the native plugin when the Firebase SDK needs
280+
* a fresh App Check token. Implementations typically call a backend service
281+
* (for example a Cloud Function with `enforceAppCheck: false`) that mints a
282+
* token using the Firebase Admin SDK. The native side awaits the future,
283+
* then hands the token to the Firebase SDK, which attaches it to subsequent
284+
* Firebase backend requests (Firestore, Functions, Storage, Auth, RTDB).
285+
*
286+
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
287+
*/
191288
class FirebaseAppCheckFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
192289
companion object {
193290
/** The codec used by FirebaseAppCheckFlutterApi. */
194291
val codec: MessageCodec<Any?> by lazy {
195292
GeneratedAndroidFirebaseAppCheckPigeonCodec()
196293
}
197294
}
198-
fun getCustomToken(callback: (Result<String>) -> Unit)
295+
fun getCustomToken(callback: (Result<CustomAppCheckToken>) -> Unit)
199296
{
200297
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
201298
val channelName = "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken$separatedMessageChannelSuffix"
@@ -207,7 +304,7 @@ class FirebaseAppCheckFlutterApi(private val binaryMessenger: BinaryMessenger, p
207304
} else if (it[0] == null) {
208305
callback(Result.failure(FlutterError("null-error", "Flutter api returned null value for non-null return value.", "")))
209306
} else {
210-
val output = it[0] as String
307+
val output = it[0] as CustomAppCheckToken
211308
callback(Result.success(output))
212309
}
213310
} else {

packages/firebase_app_check/firebase_app_check/ios/firebase_app_check/Sources/firebase_app_check/FirebaseAppCheckMessages.g.swift

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,129 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
7171
return value as! T?
7272
}
7373

74+
func deepEqualsFirebaseAppCheckMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
75+
let cleanLhs = nilOrValue(lhs) as Any?
76+
let cleanRhs = nilOrValue(rhs) as Any?
77+
switch (cleanLhs, cleanRhs) {
78+
case (nil, nil):
79+
return true
80+
81+
case (nil, _), (_, nil):
82+
return false
83+
84+
case is (Void, Void):
85+
return true
86+
87+
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
88+
return cleanLhsHashable == cleanRhsHashable
89+
90+
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
91+
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
92+
for (index, element) in cleanLhsArray.enumerated() {
93+
if !deepEqualsFirebaseAppCheckMessages(element, cleanRhsArray[index]) {
94+
return false
95+
}
96+
}
97+
return true
98+
99+
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
100+
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
101+
for (key, cleanLhsValue) in cleanLhsDictionary {
102+
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
103+
if !deepEqualsFirebaseAppCheckMessages(cleanLhsValue, cleanRhsDictionary[key]!) {
104+
return false
105+
}
106+
}
107+
return true
108+
109+
default:
110+
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
111+
return false
112+
}
113+
}
114+
115+
func deepHashFirebaseAppCheckMessages(value: Any?, hasher: inout Hasher) {
116+
if let valueList = value as? [AnyHashable] {
117+
for item in valueList { deepHashFirebaseAppCheckMessages(value: item, hasher: &hasher) }
118+
return
119+
}
120+
121+
if let valueDict = value as? [AnyHashable: AnyHashable] {
122+
for key in valueDict.keys {
123+
hasher.combine(key)
124+
deepHashFirebaseAppCheckMessages(value: valueDict[key]!, hasher: &hasher)
125+
}
126+
return
127+
}
128+
129+
if let hashableValue = value as? AnyHashable {
130+
hasher.combine(hashableValue.hashValue)
131+
}
132+
133+
return hasher.combine(String(describing: value))
134+
}
135+
136+
137+
138+
/// Carries a minted App Check token plus the wall-clock expiry the Firebase
139+
/// SDK should associate with it. Returning the expiry alongside the token lets
140+
/// backends mint tokens with arbitrary lifetimes (short TTLs for a stricter
141+
/// security posture, longer TTLs for fewer round-trips) without the plugin
142+
/// hardcoding a refresh window.
143+
///
144+
/// Generated class from Pigeon that represents data sent in messages.
145+
struct CustomAppCheckToken: Hashable {
146+
/// The App Check token string to send with Firebase requests.
147+
var token: String
148+
/// Absolute expiry as Unix epoch milliseconds (UTC). The Firebase SDK uses
149+
/// this to decide when to refresh; a token returned with an expiry in the
150+
/// past is treated as immediately expired.
151+
var expireTimeMillis: Int64
152+
153+
154+
// swift-format-ignore: AlwaysUseLowerCamelCase
155+
static func fromList(_ pigeonVar_list: [Any?]) -> CustomAppCheckToken? {
156+
let token = pigeonVar_list[0] as! String
157+
let expireTimeMillis = pigeonVar_list[1] as! Int64
158+
159+
return CustomAppCheckToken(
160+
token: token,
161+
expireTimeMillis: expireTimeMillis
162+
)
163+
}
164+
func toList() -> [Any?] {
165+
return [
166+
token,
167+
expireTimeMillis,
168+
]
169+
}
170+
static func == (lhs: CustomAppCheckToken, rhs: CustomAppCheckToken) -> Bool {
171+
return deepEqualsFirebaseAppCheckMessages(lhs.toList(), rhs.toList()) }
172+
func hash(into hasher: inout Hasher) {
173+
deepHashFirebaseAppCheckMessages(value: toList(), hasher: &hasher)
174+
}
175+
}
74176

75177
private class FirebaseAppCheckMessagesPigeonCodecReader: FlutterStandardReader {
178+
override func readValue(ofType type: UInt8) -> Any? {
179+
switch type {
180+
case 129:
181+
return CustomAppCheckToken.fromList(self.readValue() as! [Any?])
182+
default:
183+
return super.readValue(ofType: type)
184+
}
185+
}
76186
}
77187

78188
private class FirebaseAppCheckMessagesPigeonCodecWriter: FlutterStandardWriter {
189+
override func writeValue(_ value: Any) {
190+
if let value = value as? CustomAppCheckToken {
191+
super.writeByte(129)
192+
super.writeValue(value.toList())
193+
} else {
194+
super.writeValue(value)
195+
}
196+
}
79197
}
80198

81199
private class FirebaseAppCheckMessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter {
@@ -201,9 +319,16 @@ class FirebaseAppCheckHostApiSetup {
201319
}
202320
}
203321
}
322+
/// Dart-side handler invoked by the native plugin when the Firebase SDK needs
323+
/// a fresh App Check token. Implementations typically call a backend service
324+
/// (for example a Cloud Function with `enforceAppCheck: false`) that mints a
325+
/// token using the Firebase Admin SDK. The native side awaits the future,
326+
/// then hands the token to the Firebase SDK, which attaches it to subsequent
327+
/// Firebase backend requests (Firestore, Functions, Storage, Auth, RTDB).
328+
///
204329
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
205330
protocol FirebaseAppCheckFlutterApiProtocol {
206-
func getCustomToken(completion: @escaping (Result<String, PigeonError>) -> Void)
331+
func getCustomToken(completion: @escaping (Result<CustomAppCheckToken, PigeonError>) -> Void)
207332
}
208333
class FirebaseAppCheckFlutterApi: FirebaseAppCheckFlutterApiProtocol {
209334
private let binaryMessenger: FlutterBinaryMessenger
@@ -215,7 +340,7 @@ class FirebaseAppCheckFlutterApi: FirebaseAppCheckFlutterApiProtocol {
215340
var codec: FirebaseAppCheckMessagesPigeonCodec {
216341
return FirebaseAppCheckMessagesPigeonCodec.shared
217342
}
218-
func getCustomToken(completion: @escaping (Result<String, PigeonError>) -> Void) {
343+
func getCustomToken(completion: @escaping (Result<CustomAppCheckToken, PigeonError>) -> Void) {
219344
let channelName: String = "dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken\(messageChannelSuffix)"
220345
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
221346
channel.sendMessage(nil) { response in
@@ -231,7 +356,7 @@ class FirebaseAppCheckFlutterApi: FirebaseAppCheckFlutterApiProtocol {
231356
} else if listResponse[0] == nil {
232357
completion(.failure(PigeonError(code: "null-error", message: "Flutter api returned null value for non-null return value.", details: "")))
233358
} else {
234-
let result = listResponse[0] as! String
359+
let result = listResponse[0] as! CustomAppCheckToken
235360
completion(.success(result))
236361
}
237362
}

packages/firebase_app_check/firebase_app_check/windows/firebase_app_check_plugin.cpp

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
#include <flutter/standard_method_codec.h>
1111
#include <windows.h>
1212

13-
#include <chrono>
1413
#include <future>
1514
#include <memory>
1615
#include <string>
@@ -99,9 +98,11 @@ class TokenStreamHandler
9998
std::unique_ptr<FlutterAppCheckListener> listener_;
10099
};
101100

102-
// FlutterCustomAppCheckProvider calls into Dart via the FlutterApi and
101+
// FlutterCustomAppCheckProvider calls into Dart via the FlutterApi and
103102
// completes the Firebase C++ SDK callback asynchronously when Dart returns a
104-
// token (or an error).
103+
// token (or an error). The Dart handler returns the token together with its
104+
// expiry, so the C++ SDK can cache for the exact lifetime the backend minted
105+
// rather than a hardcoded refresh window.
105106
FlutterCustomAppCheckProvider::FlutterCustomAppCheckProvider(
106107
flutter::BinaryMessenger* binary_messenger)
107108
: flutter_api_(
@@ -116,17 +117,10 @@ void FlutterCustomAppCheckProvider::GetToken(
116117
const std::string&)>>(std::move(completion_callback));
117118

118119
flutter_api_->GetCustomToken(
119-
[completion](const std::string& token) {
120+
[completion](const CustomAppCheckToken& dart_token) {
120121
firebase::app_check::AppCheckToken result_token;
121-
result_token.token = token;
122-
// Set expiry to 55 minutes from now (server mints 1-hour tokens;
123-
// 5-minute buffer avoids using a nearly-expired token).
124-
result_token.expire_time_millis =
125-
static_cast<int64_t>(
126-
std::chrono::duration_cast<std::chrono::milliseconds>(
127-
std::chrono::system_clock::now().time_since_epoch())
128-
.count()) +
129-
55LL * 60LL * 1000LL;
122+
result_token.token = dart_token.token();
123+
result_token.expire_time_millis = dart_token.expire_time_millis();
130124
(*completion)(result_token, firebase::app_check::kAppCheckErrorNone,
131125
"");
132126
},

0 commit comments

Comments
 (0)