Skip to content

[flutter_appauth] feat(android): allow restricting which browser handles auth requests - #682

Open
ddfreiling wants to merge 7 commits into
MaikuB:masterfrom
Notalib:feat/android-browser-matcher
Open

ddfreiling wants to merge 7 commits into
MaikuB:masterfrom
Notalib:feat/android-browser-matcher

Conversation

@ddfreiling

@ddfreiling ddfreiling commented Sep 16, 2026 •

Copy link
Copy Markdown

What

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.

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's BrowserSelector.getAllBrowsers, so the hashes are exactly what an AndroidBrowser.custom entry 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 androidAllowedBrowsers would 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.

⚠️ Custom Tabs warm-up change

@MaikuB

Behaviour change you should be aware of: the plugin used to create two AuthorizationServices in onAttachedToEngine and 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.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread flutter_appauth/README.md Resolved
Comment thread flutter_appauth/README.md Outdated
],
```

A custom browser's signature hash can be obtained from AppAuth for Android's `BrowserDescriptor.generateSignatureHash`, given the `PackageInfo` of the installed browser APK.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@ddfreiling ddfreiling Oct 22, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ddfreiling and others added 4 commits October 22, 2026 21:53
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Service caching can bypass signature restrictions after browser replacement and retain an unbounded number of browser service connections.

2 open issues
3 resolved since last review

Review effort: Balanced

ddfreiling and others added 2 commits October 23, 2026 20:55
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants