From d375415aa3ac427f9c172c8f04ee05523b31e0dc Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Fri, 14 Aug 2026 16:22:31 +0200 Subject: [PATCH 1/2] fix(appcheck,macos): fix how appAttestWithDeviceCheckFallback is working on fallback --- .../example/integration_test/e2e_test.dart | 29 +++++++++++++++++++ .../FirebaseAppCheckPlugin.swift | 14 ++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart index 9af6341c8823..66d0abf48421 100644 --- a/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart +++ b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart @@ -140,6 +140,35 @@ void main() { skip: defaultTargetPlatform != TargetPlatform.iOS, ); + test( + 'appAttestWithDeviceCheckFallback falls back rather than erroring', + () async { + await FirebaseAppCheck.instance.activate( + providerApple: + const AppleAppAttestWithDeviceCheckFallbackProvider(), + ); + + // On devices without App Attest support — most Macs — the provider + // has to fall back to DeviceCheck. It used to pick App Attest purely + // on OS version and fail with "The attestation provider + // AppAttestProvider is not supported on current platform and OS + // version", so that specific error must not come back. + try { + await FirebaseAppCheck.instance.getToken(true); + } on FirebaseException catch (e) { + expect( + '${e.message}', + isNot(contains('is not supported on current platform')), + reason: 'the DeviceCheck fallback did not engage', + ); + } + }, + skip: defaultTargetPlatform != TargetPlatform.macOS && + defaultTargetPlatform != TargetPlatform.iOS + ? 'Apple platforms only.' + : null, + ); + test( 'uses Apple debug token when both Android and Apple debug tokens are configured', () async { 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 7c7cb4514293..9b702d03aef0 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 @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import DeviceCheck import FirebaseAppCheck import FirebaseCore @@ -371,11 +372,16 @@ class AppCheckProviderWrapper: NSObject, AppCheckProvider { delegateProvider = AppCheckDebugProvider(app: app) } case "appAttestWithDeviceCheckFallback": - if #available(iOS 14.0, macOS 14.0, *) { - delegateProvider = AppAttestProvider(app: app) - } else { - delegateProvider = DeviceCheckProvider(app: app) + // A new enough OS is not sufficient: the device must also support App + // Attest, which is often false on macOS. The SDK only reports that when a + // token is first requested, so check the same capability it checks — + // otherwise this provider never actually falls back. Typed as the + // protocol to keep the availability-annotated type inside `#available`. + var appAttestProvider: (any AppCheckProvider)? + if #available(iOS 14.0, macOS 14.0, *), DCAppAttestService.shared.isSupported { + appAttestProvider = AppAttestProvider(app: app) } + delegateProvider = appAttestProvider ?? DeviceCheckProvider(app: app) case "recaptcha": #if os(iOS) if let recaptchaSiteKey { From c816eadf102e88f048dc11add1a0f76b3efa861f Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Mon, 17 Aug 2026 09:52:09 +0200 Subject: [PATCH 2/2] test(appcheck): assert App Attest is not selected instead of matching any unsupported-provider error iOS simulators do not support DeviceCheck, so the fallback path legitimately errors with "DeviceCheckProvider is not supported". Narrow the assertion to AppAttestProvider, which is the regression being guarded. --- .../example/integration_test/e2e_test.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart index 66d0abf48421..db9f90b0508f 100644 --- a/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart +++ b/packages/firebase_app_check/firebase_app_check/example/integration_test/e2e_test.dart @@ -148,17 +148,20 @@ void main() { const AppleAppAttestWithDeviceCheckFallbackProvider(), ); - // On devices without App Attest support — most Macs — the provider - // has to fall back to DeviceCheck. It used to pick App Attest purely - // on OS version and fail with "The attestation provider - // AppAttestProvider is not supported on current platform and OS - // version", so that specific error must not come back. + // On devices without App Attest support — most Macs, and every + // simulator — the provider has to fall back to DeviceCheck. It used + // to pick App Attest purely on OS version and fail with "The + // attestation provider AppAttestProvider is not supported on current + // platform and OS version", so App Attest must not be the provider + // named in any error. Fetching a token can still fail beyond that + // (simulators do not support DeviceCheck either, and there is no + // debug token configured), which is fine here. try { await FirebaseAppCheck.instance.getToken(true); } on FirebaseException catch (e) { expect( '${e.message}', - isNot(contains('is not supported on current platform')), + isNot(contains('AppAttestProvider')), reason: 'the DeviceCheck fallback did not engage', ); }