From c6d097f2db6ff3f43d718296653b19b45026e039 Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Mon, 14 Sep 2026 00:11:29 +0200 Subject: [PATCH] feat(android): allow restricting which browser handles auth requests Adds an `androidAllowedBrowsers` property to `AuthorizationRequest`, `AuthorizationTokenRequest` and `EndSessionRequest`. Each entry is either one of six presets that map to AppAuth's `VersionedBrowserMatcher` constants, or a custom descriptor naming any browser by package name and signature hashes. The list becomes a `BrowserAllowList` on the `AppAuthConfiguration`. A null or empty list keeps the existing behaviour of allowing any installed browser. The browser matcher is fixed when an `AuthorizationService` is built, so the two pre-built services are replaced with a cache keyed by connection security and browser list. Caching rather than recreating avoids disposing a service while an authorization flow is still in progress. Also wraps the end session intent launch in the same `ActivityNotFoundException` handling that `performAuthorization` already had. Without it, an allow list that matches no installed browser would throw instead of returning a `no_browser_available` error. Refs https://github.com/MaikuB/flutter_appauth/issues/537 Co-Authored-By: Claude Opus 5 (1M context) --- flutter_appauth/CHANGELOG.md | 5 + flutter_appauth/README.md | 35 ++++ .../flutterappauth/FlutterAppauthPlugin.java | 124 ++++++++++---- flutter_appauth/lib/flutter_appauth.dart | 1 + .../CHANGELOG.md | 4 + .../flutter_appauth_platform_interface.dart | 1 + .../lib/src/android_browser.dart | 93 +++++++++++ .../lib/src/authorization_parameters.dart | 10 ++ .../lib/src/authorization_request.dart | 3 + .../lib/src/authorization_token_request.dart | 3 + .../lib/src/end_session_request.dart | 10 ++ .../lib/src/method_channel_mappers.dart | 7 + .../method_channel_flutter_appauth_test.dart | 152 ++++++++++++++++++ 13 files changed, 421 insertions(+), 27 deletions(-) create mode 100644 flutter_appauth_platform_interface/lib/src/android_browser.dart diff --git a/flutter_appauth/CHANGELOG.md b/flutter_appauth/CHANGELOG.md index 44d34f5b..f4b7f5da 100644 --- a/flutter_appauth/CHANGELOG.md +++ b/flutter_appauth/CHANGELOG.md @@ -1,3 +1,8 @@ +## [Unreleased] + +* [Android] Added `androidAllowedBrowsers` property to the `AuthorizationRequest`, `AuthorizationTokenRequest` and `EndSessionRequest` classes to restrict which browser (or Custom Tab implementation) is allowed to handle the request. See the "Restricting the Android browser" section of the README for details +* [Android] Fixed `endSession` throwing an uncaught `ActivityNotFoundException` instead of returning a `no_browser_available` error when no suitable browser is installed + ## [13.0.0-dev.1] * **Breaking change** updated minimum supported SDK version to Flutter 3.44.0/Dart 3.12.0 diff --git a/flutter_appauth/README.md b/flutter_appauth/README.md index ad6b5e5d..9e3f4be1 100644 --- a/flutter_appauth/README.md +++ b/flutter_appauth/README.md @@ -233,6 +233,41 @@ Attribute application@name at AndroidManifest.xml:5:9-42 requires a placeholder If you see this error then update your `build.gradle` to use `+=` instead. +### Restricting the Android browser + +By default, AppAuth for Android will use whichever installed browser it considers best, falling back through Chrome Custom Tabs, standalone browsers, Custom Tabs from other browsers etc. The `androidAllowedBrowsers` property on `AuthorizationRequest`, `AuthorizationTokenRequest` and `EndSessionRequest` restricts this to a specific list of browsers, for example to force Chrome + +```dart +final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode( + AuthorizationTokenRequest( + '', + '', + discoveryUrl: '', + androidAllowedBrowsers: [ + AndroidBrowser.chromeCustomTab, + AndroidBrowser.chromeBrowser, + ], + ), + ); +``` + +The `AndroidBrowser` class offers six presets covering Chrome, Firefox and the Samsung Internet browser, each as either a Custom Tab or a standalone browser: `chromeCustomTab`, `chromeBrowser`, `firefoxCustomTab`, `firefoxBrowser`, `samsungCustomTab` and `samsungBrowser`. For a browser that isn't covered by a preset (e.g. one pushed to devices via MDM), use `AndroidBrowser.custom` and provide its package name, signing certificate hash(es) and whether it should be used as a Custom Tab + +```dart +androidAllowedBrowsers: [ + AndroidBrowser.custom( + packageName: 'com.acme.mdmbrowser', + signatureHashes: {''}, + useCustomTab: true, + minVersion: '12', + ), +], +``` + +A custom browser's signature hash can be obtained from AppAuth for Android's `BrowserDescriptor.generateSignatureHash`, given the `PackageInfo` of the installed browser APK. + +If none of the installed browsers on the device match the allow-list, the request fails the same way it would if no browser were installed at all (a `no_browser_available` error). This property is Android-only; a `null` or empty list means any installed browser may be used, which is the existing behaviour. + ### Troubleshooting #### No Redirect to app after login diff --git a/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java b/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java index a24b4894..d5b6cd91 100644 --- a/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java +++ b/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java @@ -15,6 +15,11 @@ import net.openid.appauth.AuthorizationResponse; import net.openid.appauth.AuthorizationService; import net.openid.appauth.AuthorizationServiceConfiguration; +import net.openid.appauth.browser.AnyBrowserMatcher; +import net.openid.appauth.browser.BrowserAllowList; +import net.openid.appauth.browser.BrowserMatcher; +import net.openid.appauth.browser.VersionRange; +import net.openid.appauth.browser.VersionedBrowserMatcher; import net.openid.appauth.ClientSecretBasic; import net.openid.appauth.EndSessionRequest; import net.openid.appauth.EndSessionResponse; @@ -30,7 +35,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; @@ -63,6 +71,8 @@ public class FlutterAppauthPlugin private static final String NULL_ACTIVITY_ERROR_CODE = "null_activity"; private static final String INVALID_CLAIMS_ERROR_CODE = "invalid_claims"; private static final String NO_BROWSER_AVAILABLE_ERROR_CODE = "no_browser_available"; + private static final String INVALID_ANDROID_ALLOWED_BROWSER_ERROR_CODE = + "invalid_android_allowed_browser"; private static final String DISCOVERY_ERROR_MESSAGE_FORMAT = "Error retrieving discovery document: [error: %s, description: %s]"; @@ -80,6 +90,8 @@ public class FlutterAppauthPlugin "Failed to authorize: Null activity received"; private static final String NO_BROWSER_AVAILABLE_ERROR_FORMAT = "Failed to authorize: No suitable browser is available"; + private static final String INVALID_ANDROID_ALLOWED_BROWSER_ERROR_FORMAT = + "Unrecognised androidAllowedBrowsers preset: %s"; private final int RC_AUTH_EXCHANGE_CODE = 65030; private final int RC_AUTH = 65031; @@ -90,12 +102,11 @@ public class FlutterAppauthPlugin private PendingOperation pendingOperation; private String clientSecret; private boolean allowInsecureConnections; - private AuthorizationService defaultAuthorizationService; - private AuthorizationService insecureAuthorizationService; + private List> androidAllowedBrowsers; + private final Map authorizationServices = new HashMap<>(); private void onAttachedToEngine(Context context, BinaryMessenger binaryMessenger) { this.applicationContext = context; - createAuthorizationServices(); final MethodChannel channel = new MethodChannel(binaryMessenger, "crossingthestreams.io/flutter_appauth"); channel.setMethodCallHandler(this); @@ -133,25 +144,13 @@ public void onDetachedFromActivity() { this.mainActivity = null; } - private void createAuthorizationServices() { - if (defaultAuthorizationService == null) { - defaultAuthorizationService = new AuthorizationService(this.applicationContext); - } - - if (insecureAuthorizationService == null) { - AppAuthConfiguration.Builder authConfigBuilder = new AppAuthConfiguration.Builder(); - authConfigBuilder.setConnectionBuilder(InsecureConnectionBuilder.INSTANCE); - authConfigBuilder.setSkipIssuerHttpsCheck(true); - insecureAuthorizationService = - new AuthorizationService(applicationContext, authConfigBuilder.build()); - } - } - private void disposeAuthorizationServices() { - defaultAuthorizationService.dispose(); - insecureAuthorizationService.dispose(); - defaultAuthorizationService = null; - insecureAuthorizationService = null; + for (AuthorizationService authorizationService : authorizationServices.values()) { + if (authorizationService != null) { + authorizationService.dispose(); + } + } + authorizationServices.clear(); } private void checkAndSetPendingOperation(String method, Result result) { @@ -221,6 +220,8 @@ private AuthorizationTokenRequestParameters processAuthorizationTokenRequestArgu (Map) arguments.get("additionalParameters"); allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections"); final String responseMode = (String) arguments.get("responseMode"); + androidAllowedBrowsers = + (List>) arguments.get("androidAllowedBrowsers"); return new AuthorizationTokenRequestParameters( clientId, @@ -266,6 +267,9 @@ private TokenRequestParameters processTokenRequestArguments(Map final Map additionalParameters = (Map) arguments.get("additionalParameters"); allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections"); + // A bare token/refresh call never opens a browser, so it must not inherit a stale + // browser list left over from an earlier authorize call. + androidAllowedBrowsers = null; return new TokenRequestParameters( clientId, issuer, @@ -294,6 +298,8 @@ private EndSessionRequestParameters processEndSessionRequestArguments( (Map) arguments.get("serviceConfiguration"); final Map additionalParameters = (Map) arguments.get("additionalParameters"); + androidAllowedBrowsers = + (List>) arguments.get("androidAllowedBrowsers"); return new EndSessionRequestParameters( idTokenHint, postLogoutRedirectUrl, @@ -581,24 +587,88 @@ private void performEndSessionRequest( final EndSessionRequest endSessionRequest = endSessionRequestBuilder.build(); AuthorizationService authorizationService = getAuthorizationService(); - Intent endSessionIntent = authorizationService.getEndSessionRequestIntent(endSessionRequest); - try { + Intent endSessionIntent = authorizationService.getEndSessionRequestIntent(endSessionRequest); mainActivity.startActivityForResult(endSessionIntent, RC_END_SESSION); + } catch (ActivityNotFoundException ex) { + finishWithError(NO_BROWSER_AVAILABLE_ERROR_CODE, NO_BROWSER_AVAILABLE_ERROR_FORMAT, ex); } catch (NullPointerException ex) { finishWithError(NULL_ACTIVITY_ERROR_CODE, NULL_ACTIVITY_ERROR_FORMAT, ex); } } private AuthorizationService getAuthorizationService() { - // Call to createAuthorizationService() is done as there have been some reported instances where + final String key = + authorizationServiceCacheKey(allowInsecureConnections, androidAllowedBrowsers); + // Services are built on a cache miss as there have been some reported instances where // the services have been disposed but they're still needed e.g. to refresh tokens - createAuthorizationServices(); - AuthorizationService authorizationService = - allowInsecureConnections ? insecureAuthorizationService : defaultAuthorizationService; + AuthorizationService authorizationService = authorizationServices.get(key); + if (authorizationService == null) { + AppAuthConfiguration.Builder authConfigBuilder = new AppAuthConfiguration.Builder(); + if (allowInsecureConnections) { + authConfigBuilder.setConnectionBuilder(InsecureConnectionBuilder.INSTANCE); + authConfigBuilder.setSkipIssuerHttpsCheck(true); + } + authConfigBuilder.setBrowserMatcher(buildBrowserMatcher(androidAllowedBrowsers)); + authorizationService = + new AuthorizationService(applicationContext, authConfigBuilder.build()); + authorizationServices.put(key, authorizationService); + } return authorizationService; } + private String authorizationServiceCacheKey( + boolean allowInsecureConnections, List> androidAllowedBrowsers) { + return allowInsecureConnections + "|" + androidAllowedBrowsers; + } + + private BrowserMatcher buildBrowserMatcher(List> specs) { + if (specs == null || specs.isEmpty()) { + return AnyBrowserMatcher.INSTANCE; + } + List matchers = new ArrayList<>(); + for (Map spec : specs) { + final String preset = (String) spec.get("preset"); + if (preset != null) { + matchers.add(presetBrowserMatcher(preset)); + continue; + } + final String packageName = (String) spec.get("packageName"); + @SuppressWarnings("unchecked") + final Set hashes = new HashSet<>((List) spec.get("signatureHashes")); + final boolean useCustomTab = Boolean.TRUE.equals(spec.get("useCustomTab")); + final String minVersion = (String) spec.get("minVersion"); + final VersionRange range = + minVersion == null ? VersionRange.ANY_VERSION : VersionRange.atLeast(minVersion); + matchers.add(new VersionedBrowserMatcher(packageName, hashes, useCustomTab, range)); + } + return new BrowserAllowList(matchers.toArray(new BrowserMatcher[0])); + } + + private BrowserMatcher presetBrowserMatcher(String preset) { + switch (preset) { + case "chromeCustomTab": + return VersionedBrowserMatcher.CHROME_CUSTOM_TAB; + case "chromeBrowser": + return VersionedBrowserMatcher.CHROME_BROWSER; + case "firefoxCustomTab": + return VersionedBrowserMatcher.FIREFOX_CUSTOM_TAB; + case "firefoxBrowser": + return VersionedBrowserMatcher.FIREFOX_BROWSER; + case "samsungCustomTab": + return VersionedBrowserMatcher.SAMSUNG_CUSTOM_TAB; + case "samsungBrowser": + return VersionedBrowserMatcher.SAMSUNG_BROWSER; + default: + finishWithError( + INVALID_ANDROID_ALLOWED_BROWSER_ERROR_CODE, + String.format(INVALID_ANDROID_ALLOWED_BROWSER_ERROR_FORMAT, preset), + null); + throw new IllegalArgumentException( + String.format(INVALID_ANDROID_ALLOWED_BROWSER_ERROR_FORMAT, preset)); + } + } + private void finishWithTokenError(AuthorizationException ex) { finishWithError( TOKEN_ERROR_CODE, diff --git a/flutter_appauth/lib/flutter_appauth.dart b/flutter_appauth/lib/flutter_appauth.dart index 0ea2ac94..7e88dc9c 100644 --- a/flutter_appauth/lib/flutter_appauth.dart +++ b/flutter_appauth/lib/flutter_appauth.dart @@ -1,5 +1,6 @@ export 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart' show + AndroidBrowser, AuthorizationRequest, AuthorizationResponse, AuthorizationServiceConfiguration, diff --git a/flutter_appauth_platform_interface/CHANGELOG.md b/flutter_appauth_platform_interface/CHANGELOG.md index 75d299b8..61947ac7 100644 --- a/flutter_appauth_platform_interface/CHANGELOG.md +++ b/flutter_appauth_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## [Unreleased] + +* Added `androidAllowedBrowsers` property to the `AuthorizationRequest`, `AuthorizationTokenRequest` and `EndSessionRequest` classes to restrict which Android browser (or Custom Tab implementation) is allowed to handle the request. Accepts a list of `AndroidBrowser` entries, either one of the built-in presets (e.g. `AndroidBrowser.chromeCustomTab`) or a custom browser described via `AndroidBrowser.custom`. This is only applicable to Android + ## [13.0.0-dev.1] * **Breaking change** updated minimum supported SDK version to Flutter 3.44.0/Dart 3.12.0 diff --git a/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart b/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart index 7039340c..fa053d80 100644 --- a/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart +++ b/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart @@ -1,3 +1,4 @@ +export 'src/android_browser.dart'; export 'src/authorization_request.dart'; export 'src/authorization_response.dart'; export 'src/authorization_service_configuration.dart'; diff --git a/flutter_appauth_platform_interface/lib/src/android_browser.dart b/flutter_appauth_platform_interface/lib/src/android_browser.dart new file mode 100644 index 00000000..2c911cf3 --- /dev/null +++ b/flutter_appauth_platform_interface/lib/src/android_browser.dart @@ -0,0 +1,93 @@ +/// Describes a browser (or custom tab implementation) that is allowed to +/// handle an authorization or end session request. +/// +/// This is only applicable to Android. Use the `chrome*`, `firefox*` and +/// `samsung*` presets to restrict the request to one of the browsers that +/// AppAuth for Android already recognises, or [AndroidBrowser.custom] to +/// describe a browser that isn't covered by a preset e.g. one pushed to +/// devices via MDM. +class AndroidBrowser { + const AndroidBrowser._(this.preset) + : packageName = null, + signatureHashes = null, + useCustomTab = null, + minVersion = null; + + /// Describes a custom browser (or custom tab implementation) by its package + /// name and signing certificate. + /// + /// [packageName] is the browser's package name, e.g. `com.acme.browser`. + /// + /// [signatureHashes] are the browser's Base64-encoded, SHA-512 signing + /// certificate hashes, as returned by AppAuth for Android's + /// `BrowserDescriptor.generateSignatureHash`. A browser matches if it was + /// signed with any of the supplied hashes. + /// + /// [useCustomTab] indicates whether the browser should be used as a Chrome + /// Custom Tab (`true`) or as a standalone browser (`false`). Defaults to + /// `true`. + /// + /// [minVersion] is the minimum version of the browser that is allowed. When + /// omitted, any version is allowed. + const AndroidBrowser.custom({ + required this.packageName, + required this.signatureHashes, + this.useCustomTab = true, + this.minVersion, + }) : preset = null; + + /// Chrome, used as a Chrome Custom Tab. + static const AndroidBrowser chromeCustomTab = + AndroidBrowser._('chromeCustomTab'); + + /// Chrome, used as a standalone browser. + static const AndroidBrowser chromeBrowser = AndroidBrowser._('chromeBrowser'); + + /// Firefox, used as a Custom Tab. + static const AndroidBrowser firefoxCustomTab = + AndroidBrowser._('firefoxCustomTab'); + + /// Firefox, used as a standalone browser. + static const AndroidBrowser firefoxBrowser = + AndroidBrowser._('firefoxBrowser'); + + /// The Samsung Internet browser, used as a Custom Tab. + static const AndroidBrowser samsungCustomTab = + AndroidBrowser._('samsungCustomTab'); + + /// The Samsung Internet browser, used as a standalone browser. + static const AndroidBrowser samsungBrowser = + AndroidBrowser._('samsungBrowser'); + + /// The name Android resolves to one of AppAuth for Android's built-in + /// browser matchers, or `null` for a [AndroidBrowser.custom] entry. + final String? preset; + + /// The custom browser's package name, or `null` for a preset entry. + final String? packageName; + + /// The custom browser's Base64-encoded signing certificate hashes, or + /// `null` for a preset entry. + final Set? signatureHashes; + + /// Whether the custom browser should be used as a Custom Tab, or `null` for + /// a preset entry. + final bool? useCustomTab; + + /// The minimum allowed version of the custom browser, or `null` for a + /// preset entry or when any version is allowed. + final String? minVersion; + + /// Converts this to a map that can be sent over the method channel. + Map toMap() { + if (preset != null) { + return {'preset': preset}; + } + return { + 'packageName': packageName, + 'signatureHashes': signatureHashes?.toList(), + 'useCustomTab': useCustomTab, + 'minVersion': minVersion, + }; + } +} diff --git a/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart b/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart index 88572d66..b9268f65 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart @@ -1,3 +1,4 @@ +import 'android_browser.dart'; import 'external_user_agent.dart'; mixin AuthorizationParameters { @@ -14,4 +15,13 @@ mixin AuthorizationParameters { /// Specifies the response mode to use. String? responseMode; + + /// Restricts which Android browsers (or Custom Tab implementations) are + /// allowed to handle the request. + /// + /// This is only applicable to Android. A `null` or empty list means any + /// installed browser may be used, which is the existing behaviour. If none + /// of the installed browsers match, the request fails the same way it + /// would if no browser were installed at all. + List? androidAllowedBrowsers; } diff --git a/flutter_appauth_platform_interface/lib/src/authorization_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_request.dart index a0a2fd3a..7a5f32f0 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_request.dart @@ -1,3 +1,4 @@ +import 'android_browser.dart'; import 'authorization_parameters.dart'; import 'authorization_service_configuration.dart'; import 'common_request_details.dart'; @@ -21,6 +22,7 @@ class AuthorizationRequest extends CommonRequestDetails ExternalUserAgent.asWebAuthenticationSession, String? nonce, String? responseMode, + List? androidAllowedBrowsers, }) { this.clientId = clientId; this.redirectUrl = redirectUrl; @@ -35,6 +37,7 @@ class AuthorizationRequest extends CommonRequestDetails this.externalUserAgent = externalUserAgent; this.nonce = nonce; this.responseMode = responseMode; + this.androidAllowedBrowsers = androidAllowedBrowsers; assertConfigurationInfo(); } } diff --git a/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart index 8436c45d..cd72df3a 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart @@ -1,3 +1,4 @@ +import 'android_browser.dart'; import 'authorization_parameters.dart'; import 'external_user_agent.dart'; import 'grant_type.dart'; @@ -22,6 +23,7 @@ class AuthorizationTokenRequest extends TokenRequest ExternalUserAgent.asWebAuthenticationSession, super.nonce, String? responseMode, + List? androidAllowedBrowsers, }) : super( grantType: GrantType.authorizationCode, ) { @@ -29,5 +31,6 @@ class AuthorizationTokenRequest extends TokenRequest this.promptValues = promptValues; this.externalUserAgent = externalUserAgent; this.responseMode = responseMode; + this.androidAllowedBrowsers = androidAllowedBrowsers; } } diff --git a/flutter_appauth_platform_interface/lib/src/end_session_request.dart b/flutter_appauth_platform_interface/lib/src/end_session_request.dart index 0c1cff12..43638d41 100644 --- a/flutter_appauth_platform_interface/lib/src/end_session_request.dart +++ b/flutter_appauth_platform_interface/lib/src/end_session_request.dart @@ -10,6 +10,7 @@ class EndSessionRequest with AcceptedAuthorizationServiceConfigurationDetails { this.allowInsecureConnections = false, this.externalUserAgent = ExternalUserAgent.asWebAuthenticationSession, this.additionalParameters, + this.androidAllowedBrowsers, String? issuer, String? discoveryUrl, AuthorizationServiceConfiguration? serviceConfiguration, @@ -42,4 +43,13 @@ class EndSessionRequest with AcceptedAuthorizationServiceConfigurationDetails { /// Additional parameters to include in the request. final Map? additionalParameters; + + /// Restricts which Android browsers (or Custom Tab implementations) are + /// allowed to handle the request. + /// + /// This is only applicable to Android. A `null` or empty list means any + /// installed browser may be used, which is the existing behaviour. If none + /// of the installed browsers match, the request fails the same way it + /// would if no browser were installed at all. + List? androidAllowedBrowsers; } diff --git a/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart b/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart index aab82ea2..9d0aa3da 100644 --- a/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart +++ b/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart @@ -1,3 +1,4 @@ +import 'android_browser.dart'; import 'authorization_parameters.dart'; import 'authorization_request.dart'; import 'authorization_service_configuration.dart'; @@ -34,6 +35,9 @@ extension EndSessionRequestMapper on EndSessionRequest { 'discoveryUrl': discoveryUrl, 'serviceConfiguration': serviceConfiguration?.toMap(), 'externalUserAgent': externalUserAgent?.index, + 'androidAllowedBrowsers': androidAllowedBrowsers + ?.map((AndroidBrowser browser) => browser.toMap()) + .toList(), }; } } @@ -101,5 +105,8 @@ Map _convertAuthorizationParametersToMap( 'promptValues': authorizationParameters.promptValues, 'externalUserAgent': authorizationParameters.externalUserAgent?.index, 'responseMode': authorizationParameters.responseMode, + 'androidAllowedBrowsers': authorizationParameters.androidAllowedBrowsers + ?.map((AndroidBrowser browser) => browser.toMap()) + .toList(), }; } diff --git a/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart b/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart index dd59971e..9f838a31 100644 --- a/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart +++ b/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart @@ -43,6 +43,7 @@ void main() { 'promptValues': null, 'responseMode': null, 'nonce': null, + 'androidAllowedBrowsers': null, }) ], ); @@ -77,6 +78,7 @@ void main() { 'codeVerifier': null, 'responseMode': 'fragment', 'nonce': null, + 'androidAllowedBrowsers': null, }) ], ); @@ -187,6 +189,156 @@ void main() { 'discoveryUrl': 'someDiscoveryUrl', 'serviceConfiguration': null, 'externalUserAgent': ExternalUserAgent.asWebAuthenticationSession.index, + 'androidAllowedBrowsers': null, + }) + ]); + }); + + test('authorize with androidAllowedBrowsers presets', () async { + await flutterAppAuth.authorize(AuthorizationRequest( + 'someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + androidAllowedBrowsers: [ + AndroidBrowser.chromeCustomTab, + AndroidBrowser.chromeBrowser, + ])); + expect( + log, + [ + isMethodCall('authorize', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'loginHint': null, + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'externalUserAgent': + ExternalUserAgent.asWebAuthenticationSession.index, + 'promptValues': null, + 'responseMode': null, + 'nonce': null, + 'androidAllowedBrowsers': >[ + {'preset': 'chromeCustomTab'}, + {'preset': 'chromeBrowser'}, + ], + }) + ], + ); + }); + + test('authorize with a custom androidAllowedBrowsers entry', () async { + await flutterAppAuth.authorize(AuthorizationRequest( + 'someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + androidAllowedBrowsers: [ + const AndroidBrowser.custom( + packageName: 'com.acme.mdmbrowser', + signatureHashes: {'AbC123...'}, + useCustomTab: true, + minVersion: '12', + ), + ])); + expect( + log, + [ + isMethodCall('authorize', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'loginHint': null, + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'externalUserAgent': + ExternalUserAgent.asWebAuthenticationSession.index, + 'promptValues': null, + 'responseMode': null, + 'nonce': null, + 'androidAllowedBrowsers': >[ + { + 'packageName': 'com.acme.mdmbrowser', + 'signatureHashes': ['AbC123...'], + 'useCustomTab': true, + 'minVersion': '12', + }, + ], + }) + ], + ); + }); + + test('authorize with a mixed androidAllowedBrowsers list', () async { + await flutterAppAuth.authorize(AuthorizationRequest( + 'someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + androidAllowedBrowsers: [ + AndroidBrowser.chromeCustomTab, + const AndroidBrowser.custom( + packageName: 'com.acme.mdmbrowser', + signatureHashes: {'AbC123...'}, + useCustomTab: false, + ), + ])); + expect( + log, + [ + isMethodCall('authorize', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'loginHint': null, + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'externalUserAgent': + ExternalUserAgent.asWebAuthenticationSession.index, + 'promptValues': null, + 'responseMode': null, + 'nonce': null, + 'androidAllowedBrowsers': >[ + {'preset': 'chromeCustomTab'}, + { + 'packageName': 'com.acme.mdmbrowser', + 'signatureHashes': ['AbC123...'], + 'useCustomTab': false, + 'minVersion': null, + }, + ], + }) + ], + ); + }); + + test('endSession with androidAllowedBrowsers', () async { + await flutterAppAuth.endSession(EndSessionRequest( + idTokenHint: 'someIdToken', + postLogoutRedirectUrl: 'somePostLogoutRedirectUrl', + state: 'someState', + discoveryUrl: 'someDiscoveryUrl', + androidAllowedBrowsers: [ + AndroidBrowser.chromeCustomTab, + ])); + expect(log, [ + isMethodCall('endSession', arguments: { + 'idTokenHint': 'someIdToken', + 'postLogoutRedirectUrl': 'somePostLogoutRedirectUrl', + 'state': 'someState', + 'allowInsecureConnections': false, + 'additionalParameters': null, + 'issuer': null, + 'discoveryUrl': 'someDiscoveryUrl', + 'serviceConfiguration': null, + 'externalUserAgent': ExternalUserAgent.asWebAuthenticationSession.index, + 'androidAllowedBrowsers': >[ + {'preset': 'chromeCustomTab'}, + ], }) ]); });