From 599fb997f1cd73a191496c7e378ba6317c002656 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Sun, 9 Aug 2026 04:31:22 +0200 Subject: [PATCH] fix auth flows --- .../services/app_link_policy_replication.dart | 1 + .../app_link_policy_replication.g.dart | 2 +- .../geckoview/domain/repositories/tab.g.dart | 2 +- .../features/browser/domain/providers.g.dart | 12 +- .../screens/browsing_settings.dart | 22 ++++ .../user/data/models/general_settings.dart | 10 ++ .../user/data/models/general_settings.g.dart | 19 ++++ .../domain/repositories/general_settings.dart | 1 + .../activities/AuthIntentReceiverActivity.kt | 4 + .../activities/ExternalCallerInfo.kt | 83 ++++++++++++++ .../activities/IntentReceiverActivity.kt | 27 +---- .../applinks/AppLinkClassifier.kt | 2 + .../applinks/AppLinkLauncher.kt | 14 ++- .../applinks/AppLinkPolicyMapper.kt | 1 + .../applinks/AppLinkPolicyStore.kt | 3 + .../applinks/WebLibreAppLinksInterceptor.kt | 106 ++++++++++++++++-- .../pigeons/Gecko.g.kt | 24 ++-- .../applinks/AppLinkClassifierTest.kt | 5 + .../applinks/AppLinkLauncherTest.kt | 44 ++++++++ .../lib/src/pigeons/gecko.g.dart | 20 +++- .../pigeons/gecko.dart | 5 + 21 files changed, 349 insertions(+), 58 deletions(-) create mode 100644 packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalCallerInfo.kt diff --git a/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.dart b/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.dart index 68365530..f813fda8 100644 --- a/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.dart +++ b/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.dart @@ -110,6 +110,7 @@ AppLinkPolicySnapshot? appLinkPolicySnapshot(Ref ref) { key: _toNativeRule(value), }, marketplaceFallbackEnabled: settings.appLinkMarketplaceFallback, + authExceptionsEnabled: settings.appLinkAuthExceptionsEnabled, protectGeneralContext: protection.protectGeneralContext, protectedContextIds: protection.protectedContextIds.toList(), strictContextIds: protection.strictContextIds.toList(), diff --git a/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.g.dart b/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.g.dart index 8b258c88..806ced1c 100644 --- a/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.g.dart +++ b/apps/weblibre/lib/features/app_links/domain/services/app_link_policy_replication.g.dart @@ -120,7 +120,7 @@ final class AppLinkPolicySnapshotProvider } String _$appLinkPolicySnapshotHash() => - r'6fe2dca118d7162561fc7f6280d1a0411d50972a'; + r'4d456a3cbf091e95ac35d913e8fa20d5f25978ab'; /// Single serialised writer that mirrors the Dart-owned app-link policy to the /// native profile-scoped store (§2.8), the sole policy source consulted by the diff --git a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart index 99832299..d27213d6 100644 --- a/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/apps/weblibre/lib/features/geckoview/domain/repositories/tab.g.dart @@ -41,7 +41,7 @@ final class TabRepositoryProvider } } -String _$tabRepositoryHash() => r'797520166026d1f0272cf713aa1c25ecf129c236'; +String _$tabRepositoryHash() => r'5824be32664bac24623cf8863f68b7afa254d025'; abstract class _$TabRepository extends $Notifier { void build(); diff --git a/apps/weblibre/lib/features/geckoview/features/browser/domain/providers.g.dart b/apps/weblibre/lib/features/geckoview/features/browser/domain/providers.g.dart index 01f3ed95..35fd046d 100644 --- a/apps/weblibre/lib/features/geckoview/features/browser/domain/providers.g.dart +++ b/apps/weblibre/lib/features/geckoview/features/browser/domain/providers.g.dart @@ -1397,8 +1397,8 @@ final class VisibleTabListItemsFamily extends $Family /// switcher lays them out — the unassigned bucket first, then containers by /// pinned/`order_key`. Stepping off the end of one container therefore /// continues into the next, and selecting that tab moves the selected container -/// along with it. Containers without tabs are skipped so their tree query never -/// runs. +/// along with it. Named containers holding no tabs are skipped so their tree +/// query never runs. /// /// "Previous" is a step towards the top of that order and "next" a step /// towards its end, so direction follows `tabListDirection` (baked into the @@ -1443,8 +1443,8 @@ final sequentialTabNavigationOrderProvider = /// switcher lays them out — the unassigned bucket first, then containers by /// pinned/`order_key`. Stepping off the end of one container therefore /// continues into the next, and selecting that tab moves the selected container -/// along with it. Containers without tabs are skipped so their tree query never -/// runs. +/// along with it. Named containers holding no tabs are skipped so their tree +/// query never runs. /// /// "Previous" is a step towards the top of that order and "next" a step /// towards its end, so direction follows `tabListDirection` (baked into the @@ -1493,8 +1493,8 @@ final class SequentialTabNavigationOrderProvider /// switcher lays them out — the unassigned bucket first, then containers by /// pinned/`order_key`. Stepping off the end of one container therefore /// continues into the next, and selecting that tab moves the selected container - /// along with it. Containers without tabs are skipped so their tree query never - /// runs. + /// along with it. Named containers holding no tabs are skipped so their tree + /// query never runs. /// /// "Previous" is a step towards the top of that order and "next" a step /// towards its end, so direction follows `tabListDirection` (baked into the diff --git a/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart index 78ff84bf..6cb7c298 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart @@ -817,6 +817,11 @@ class _AppLinksModeSection extends HookConsumerWidget { (s) => s.appLinkMarketplaceFallback, ), ); + final authExceptionsEnabled = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.appLinkAuthExceptionsEnabled, + ), + ); final rules = ref.watch( generalSettingsWithDefaultsProvider.select((s) => s.appLinkRules), ); @@ -887,6 +892,23 @@ class _AppLinksModeSection extends HookConsumerWidget { ); }, ), + SwitchListTile.adaptive( + contentPadding: EdgeInsets.zero, + title: const Text('Allow login app callbacks'), + subtitle: const Text( + 'Let apps that opened a Custom Tab receive their login callback, ' + 'even when links are set to never open in apps', + ), + value: authExceptionsEnabled, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (current) => + current.copyWith.appLinkAuthExceptionsEnabled(value), + ); + }, + ), _AppLinkRulesSubsection(rules: rules), ], ), diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.dart b/apps/weblibre/lib/features/user/data/models/general_settings.dart index 2e55dd43..0fc7f688 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.dart @@ -252,6 +252,12 @@ class GeneralSettings with FastEquatable { /// Defaults to false — the wrong default for a de-Googled browser. final bool appLinkMarketplaceFallback; + /// Whether app-link "never" rules allow a same-caller Custom Tab / ActionView + /// login callback to return to the app that opened the browser. Defaults to + /// true to keep OAuth-style sign-in flows working while normal app links still + /// obey [appLinksMode]. + final bool appLinkAuthExceptionsEnabled; + /// Whether the local search index (`history` table populated via tab→ /// history triggers) is active. When false, the SQL trigger guard returns /// without writing; existing rows stay until the user clears them. @@ -359,6 +365,7 @@ class GeneralSettings with FastEquatable { required this.appLinkRules, required this.appLinkContextOverrides, required this.appLinkMarketplaceFallback, + required this.appLinkAuthExceptionsEnabled, required this.enableLocalSearchIndex, required this.indexPrivateTabs, required this.acceptSuggestionOnSubmit, @@ -436,6 +443,7 @@ class GeneralSettings with FastEquatable { Map? appLinkRules, Map? appLinkContextOverrides, bool? appLinkMarketplaceFallback, + bool? appLinkAuthExceptionsEnabled, bool? enableLocalSearchIndex, bool? indexPrivateTabs, bool? acceptSuggestionOnSubmit, @@ -524,6 +532,7 @@ class GeneralSettings with FastEquatable { appLinkRules = appLinkRules ?? const {}, appLinkContextOverrides = appLinkContextOverrides ?? const {}, appLinkMarketplaceFallback = appLinkMarketplaceFallback ?? false, + appLinkAuthExceptionsEnabled = appLinkAuthExceptionsEnabled ?? true, enableLocalSearchIndex = enableLocalSearchIndex ?? true, indexPrivateTabs = indexPrivateTabs ?? false, acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? true, @@ -693,6 +702,7 @@ class GeneralSettings with FastEquatable { appLinkRules, appLinkContextOverrides, appLinkMarketplaceFallback, + appLinkAuthExceptionsEnabled, enableLocalSearchIndex, indexPrivateTabs, acceptSuggestionOnSubmit, diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart index b3dc1ea9..149a4350 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart @@ -161,6 +161,10 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback); + GeneralSettings appLinkAuthExceptionsEnabled( + bool appLinkAuthExceptionsEnabled, + ); + GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex); GeneralSettings indexPrivateTabs(bool indexPrivateTabs); @@ -253,6 +257,7 @@ abstract class _$GeneralSettingsCWProxy { Map appLinkRules, Map appLinkContextOverrides, bool appLinkMarketplaceFallback, + bool appLinkAuthExceptionsEnabled, bool enableLocalSearchIndex, bool indexPrivateTabs, bool acceptSuggestionOnSubmit, @@ -556,6 +561,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings appLinkMarketplaceFallback(bool appLinkMarketplaceFallback) => call(appLinkMarketplaceFallback: appLinkMarketplaceFallback); + @override + GeneralSettings appLinkAuthExceptionsEnabled( + bool appLinkAuthExceptionsEnabled, + ) => call(appLinkAuthExceptionsEnabled: appLinkAuthExceptionsEnabled); + @override GeneralSettings enableLocalSearchIndex(bool enableLocalSearchIndex) => call(enableLocalSearchIndex: enableLocalSearchIndex); @@ -665,6 +675,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? appLinkRules = const $CopyWithPlaceholder(), Object? appLinkContextOverrides = const $CopyWithPlaceholder(), Object? appLinkMarketplaceFallback = const $CopyWithPlaceholder(), + Object? appLinkAuthExceptionsEnabled = const $CopyWithPlaceholder(), Object? enableLocalSearchIndex = const $CopyWithPlaceholder(), Object? indexPrivateTabs = const $CopyWithPlaceholder(), Object? acceptSuggestionOnSubmit = const $CopyWithPlaceholder(), @@ -1061,6 +1072,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.appLinkMarketplaceFallback // ignore: cast_nullable_to_non_nullable : appLinkMarketplaceFallback as bool, + appLinkAuthExceptionsEnabled: + appLinkAuthExceptionsEnabled == const $CopyWithPlaceholder() || + appLinkAuthExceptionsEnabled == null + ? _value.appLinkAuthExceptionsEnabled + // ignore: cast_nullable_to_non_nullable + : appLinkAuthExceptionsEnabled as bool, enableLocalSearchIndex: enableLocalSearchIndex == const $CopyWithPlaceholder() || enableLocalSearchIndex == null @@ -1257,6 +1274,7 @@ GeneralSettings _$GeneralSettingsFromJson( json['appLinkContextOverrides'] as Map?, ), appLinkMarketplaceFallback: json['appLinkMarketplaceFallback'] as bool?, + appLinkAuthExceptionsEnabled: json['appLinkAuthExceptionsEnabled'] as bool?, enableLocalSearchIndex: json['enableLocalSearchIndex'] as bool?, indexPrivateTabs: json['indexPrivateTabs'] as bool?, acceptSuggestionOnSubmit: json['acceptSuggestionOnSubmit'] as bool?, @@ -1356,6 +1374,7 @@ Map _$GeneralSettingsToJson( (k, e) => MapEntry(k, e.toJson()), ), 'appLinkMarketplaceFallback': instance.appLinkMarketplaceFallback, + 'appLinkAuthExceptionsEnabled': instance.appLinkAuthExceptionsEnabled, 'enableLocalSearchIndex': instance.enableLocalSearchIndex, 'indexPrivateTabs': instance.indexPrivateTabs, 'acceptSuggestionOnSubmit': instance.acceptSuggestionOnSubmit, diff --git a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart index 85d10d13..06fc2cfb 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart @@ -109,6 +109,7 @@ const generalSettingColumnTypes = { 'customTabsEnabled': DriftSqlType.bool, 'appLinksMode': DriftSqlType.string, 'appLinkMarketplaceFallback': DriftSqlType.bool, + 'appLinkAuthExceptionsEnabled': DriftSqlType.bool, 'enableLocalSearchIndex': DriftSqlType.bool, 'indexPrivateTabs': DriftSqlType.bool, 'acceptSuggestionOnSubmit': DriftSqlType.bool, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/AuthIntentReceiverActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/AuthIntentReceiverActivity.kt index f03491ec..0f252364 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/AuthIntentReceiverActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/AuthIntentReceiverActivity.kt @@ -19,6 +19,10 @@ class AuthIntentReceiverActivity : Activity() { val sourceIntent = intent?.let { Intent(it) } ?: Intent() + // Stamp the caller before CustomTabIntentProcessor builds the session source, so the + // app-links authentication carve-out can recognise a sign-in callback for this tab. + addExternalCallerInformation(sourceIntent) + if (GlobalComponents.components == null && !GlobalComponents.ensureExternalComponents(applicationContext)) { finish() return diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalCallerInfo.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalCallerInfo.kt new file mode 100644 index 00000000..a8ca3b8d --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalCallerInfo.kt @@ -0,0 +1,83 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package eu.weblibre.flutter_mozilla_components.activities + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY +import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE +import mozilla.components.support.utils.ext.packageManagerCompatHelper + +/** + * Records which app sent [intent] so the session created from it carries a `caller`. + * + * AC's `CustomTabIntentProcessor` reads the caller through `SafeIntent.externalPackage()`, which + * only looks at the [EXTRA_ACTIVITY_REFERRER_PACKAGE] extra — nothing populates it for us, so a + * receiver has to stamp it before handing the intent to the processors or every custom tab ends up + * with `Source.External.CustomTab(null)`. Mirrors Fenix's `IntentReceiverActivity` + * `addReferrerInformation`. + * + * The app-links authentication carve-out + * ([eu.weblibre.flutter_mozilla_components.applinks.WebLibreAppLinksInterceptor]) is the consumer: + * it lets a sign-in callback return to the app that opened the tab. + */ +fun Activity.addExternalCallerInformation(intent: Intent) { + val caller = resolveExternalCallerPackage(intent) ?: return + intent.putExtra(EXTRA_ACTIVITY_REFERRER_PACKAGE, caller) + + // ApplicationInfo.category is API 26+; this module builds against minSdk 24. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + val category = packageManagerCompatHelper.getApplicationInfoCompat(caller, 0).category + intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category) + } catch (e: PackageManager.NameNotFoundException) { + // The caller is not resolvable — the package id alone is enough for our purposes. + } + } +} + +/** + * Best-effort identity of the app that sent [intent]. + * + * [Activity.getCallingPackage] is supplied by the system and cannot be forged, so it wins when + * present (only set for `startActivityForResult` callers). The referrer chain below is + * caller-controlled and therefore spoofable — an app can claim to be another package. Consumers + * must not grant anything on it that the caller could not already do itself. + */ +@Suppress("TooGenericExceptionCaught") +fun Activity.resolveExternalCallerPackage(intent: Intent): String? { + callingPackage?.let { return it } + + // Android can throw when the referrer carries data it cannot deserialise. + val activityReferrer = try { + referrer + } catch (e: RuntimeException) { + null + } + activityReferrer?.let { uri -> + if (uri.scheme == ANDROID_APP_SCHEME) { + uri.host?.let { return it } + } + } + + @Suppress("DEPRECATION") + val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER) + if (referrerUri?.scheme == ANDROID_APP_SCHEME) { + referrerUri.host?.let { return it } + } + + intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name -> + Uri.parse(name).takeIf { it.scheme == ANDROID_APP_SCHEME }?.host?.let { return it } + } + + return null +} + +private const val ANDROID_APP_SCHEME = "android-app" diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt index a7940cd3..31ef514d 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt @@ -93,7 +93,7 @@ class IntentReceiverActivity : Activity() { } } - val caller = resolveCallerPackage(intent) ?: return false + val caller = resolveExternalCallerPackage(intent) ?: return false if (caller == packageName) return false if (!IntentGatekeeperPreferences.isBlocked(applicationContext, caller)) return false @@ -102,32 +102,17 @@ class IntentReceiverActivity : Activity() { return true } - private fun resolveCallerPackage(intent: Intent): String? { - referrer?.let { uri -> - if (uri.scheme == "android-app") { - uri.host?.let { return it } - } - } - - @Suppress("DEPRECATION") - val referrerUri: Uri? = intent.getParcelableExtra(Intent.EXTRA_REFERRER) - if (referrerUri?.scheme == "android-app") { - referrerUri.host?.let { return it } - } - - intent.getStringExtra(Intent.EXTRA_REFERRER_NAME)?.let { name -> - Uri.parse(name).takeIf { it.scheme == "android-app" }?.host?.let { return it } - } - - return callingPackage - } - override fun onDestroy() { super.onDestroy() coroutineScope.cancel() } private fun processIntent(intent: Intent) { + // Must run before any intent processor: CustomTabIntentProcessor reads the caller off the + // intent when it builds the session source, and the app-links authentication carve-out + // needs that caller to recognise a sign-in callback. + addExternalCallerInformation(intent) + if (GlobalComponents.components == null) { if (GlobalComponents.ensureExternalComponents(applicationContext)) { routeIntent(intent) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifier.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifier.kt index a3c82fb8..ff8e2125 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifier.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifier.kt @@ -57,6 +57,7 @@ data class AppLinkPolicy( val protectedContextIds: Set, val strictContextIds: Set, val protectedTargetPatterns: List, + val authExceptionsEnabled: Boolean, /** * Per-container overrides keyed by contextId; only isolated containers appear. A navigation whose * source contextId is a key uses the entry's mode + rules instead of the global ones (replace). @@ -72,6 +73,7 @@ data class AppLinkPolicy( protectedContextIds = emptySet(), strictContextIds = emptySet(), protectedTargetPatterns = emptyList(), + authExceptionsEnabled = true, contextOverrides = emptyMap(), ) } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncher.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncher.kt index b9a9d0d1..b9d8c45e 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncher.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncher.kt @@ -16,11 +16,15 @@ import mozilla.components.support.base.log.logger.Logger * behaviour so the app opens in its own recents entry. * - [AUTOMATIC]: global-`always` or a remembered `alwaysOpen` rule — `NEW_TASK`, subject to the * 2 s same-package cooldown loop-breaker (§2.4). + * - [AUTHENTICATION]: same-caller Custom Tab / ActionView callback — `NEW_TASK | CLEAR_TOP` so the + * originating app can resume its existing task. Not a user gesture, so it takes the same 2 s + * cooldown as [AUTOMATIC] (AC applies its loop-breaker to authentication flows too). * - [MARKETPLACE]: install-app fallback — `NEW_TASK | CLEAR_TASK`. */ enum class AppLinkLaunchMode { MANUAL, AUTOMATIC, + AUTHENTICATION, MARKETPLACE, } @@ -34,9 +38,9 @@ enum class AppLinkLaunchResult { /** * Launches external apps. Every launch re-resolves immediately first (no cache) and verifies the - * expected package before `startActivity` (§2.7). Automatic launches honour a 2 s same-package - * cooldown to break app→browser→app ping-pong loops (§2.4); manual and prompt-resolved opens are - * user gestures that bypass the check but still record it. + * expected package before `startActivity` (§2.7). Automatic and authentication launches honour a 2 s + * same-package cooldown to break app→browser→app ping-pong loops (§2.4); manual and prompt-resolved + * opens are user gestures that bypass the check but still record it. */ class AppLinkLauncher( private val resolver: ExternalAppResolver, @@ -81,7 +85,7 @@ class AppLinkLauncher( else -> resolved.packageName } - if (mode == AppLinkLaunchMode.AUTOMATIC) { + if (mode == AppLinkLaunchMode.AUTOMATIC || mode == AppLinkLaunchMode.AUTHENTICATION) { val (lastPackage, lastTs) = lastLaunch if (lastPackage != null && lastPackage == targetPackage && clock.elapsedRealtime() < lastTs + cooldownMs @@ -117,6 +121,8 @@ class AppLinkLauncher( Intent.FLAG_ACTIVITY_NEW_TASK AppLinkLaunchMode.AUTOMATIC -> Intent.FLAG_ACTIVITY_NEW_TASK + AppLinkLaunchMode.AUTHENTICATION -> + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP AppLinkLaunchMode.MARKETPLACE -> Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyMapper.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyMapper.kt index 03646fc7..8b431659 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyMapper.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyMapper.kt @@ -28,6 +28,7 @@ fun AppLinkPolicySnapshot.toAppLinkPolicy(): AppLinkPolicy { port = pattern.port?.toInt(), ) }, + authExceptionsEnabled = authExceptionsEnabled, contextOverrides = contextOverrides.mapValues { (_, override) -> ContextAppLinkPolicy( globalMode = override.mode.toAppLinkMode(), diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyStore.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyStore.kt index 47482868..24f1448d 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyStore.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkPolicyStore.kt @@ -93,6 +93,7 @@ class AppLinkPolicyStore internal constructor( root.put(FIELD_MIGRATED, migrated) root.put(FIELD_GLOBAL_MODE, policy.globalMode.name) root.put(FIELD_MARKETPLACE, policy.marketplaceFallbackEnabled) + root.put(FIELD_AUTH_EXCEPTIONS, policy.authExceptionsEnabled) root.put(FIELD_PROTECT_GENERAL, policy.protectGeneralContext) root.put(FIELD_PROTECTED_CONTEXTS, JSONArray(policy.protectedContextIds.toList())) root.put(FIELD_STRICT_CONTEXTS, JSONArray(policy.strictContextIds.toList())) @@ -177,6 +178,7 @@ class AppLinkPolicyStore internal constructor( globalMode = AppLinkMode.valueOf(root.getString(FIELD_GLOBAL_MODE)), rules = rules, marketplaceFallbackEnabled = root.optBoolean(FIELD_MARKETPLACE, false), + authExceptionsEnabled = root.optBoolean(FIELD_AUTH_EXCEPTIONS, true), protectGeneralContext = root.optBoolean(FIELD_PROTECT_GENERAL, false), protectedContextIds = root.optJSONArray(FIELD_PROTECTED_CONTEXTS).toStringSet(), strictContextIds = root.optJSONArray(FIELD_STRICT_CONTEXTS).toStringSet(), @@ -218,6 +220,7 @@ class AppLinkPolicyStore internal constructor( private const val FIELD_MIGRATED = "migrated" private const val FIELD_GLOBAL_MODE = "globalMode" private const val FIELD_MARKETPLACE = "marketplaceFallbackEnabled" + private const val FIELD_AUTH_EXCEPTIONS = "authExceptionsEnabled" private const val FIELD_PROTECT_GENERAL = "protectGeneralContext" private const val FIELD_PROTECTED_CONTEXTS = "protectedContextIds" private const val FIELD_STRICT_CONTEXTS = "strictContextIds" diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/WebLibreAppLinksInterceptor.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/WebLibreAppLinksInterceptor.kt index 82b67ade..006a0067 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/WebLibreAppLinksInterceptor.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/applinks/WebLibreAppLinksInterceptor.kt @@ -58,9 +58,27 @@ class WebLibreAppLinksInterceptor( val uriScheme = runCatching { uri.toUri().scheme }.getOrNull() val engineSupportsScheme = AppLinkSchemes.isEngineSupported(uriScheme) + val session = components.core.store.state.findTabOrCustomTab(engineSession) + val policy = AppLinkPolicyStores.forProfile(components.profileApplicationContext).policy + + // A tab an external app opened for us (Custom Tab / ActionView) may be hosting a sign-in + // round trip. Gated on the policy so turning the carve-out off restores the plain §2.4 + // eligibility rules rather than only skipping the launch below. + val authExceptionsAllowed = policy.authExceptionsEnabled && isPossibleAuthentication(session) // Step 2 — navigation eligibility. Any hit lets the engine proceed normally. - if (!isEligible(uri, lastUri, uriScheme, engineSupportsScheme, hasUserGesture, isRedirect, isDirectNavigation, isSubframeRequest)) { + if (!isEligible( + uri, + lastUri, + uriScheme, + engineSupportsScheme, + hasUserGesture, + isRedirect, + isDirectNavigation, + isSubframeRequest, + authExceptionsAllowed, + ) + ) { return null } @@ -74,10 +92,6 @@ class WebLibreAppLinksInterceptor( val resolved = runtime.resolver.resolve(uri, includeHttpAppLinks = true, useCache = true) - val policy = AppLinkPolicyStores.forProfile(components.profileApplicationContext).policy - - val session = components.core.store.state.findTabOrCustomTab(engineSession) - // Container isolation (replace semantics): a container with "isolated app link settings" // enabled contributes an entry keyed by its contextId. When the source tab's contextId has // one, its mode + rules fully replace the global ones for this navigation. @@ -85,12 +99,47 @@ class WebLibreAppLinksInterceptor( val effectiveMode = override?.globalMode ?: policy.globalMode val effectiveRules = override?.rules ?: policy.rules + val isProtectedNavigation = isProtected(policy, session, uri) + val isPrivateNavigation = session?.content?.private ?: false + val isWalletNavigation = AppLinkSchemes.isWallet(resolved.originalScheme) || + AppLinkSchemes.isWallet(resolved.intentDataScheme) + + // An ambiguous resolution is never treated as a callback: with several handlers we cannot + // show the caller *is* the target, and the launch would raise a chooser rather than return + // to the app. AC declines here too — its package comes from the bound component, which is + // only set for an unambiguous handler. + val authTargetPackage = if (resolved.isAmbiguous) null else resolved.packageName + + // §2.4 authentication carve-out (AC parity): a tab opened *by* the app the navigation + // targets is a sign-in round trip rather than a general app link, so it returns to its + // caller even under `never`. The forced-prompt contexts still win — a protected container, + // a private tab or a wallet scheme must not leak out silently, so those fall through to the + // classifier, which prompts for them regardless of mode (§2.4 step 4). + if (authExceptionsAllowed && + isAuthenticationCallback(session, authTargetPackage) && + !isProtectedNavigation && !isPrivateNavigation && !isWalletNavigation + ) { + val result = runtime.launcher.launch( + uri, + AppLinkLaunchMode.AUTHENTICATION, + expectedPackage = authTargetPackage, + ) + logger.info( + "auth app-link callback uri=$uri tab=${session?.id} " + + "caller=${callerPackage(session)} package=$authTargetPackage -> $result", + ) + return if (result == AppLinkLaunchResult.LAUNCHED) { + RequestInterceptor.InterceptionResponse.Deny + } else { + safeNonLaunchResponse(pendingStore, resolved) + } + } + val input = ClassifierInput( resolved = resolved, - isProtected = isProtected(policy, session, uri), - isPrivate = session?.content?.private ?: false, - isWallet = AppLinkSchemes.isWallet(resolved.originalScheme) || - AppLinkSchemes.isWallet(resolved.intentDataScheme), + isProtected = isProtectedNavigation, + isPrivate = isPrivateNavigation, + isWallet = isWalletNavigation, missingSession = session == null, suppressionHit = session != null && pendingStore.isSuppressed(session.id, targetFingerprint(uri, resolved)), @@ -262,6 +311,38 @@ class WebLibreAppLinksInterceptor( } } + /** + * True when [targetPackage] is the very app that opened this tab — the shape of a sign-in + * callback. [targetPackage] must be an unambiguous resolution; pass `null` otherwise. + */ + private fun isAuthenticationCallback(session: SessionState?, targetPackage: String?): Boolean { + if (targetPackage.isNullOrEmpty()) return false + return callerPackage(session) == targetPackage + } + + /** + * The package that launched this session, as recorded by + * [eu.weblibre.flutter_mozilla_components.activities.addExternalCallerInformation]. Note the + * underlying referrer is caller-supplied and can be spoofed, so this may only gate actions the + * caller could already perform itself (here: launching its own intent). + */ + private fun callerPackage(session: SessionState?): String? { + return when (val source = session?.source) { + is SessionState.Source.External.CustomTab -> source.caller?.packageId + is SessionState.Source.External.ActionView -> source.caller?.packageId + else -> null + } + } + + private fun isPossibleAuthentication(session: SessionState?): Boolean { + return when (session?.source) { + is SessionState.Source.External.CustomTab, + is SessionState.Source.External.ActionView, + -> true + else -> false + } + } + // ---- Eligibility (§2.4 step 2) ---- private fun isEligible( @@ -273,6 +354,7 @@ class WebLibreAppLinksInterceptor( isRedirect: Boolean, isDirectNavigation: Boolean, isSubframeRequest: Boolean, + authExceptionsAllowed: Boolean, ): Boolean { if (uriScheme == null) return false // A subframe request not triggered by the user and outside the allowlist stays in-page. @@ -282,8 +364,10 @@ class WebLibreAppLinksInterceptor( val isIntentionalNavigation = hasUserGesture || isAllowedRedirect || isDirectNavigation // Unintentional engine-supported navigation continues in the browser. if (engineSupportsScheme && !isIntentionalNavigation) return false - // Same-domain engine-supported navigation continues in the browser (AC subdomain stripping). - if (engineSupportsScheme && isSameDomain(lastUri, uri)) return false + // Same-domain engine-supported navigation continues in the browser (AC subdomain stripping), + // unless this tab could be hosting an authentication round trip whose callback is an http + // app link on the same site. + if (engineSupportsScheme && isSameDomain(lastUri, uri) && !authExceptionsAllowed) return false // Always-denied schemes never resolve or launch externally. if (AppLinkSchemes.isAlwaysDenied(uriScheme)) return false return true diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index e811d5e7..e9a7f2bc 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -5830,6 +5830,11 @@ data class AppLinkPolicySnapshot ( /** Remembered rules keyed by canonical scope. */ val rules: Map, val marketplaceFallbackEnabled: Boolean, + /** + * Allows same-caller Custom Tab / ActionView authentication callbacks to + * return to their app even when the general app-link mode is `never`. + */ + val authExceptionsEnabled: Boolean, /** Regular / no-contextId tabs are proxied via the `general` scope. */ val protectGeneralContext: Boolean, /** contextIds that resolve to a proxy after inherit/bypass/alias. */ @@ -5850,12 +5855,13 @@ data class AppLinkPolicySnapshot ( val globalMode = pigeonVar_list[0] as AppLinksMode val rules = pigeonVar_list[1] as Map val marketplaceFallbackEnabled = pigeonVar_list[2] as Boolean - val protectGeneralContext = pigeonVar_list[3] as Boolean - val protectedContextIds = pigeonVar_list[4] as List - val strictContextIds = pigeonVar_list[5] as List - val protectedTargetPatterns = pigeonVar_list[6] as List - val contextOverrides = pigeonVar_list[7] as Map - return AppLinkPolicySnapshot(globalMode, rules, marketplaceFallbackEnabled, protectGeneralContext, protectedContextIds, strictContextIds, protectedTargetPatterns, contextOverrides) + val authExceptionsEnabled = pigeonVar_list[3] as Boolean + val protectGeneralContext = pigeonVar_list[4] as Boolean + val protectedContextIds = pigeonVar_list[5] as List + val strictContextIds = pigeonVar_list[6] as List + val protectedTargetPatterns = pigeonVar_list[7] as List + val contextOverrides = pigeonVar_list[8] as Map + return AppLinkPolicySnapshot(globalMode, rules, marketplaceFallbackEnabled, authExceptionsEnabled, protectGeneralContext, protectedContextIds, strictContextIds, protectedTargetPatterns, contextOverrides) } } fun toList(): List { @@ -5863,6 +5869,7 @@ data class AppLinkPolicySnapshot ( globalMode, rules, marketplaceFallbackEnabled, + authExceptionsEnabled, protectGeneralContext, protectedContextIds, strictContextIds, @@ -5878,7 +5885,7 @@ data class AppLinkPolicySnapshot ( return true } val other = other as AppLinkPolicySnapshot - return GeckoPigeonUtils.deepEquals(this.globalMode, other.globalMode) && GeckoPigeonUtils.deepEquals(this.rules, other.rules) && GeckoPigeonUtils.deepEquals(this.marketplaceFallbackEnabled, other.marketplaceFallbackEnabled) && GeckoPigeonUtils.deepEquals(this.protectGeneralContext, other.protectGeneralContext) && GeckoPigeonUtils.deepEquals(this.protectedContextIds, other.protectedContextIds) && GeckoPigeonUtils.deepEquals(this.strictContextIds, other.strictContextIds) && GeckoPigeonUtils.deepEquals(this.protectedTargetPatterns, other.protectedTargetPatterns) && GeckoPigeonUtils.deepEquals(this.contextOverrides, other.contextOverrides) + return GeckoPigeonUtils.deepEquals(this.globalMode, other.globalMode) && GeckoPigeonUtils.deepEquals(this.rules, other.rules) && GeckoPigeonUtils.deepEquals(this.marketplaceFallbackEnabled, other.marketplaceFallbackEnabled) && GeckoPigeonUtils.deepEquals(this.authExceptionsEnabled, other.authExceptionsEnabled) && GeckoPigeonUtils.deepEquals(this.protectGeneralContext, other.protectGeneralContext) && GeckoPigeonUtils.deepEquals(this.protectedContextIds, other.protectedContextIds) && GeckoPigeonUtils.deepEquals(this.strictContextIds, other.strictContextIds) && GeckoPigeonUtils.deepEquals(this.protectedTargetPatterns, other.protectedTargetPatterns) && GeckoPigeonUtils.deepEquals(this.contextOverrides, other.contextOverrides) } override fun hashCode(): Int { @@ -5886,6 +5893,7 @@ data class AppLinkPolicySnapshot ( result = 31 * result + GeckoPigeonUtils.deepHash(this.globalMode) result = 31 * result + GeckoPigeonUtils.deepHash(this.rules) result = 31 * result + GeckoPigeonUtils.deepHash(this.marketplaceFallbackEnabled) + result = 31 * result + GeckoPigeonUtils.deepHash(this.authExceptionsEnabled) result = 31 * result + GeckoPigeonUtils.deepHash(this.protectGeneralContext) result = 31 * result + GeckoPigeonUtils.deepHash(this.protectedContextIds) result = 31 * result + GeckoPigeonUtils.deepHash(this.strictContextIds) @@ -5894,7 +5902,7 @@ data class AppLinkPolicySnapshot ( return result } override fun toString(): String { - return "AppLinkPolicySnapshot(globalMode=$globalMode, rules=$rules, marketplaceFallbackEnabled=$marketplaceFallbackEnabled, protectGeneralContext=$protectGeneralContext, protectedContextIds=$protectedContextIds, strictContextIds=$strictContextIds, protectedTargetPatterns=$protectedTargetPatterns, contextOverrides=$contextOverrides)" + return "AppLinkPolicySnapshot(globalMode=$globalMode, rules=$rules, marketplaceFallbackEnabled=$marketplaceFallbackEnabled, authExceptionsEnabled=$authExceptionsEnabled, protectGeneralContext=$protectGeneralContext, protectedContextIds=$protectedContextIds, strictContextIds=$strictContextIds, protectedTargetPatterns=$protectedTargetPatterns, contextOverrides=$contextOverrides)" } } diff --git a/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifierTest.kt b/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifierTest.kt index 30eb3c93..a8b4fb3e 100644 --- a/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifierTest.kt +++ b/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkClassifierTest.kt @@ -58,6 +58,11 @@ class AppLinkClassifierTest { // ---- §2.2 table: engine-supported (http) scheme, app resolves ---- + @Test + fun safeDefaultAllowsAuthExceptions() { + assertEquals(true, AppLinkPolicy.SAFE_DEFAULT.authExceptionsEnabled) + } + @Test fun engineSupportedAlwaysAutoLaunches() { val d = AppLinkClassifier.classify( diff --git a/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncherTest.kt b/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncherTest.kt index 36c2434c..61db1607 100644 --- a/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncherTest.kt +++ b/packages/flutter_mozilla_components/android/src/test/kotlin/eu/weblibre/flutter_mozilla_components/applinks/AppLinkLauncherTest.kt @@ -13,6 +13,7 @@ import kotlin.test.assertEquals import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito.mock +import org.mockito.Mockito.verify import org.mockito.Mockito.`when` class AppLinkLauncherTest { @@ -69,6 +70,33 @@ class AppLinkLauncherTest { assertEquals(1, started) } + @Test + fun authenticationLaunchUsesClearTopFlags() { + val intent = mock(Intent::class.java) + val resolved = ResolvedAppLink( + hasExternalApp = true, + appIntent = intent, + packageName = "com.example.app", + appName = "App", + fallbackUrl = null, + marketplaceIntent = null, + isAmbiguous = false, + engineSupportsScheme = false, + scopeKey = "pkg:com.example.app", + originalScheme = "example", + intentDataScheme = "example", + ) + var startedIntent: Intent? = null + val l = launcher(resolved, FakeClock()) { startedIntent = it } + + assertEquals( + AppLinkLaunchResult.LAUNCHED, + l.launch("example://callback", AppLinkLaunchMode.AUTHENTICATION), + ) + verify(intent).flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + assertEquals(intent, startedIntent) + } + @Test fun automaticLaunchWithinCooldownIsRefused() { val clock = FakeClock(1000L) @@ -78,6 +106,22 @@ class AppLinkLauncherTest { assertEquals(AppLinkLaunchResult.COOLDOWN, l.launch("zoommtg://x", AppLinkLaunchMode.AUTOMATIC)) } + @Test + fun authenticationLaunchWithinCooldownIsRefused() { + val clock = FakeClock(1000L) + val l = launcher(resolvedFor("com.example.app"), clock) + assertEquals( + AppLinkLaunchResult.LAUNCHED, + l.launch("zoommtg://x", AppLinkLaunchMode.AUTHENTICATION), + ) + // An app that re-opens its Custom Tab on receiving the callback would otherwise ping-pong. + clock.now = 1500L // < 2000 ms later + assertEquals( + AppLinkLaunchResult.COOLDOWN, + l.launch("zoommtg://x", AppLinkLaunchMode.AUTHENTICATION), + ) + } + @Test fun automaticLaunchAfterCooldownSucceeds() { val clock = FakeClock(1000L) diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index af4198ef..2e966013 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -6486,6 +6486,7 @@ class AppLinkPolicySnapshot { required this.globalMode, required this.rules, required this.marketplaceFallbackEnabled, + required this.authExceptionsEnabled, required this.protectGeneralContext, required this.protectedContextIds, required this.strictContextIds, @@ -6500,6 +6501,10 @@ class AppLinkPolicySnapshot { bool marketplaceFallbackEnabled; + /// Allows same-caller Custom Tab / ActionView authentication callbacks to + /// return to their app even when the general app-link mode is `never`. + bool authExceptionsEnabled; + /// Regular / no-contextId tabs are proxied via the `general` scope. bool protectGeneralContext; @@ -6521,6 +6526,7 @@ class AppLinkPolicySnapshot { globalMode, rules, marketplaceFallbackEnabled, + authExceptionsEnabled, protectGeneralContext, protectedContextIds, strictContextIds, @@ -6540,12 +6546,13 @@ class AppLinkPolicySnapshot { rules: (result[1]! as Map) .cast(), marketplaceFallbackEnabled: result[2]! as bool, - protectGeneralContext: result[3]! as bool, - protectedContextIds: (result[4]! as List).cast(), - strictContextIds: (result[5]! as List).cast(), - protectedTargetPatterns: (result[6]! as List) + authExceptionsEnabled: result[3]! as bool, + protectGeneralContext: result[4]! as bool, + protectedContextIds: (result[5]! as List).cast(), + strictContextIds: (result[6]! as List).cast(), + protectedTargetPatterns: (result[7]! as List) .cast(), - contextOverrides: (result[7]! as Map) + contextOverrides: (result[8]! as Map) .cast(), ); } @@ -6565,6 +6572,7 @@ class AppLinkPolicySnapshot { marketplaceFallbackEnabled, other.marketplaceFallbackEnabled, ) && + _deepEquals(authExceptionsEnabled, other.authExceptionsEnabled) && _deepEquals(protectGeneralContext, other.protectGeneralContext) && _deepEquals(protectedContextIds, other.protectedContextIds) && _deepEquals(strictContextIds, other.strictContextIds) && @@ -6578,7 +6586,7 @@ class AppLinkPolicySnapshot { @override String toString() { - return 'AppLinkPolicySnapshot(globalMode: $globalMode, rules: $rules, marketplaceFallbackEnabled: $marketplaceFallbackEnabled, protectGeneralContext: $protectGeneralContext, protectedContextIds: $protectedContextIds, strictContextIds: $strictContextIds, protectedTargetPatterns: $protectedTargetPatterns, contextOverrides: $contextOverrides)'; + return 'AppLinkPolicySnapshot(globalMode: $globalMode, rules: $rules, marketplaceFallbackEnabled: $marketplaceFallbackEnabled, authExceptionsEnabled: $authExceptionsEnabled, protectGeneralContext: $protectGeneralContext, protectedContextIds: $protectedContextIds, strictContextIds: $strictContextIds, protectedTargetPatterns: $protectedTargetPatterns, contextOverrides: $contextOverrides)'; } } diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index 305d30a6..b4210d71 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -2994,6 +2994,10 @@ class AppLinkPolicySnapshot { final bool marketplaceFallbackEnabled; + /// Allows same-caller Custom Tab / ActionView authentication callbacks to + /// return to their app even when the general app-link mode is `never`. + final bool authExceptionsEnabled; + /// Regular / no-contextId tabs are proxied via the `general` scope. final bool protectGeneralContext; @@ -3014,6 +3018,7 @@ class AppLinkPolicySnapshot { required this.globalMode, required this.rules, required this.marketplaceFallbackEnabled, + required this.authExceptionsEnabled, required this.protectGeneralContext, required this.protectedContextIds, required this.strictContextIds,