Repository navigation
[flutter_appauth] feat(android): allow restricting which browser handles auth requests - #682
ddfreiling wants to merge 7 commits into
Conversation
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 MaikuB#537 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Nullable custom-browser parameters can trigger uncaught Android exceptions during discovery-based requests.
3 open issues
What changed in this PR
Adds Android browser restrictions to authorization and logout requests, addressing #537 while preserving unrestricted browser selection by default.
Changes:
- Adds browser presets and custom descriptors to the Dart API and method channel.
- Caches Android authorization services by browser policy and connection security.
- Handles missing browsers during logout and documents the new options.
| File | Description |
|---|---|
| flutter_appauth/README.md | Documents browser restrictions and custom descriptors. |
| flutter_appauth/lib/flutter_appauth.dart | Exports AndroidBrowser. |
| flutter_appauth/CHANGELOG.md | Records Android enhancements. |
| flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java | Applies browser matchers, caches services, and handles logout launch errors. |
| flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart | Tests browser option serialization. |
| flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart | Serializes browser restrictions. |
| flutter_appauth_platform_interface/lib/src/end_session_request.dart | Adds logout browser restrictions. |
| flutter_appauth_platform_interface/lib/src/authorization_token_request.dart | Accepts browser restrictions for combined authorization. |
| flutter_appauth_platform_interface/lib/src/authorization_request.dart | Accepts authorization browser restrictions. |
| flutter_appauth_platform_interface/lib/src/authorization_parameters.dart | Defines the shared browser option. |
| flutter_appauth_platform_interface/lib/src/android_browser.dart | Defines presets and custom descriptors. |
| flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart | Exports the browser model. |
| flutter_appauth_platform_interface/CHANGELOG.md | Records the new public API. |
Review effort: Balanced
| ], | ||
| ``` | ||
|
|
||
| A custom browser's signature hash can be obtained from AppAuth for Android's `BrowserDescriptor.generateSignatureHash`, given the `PackageInfo` of the installed browser APK. |
There was a problem hiding this comment.
Based on my understanding, this would require developers to write their own Java or Kotlin code to do this? Can you confirm if this is correct? If so, I would recommend this is made more explicit as Flutter developers may not realise this. I also wonder if there's an opportunity to provide more guidance on this and/or if this plugin can provide some way to return the hashes by exposing a Dart API that can do what is stated
There was a problem hiding this comment.
Yes, that's correct. Until now you'd have had to write your own Kotlin/Java to compute the hashes, which isn't obvious for a Flutter developer.
I've now added FlutterAppAuth.getInstalledAndroidBrowsers(). It wraps AppAuth's BrowserSelector.getAllBrowsers and returns each installed browser's package name, version, Custom Tab mode and signature hashes, computed exactly the way the matcher compares them. I checked it on my phone, where Chrome's reported hash matches AppAuth's built-in Chrome constant.
It's mostly meant as a development aid: run it once on a test device, then hard-code the hashes in AndroidBrowser.custom. Hashes read at runtime and passed straight back into androidAllowedBrowsers would pin nothing, because a look-alike app installed under the same package name would be allowed too. The README section now says this and includes a short example.
I also addressed the Copilot findings. Custom entries now require a non-null package name and hashes, and invalid entries fail with an error instead of crashing the app.
The docs now also explain that a browser has to match the complete set of hashes, not just one of them.
A custom allowed browser with a null package name or signature hashes threw while the authorization service was built. For discovery-based requests that happens in the discovery callback, outside the method call's error handling, so it crashed the app. Unknown presets had the same problem. The browser matcher is now built while the request arguments are read, so invalid entries fail with invalid_android_allowed_browser. The Dart constructor also requires non-null package name and hashes. AppAuth matches a browser only when its signature hashes equal the supplied set, not when any one hash matches. The docs now say so and name the right AppAuth helper for computing the hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pinning a custom browser with AndroidBrowser.custom needs its signature hashes, which so far meant writing native code to compute them. The new method lists the browsers AppAuth can see on the device, with package name, version, Custom Tab mode and signature hashes. It wraps AppAuth's BrowserSelector.getAllBrowsers, so the hashes are exactly what the matcher compares. It is meant for development: read the hashes on a test device, then hard-code them. Hashes read at runtime and passed straight back would also allow a look-alike app under the same package name. The README and dartdoc say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Invalid androidAllowedBrowsers entries now fail with the calling method's error code (e.g. authorize_failed) rather than a dedicated invalid_android_allowed_browser code. Only a version mismatch between the Dart and native code can reach them, no app would branch on the code, and the message still says what is wrong. This removes a helper that reported the error and then relied on the catch block finding no pending operation left. Also inlines the one-line cache key helper, and drops a null check in disposeAuthorizationServices that can never trigger, as the service cache only holds services it has just created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser-matcher * upstream/master: [flutter_appauth] removed global variable declarations in Objective C plugin code (MaikuB#685) [flutter_appauth] bumped AppAuth iOS SDK to 3.0.0 (MaikuB#684) Clang Format [flutter_appauth] Improve SFSafariViewController user cancellation handling (MaikuB#670) # Conflicts: # flutter_appauth/CHANGELOG.md
AuthorizationService picks and stores a browser when it is built, so the cached services kept a stale choice: a browser installed, uninstalled or replaced under the same package name was never reselected. The cache also grew by one service per distinct browser list. Create one service per operation and dispose the previous one when the next operation starts. Only one operation can be pending, so that service is no longer in use. The services are no longer created at engine attach, so the Custom Tabs connection is not warmed up from app start. The first Custom Tab of each sign-in or sign-out may open slightly slower. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ser-matcher * upstream/master: prepare for 13.0.0 release (MaikuB#687) # Conflicts: # flutter_appauth/CHANGELOG.md # flutter_appauth_platform_interface/CHANGELOG.md



What
Adds an
androidAllowedBrowsersproperty toAuthorizationRequest,AuthorizationTokenRequestandEndSessionRequest. Each entry is either one of six presets that map to AppAuth'sVersionedBrowserMatcherconstants, or a custom descriptor naming any browser by package name and signature hashes. The list becomes aBrowserAllowListon theAppAuthConfiguration. A null or empty list keeps the existing behaviour of allowing any installed browser.The browser matcher is fixed when an
AuthorizationServiceis 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
ActivityNotFoundExceptionhandling thatperformAuthorizationalready had. Without it, an allow list that matches no installed browser would throw instead of returning ano_browser_availableerror.Why
Discussed here as an improvement: #537
Added after review
Added
getInstalledAndroidBrowsers()Adds
FlutterAppAuth.getInstalledAndroidBrowsers(), which lists the browsers installed on an Android device with their package name, version, Custom Tab mode and signature hashes. It wraps AppAuth'sBrowserSelector.getAllBrowsers, so the hashes are exactly what anAndroidBrowser.customentry is matched against.It's a development aid for finding the hashes of a browser to pin: look them up once on a test device, then hard-code them. Hashes read at runtime and passed straight back into
androidAllowedBrowserswould also allow a look-alike app installed under the same package name, and the README and dartdoc say so. It isn't implemented on iOS or macOS.@MaikuB
Behaviour change you should be aware of: the plugin used to create two AuthorizationServices in
onAttachedToEngineand keep them until the engine detached. AppAuth binds to the Custom Tabs service and calls warmup(0) in that constructor, so the browser was pre-warmed from app start. This PR now creates a fresh service for each authorize/token/endSession call and disposes the previous one when the next call starts. That fixes the browser choice going stale: a browser installed, uninstalled or replaced under the same package name was never reselected. It also keeps the number of open services at one.Cost: binding now happens just before each Custom Tab intent is built. AppAuth waits up to 1 s for the connection (
CustomTabManager.CLIENT_WAIT_TIME) on the main thread, so the first Custom Tab of each sign-in or sign-out may open slightly slower, and it is no longer pre-warmed. I haven’t exactly measured it; but it still felt instant on both emulator and real device.Alternative
If you’d rather keep warm-up, an alternative is to keep one service and rebuild it only when the browser settings change, at the cost of the stale-selection problem above (the one CoPilot review found). Happy to switch to that if you prefer.