Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/app-check/default-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ Future<void> main() async {
// 3. AppleAppAttestProvider
// 4. AppleAppAttestProviderWithDeviceCheckFallback (App Attest provider is only available on iOS 14.0+, macOS 14.0+)
providerApple: AppleAppAttestProvider(),
// Default provider for Windows is the debug provider. Use `providerWindows`
// to choose:
// 1. WindowsDebugProvider for development (pass a console-registered token)
// 2. WindowsCustomProvider for production token minting via your backend
providerWindows: WindowsDebugProvider(debugToken: 'your-debug-token'),
);
runApp(App());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class FirebaseAppCheckPlugin : FlutterFirebasePlugin, FlutterPlugin, FirebaseApp
appleProvider: String?,
debugToken: String?,
recaptchaSiteKey: String?,
windowsProvider: String?,
callback: (Result<Unit>) -> Unit
) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any?> {
return listOf(result)
}
Expand Down Expand Up @@ -229,12 +234,69 @@ data class InternalAppCheckTokenResult(val token: String, val expirationTimestam
}
}

/**
* 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<Any?>): CustomAppCheckToken {
val token = pigeonVar_list[0] as String
val expireTimeMillis = pigeonVar_list[1] as Long
return CustomAppCheckToken(token, expireTimeMillis)
}
}

fun toList(): List<Any?> {
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 when (type) {
129.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { InternalAppCheckTokenResult.fromList(it) }
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { CustomAppCheckToken.fromList(it) }
}
else -> super.readValueOfType(type, buffer)
}
}
Expand All @@ -245,6 +307,10 @@ private open class GeneratedAndroidFirebaseAppCheckPigeonCodec : StandardMessage
stream.write(129)
writeValue(stream, value.toList())
}
is CustomAppCheckToken -> {
stream.write(130)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
Expand All @@ -258,6 +324,7 @@ interface FirebaseAppCheckHostApi {
appleProvider: String?,
debugToken: String?,
recaptchaSiteKey: String?,
windowsProvider: String?,
callback: (Result<Unit>) -> Unit
)

Expand Down Expand Up @@ -308,12 +375,14 @@ interface FirebaseAppCheckHostApi {
val appleProviderArg = args[2] as String?
val debugTokenArg = args[3] as String?
val recaptchaSiteKeyArg = args[4] as String?
val windowsProviderArg = args[5] as String?
api.activate(
appNameArg,
androidProviderArg,
appleProviderArg,
debugTokenArg,
recaptchaSiteKeyArg) { result: Result<Unit> ->
recaptchaSiteKeyArg,
windowsProviderArg) { result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(GeneratedAndroidFirebaseAppCheckPigeonUtils.wrapError(error))
Expand Down Expand Up @@ -453,3 +522,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<Any?> by lazy { GeneratedAndroidFirebaseAppCheckPigeonCodec() }
}

fun getCustomToken(callback: (Result<CustomAppCheckToken>) -> Unit) {
val separatedMessageChannelSuffix =
if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName =
"dev.flutter.pigeon.firebase_app_check_platform_interface.FirebaseAppCheckFlutterApi.getCustomToken$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(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)))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,128 @@ void main() {
'APP_CHECK_APPLE_DEBUG_TOKEN dart-defines.'
: null,
);

group(
'WindowsCustomProvider',
() {
int farFutureExpireTimeMillis() {
return DateTime.now()
.add(const Duration(hours: 1))
.millisecondsSinceEpoch;
}

test(
'getToken invokes fetchToken and returns the synthetic token',
() async {
var fetchCount = 0;
const token = 'windows-custom-e2e-token';
final expireTimeMillis = farFutureExpireTimeMillis();

await FirebaseAppCheck.instance.activate(
providerWindows: WindowsCustomProvider(
fetchToken: () async {
fetchCount++;
return CustomAppCheckToken(
token: token,
expireTimeMillis: expireTimeMillis,
);
},
),
);

expect(
await FirebaseAppCheck.instance.getToken(true),
token,
);
expect(
fetchCount,
greaterThan(0),
reason: 'native must call GetCustomToken so fetchToken runs; '
'a zero count means the debug provider path was used',
);
},
);

test(
'isolates fetchToken by Firebase app name',
() async {
var defaultFetchCount = 0;
var secondaryFetchCount = 0;
const defaultToken = 'windows-custom-default-app-token';
const secondaryToken = 'windows-custom-secondary-app-token';
final expireTimeMillis = farFutureExpireTimeMillis();

final secondaryApp = await Firebase.initializeApp(
name: 'app-check-secondary',
options: DefaultFirebaseOptions.currentPlatform,
);
addTearDown(secondaryApp.delete);

final defaultAppCheck = FirebaseAppCheck.instance;
final secondaryAppCheck = FirebaseAppCheck.instanceFor(
app: secondaryApp,
);

await defaultAppCheck.activate(
providerWindows: WindowsCustomProvider(
fetchToken: () async {
defaultFetchCount++;
return CustomAppCheckToken(
token: defaultToken,
expireTimeMillis: expireTimeMillis,
);
},
),
);
await secondaryAppCheck.activate(
providerWindows: WindowsCustomProvider(
fetchToken: () async {
secondaryFetchCount++;
return CustomAppCheckToken(
token: secondaryToken,
expireTimeMillis: expireTimeMillis,
);
},
),
);

expect(await defaultAppCheck.getToken(true), defaultToken);
expect(defaultFetchCount, greaterThan(0));
expect(
secondaryFetchCount,
0,
reason: 'fetching the default app must not invoke the '
'secondary app fetchToken',
);

final defaultCountAfterDefaultFetch = defaultFetchCount;
expect(
await secondaryAppCheck.getToken(true),
secondaryToken,
);
expect(secondaryFetchCount, greaterThan(0));
expect(
defaultFetchCount,
defaultCountAfterDefaultFetch,
reason: 'fetching the secondary app must not invoke the '
'default app fetchToken',
);

final secondaryCountAfterSecondaryFetch = secondaryFetchCount;
expect(await defaultAppCheck.getToken(true), defaultToken);
expect(
defaultFetchCount,
greaterThan(defaultCountAfterDefaultFetch),
);
expect(
secondaryFetchCount,
secondaryCountAfterSecondaryFetch,
);
},
);
},
skip: kIsWeb || defaultTargetPlatform != TargetPlatform.windows,
);
},
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ Future<void> main() async {
: ReCaptchaV3Provider(kWebRecaptchaSiteKey),
providerAndroid: const AndroidDebugProvider(),
providerApple: const AppleDebugProvider(),
// On Windows, only the debug provider is available.
// You must supply a debug token — the desktop C++ SDK does not
// auto-generate one. Create one in the Firebase Console under
// App Check > Apps > Manage debug tokens, then either:
// On Windows, use WindowsDebugProvider for development or
// WindowsCustomProvider to mint production tokens from your backend.
// The desktop C++ SDK does not auto-generate debug tokens. Create one in
// the Firebase Console under App Check > Apps > Manage debug tokens,
// then either:
// - pass it via --dart-define=APP_CHECK_DEBUG_TOKEN=<token>
// - or set the APP_CHECK_DEBUG_TOKEN environment variable
providerWindows: WindowsDebugProvider(
Expand Down
Loading
Loading