Skip to content

[various] Allow resuming pending authorization after activity recreation on Android - #669

Open
moritz-j wants to merge 10 commits into
MaikuB:masterfrom
BrickmakersGmbH:fix/android_resume_auth
Open

moritz-j wants to merge 10 commits into
MaikuB:masterfrom
BrickmakersGmbH:fix/android_resume_auth

Conversation

@moritz-j

Copy link
Copy Markdown

We have encountered an issue where the auth response is not handled after returning from the browser back to the app. This happens when the app's Activity is killed by Android (e.g. for clearing up memory). This PR adds a resumePendingAuthorization() method that allows the app to get the "missed" auth response after reinitialising itself.

Steps to reproduce:

  1. Enable "Don't keep activities" in Android developer settings.
  2. Start auth flow.
  3. Complete authentication.

=> Browser closes, but app doesn't receive the auth response.

Behaviour with fix:

  1. Browser auth flow started
  2. Activity killed in background
  3. Browser flow finishes
  4. Activity is recreated and flutter reinitialized
  5. App calls resumePendingAuthorization() and receives response
  6. User logged in

moritz-j added 4 commits July 17, 2026 11:22
- for auth with and without code exchange
- only when receiving activity result with no pending operation, i.e. when no Result is available to send auth result to flutter
- preserves null intent error because then no data is available to store
takes pending authorization data and returns it to new Result
afaik there is no scenario this can happen on iOS or macOS
@MaikuB
MaikuB requested a lite review from Copilot September 13, 2026 06:04

@MaikuB MaikuB left a comment

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.

Thanks for the PR. Can you update the example app so it shows how this could be used? This includes adding code comments to the example app where appropriate

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

Two critical Android issues and one moderate test issue remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds resumePendingAuthorization() to recover Android authorization results after Activity recreation, with typed Dart responses and iOS/macOS no-op support.

Changes:

  • Adds the public resume API and response models.
  • Implements Android pending-authorization handling.
  • Adds platform mappings and method-channel tests.
File summaries
File Summary
flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m Adds macOS no-op handling.
flutter_appauth/lib/src/flutter_appauth.dart Exposes the resume API.
flutter_appauth/lib/flutter_appauth.dart Exports resume response types.
flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m Adds iOS no-op handling.
flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h Defines the resume method constant.
flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java Implements pending authorization recovery. Critical (2 votes): preserve the exchange-flow flag before clearing state to report the correct failure code (lines 208 and 735). Critical (1 vote): persist pending authorization and exchange state across Activity/engine recreation (lines 92 and 738).
flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart Tests response mapping. Moderate (2 votes): correct the epoch/timezone expectation at line 255.
flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart Adds response mapping helpers.
flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart Invokes the channel and maps responses. Nit (1 vote): preserve the original stack trace when rethrowing platform exceptions.
flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart Adds the platform interface method.
flutter_appauth_platform_interface/lib/src/authorization_resume_response.dart Defines typed resume responses.
flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart Exports the new response model.
Review details

Suppressed comments (3)

flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java:739

  • On a recreated plugin, the request-scoped clientSecret and allowInsecureConnections fields have reverted to their defaults because they are only populated by the original method call. This reuses processAuthorizationData, which reads those fields during the code exchange, so resumed authorizeAndExchangeCode flows for confidential clients or non-HTTPS endpoints fail. Persist these options in PendingAuthorization and use them for the resumed exchange (or require them as resume input).
    checkAndSetPendingOperation(RESUME_PENDING_AUTHORIZATION_METHOD, result);
    processAuthorizationData(pendingAuth.response, pendingAuth.exception, pendingAuth.exchangeCode);

flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java:739

  • If the app starts another operation after a response has been stashed but before it calls this method, pendingOperation is non-null here. The code clears the stashed response before checkAndSetPendingOperation throws; the outer catch then completes the unrelated operation via finishWithError, while the resumePendingAuthorization Result is never completed. Check for an in-flight operation and fail the resume call without consuming the pending response.
    final PendingAuthorization pendingAuth = pendingAuthorization;
    pendingAuthorization = null;

    checkAndSetPendingOperation(RESUME_PENDING_AUTHORIZATION_METHOD, result);
    processAuthorizationData(pendingAuth.response, pendingAuth.exception, pendingAuth.exchangeCode);

flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart:113

  • The previous implementation used rethrow for platform exceptions without usable details. Moving this branch into a helper and using throw e resets the Dart stack trace to the helper (the identical branch below has the same problem), changing the existing pass-through behavior and making caller failures harder to diagnose. Preserve the original stack trace when delegating this exception, or keep the pass-through rethrow in the caller.
  Never _mapAndThrowPlatformException(PlatformException e) {
    if (e.details == null) {
      throw e;
    }
  • Files reviewed: 12/12 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@moritz-j
moritz-j requested a review from MaikuB September 14, 2026 08:37
@moritz-j

Copy link
Copy Markdown
Author

Hey @MaikuB, thanks for the review. I have added this feature to the example app and fixed 2/3 Copilot findings (the 3rd being not really useful).

Activity will have been destroyed and recreated in the background.
Always returns null on other platforms, so can be called unconditionally.
*/
_resumePendingAuthorization();

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.

Let me know if I have missed something but since this is about resuming, shouldn't this hook into didChangeAppLifecycleState and hooked into the AppLifecycleState.resumed state?

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.

The "resume" part only really refers to the auth flow itself, not necessarily the app. The flutter app is restarted entirely when the host Activity is recreated. That means all widgets are reinitialized as well. Calling _resumePendingAuthorization() in the app's lifecycle resume would probably not cause any issues, but wouldn't really do anything either. If the app is actually resuming, it's probably the "normal" case that doesn't need this fix.
To summarise, I wouldn't call this in AppLifecycleState.resumed. We have been using this fix for a while now just like this (in initState on the login page) and it seems to have fixed the problems, both during testing and in production.

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.

Thanks I knew the resume part was more to do with auth flow. However, low memory scenarios are supposed to retain the app/activity and then trigger resume lifecycle events. This was the reason behind my comment.

Either way, I'm having trouble trying to ensure the solution works with the example app as the "don't keep activities" kills the Chrome Custom Tab and counts as cancelling the auth attempt. Let me know if you know of a different way to test

@moritz-j moritz-j Oct 19, 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.

@MaikuB I have tested around a bit more. The setup in the example app is of course quite a bit simpler than our app. It seems that, when "Don't keep activities" is active, there is no reliable order between initState() and the app's resume. I noticed a difference if I started the app with the debugger attached vs. just launching it from the emulator at the very least.
That being said, I now agree with also calling resumePendingAuthorization() on resumed events. I'd also keep the call in initState though, since the order seems unreliable and the cost of calling it redundantly is relatively low. I've pushed this change already.

As for the testing, I don't quite understand what you mean by the Chrome tab being killed and the auth flow cancelled. I had no issue testing this on real devices and emulator. I have recorded three test scenarios, maybe that helps:

Commented out _resumePendingAuthorization() (in both initState and resumed state) -> resume is not triggered automatically. When returning from the browser, nothing happens initially. Only when manually pressing the "Resume pending authorization" button do the tokens appear.
manual_resume.webm

With the automatic resume (so just the PR branch without local changes) -> tokens appear after short delay.
auto_resume.webm

Current main branch -> nothing happens after returning from the browser.
main_branch.webm

Let me know if I can help any further.

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

Resuming during another operation can discard the saved authorization, fail the wrong call, and leave the resume Future unresolved.

1 open finding
3 resolved since last review

🧠 Review effort: Balanced

resume should probably have lower priority than "regular" auth calls. this ensures that resume results in an error, but the actual authentication is not affected when resume is called during it.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@moritz-j
moritz-j requested a review from MaikuB October 16, 2026 12:17
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