镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(auth,ios): keep sign-in redirect URLs out of Flutter deep linking - #18841

Draft
Lyokone wants to merge 2 commits into
mainfrom
fix/auth-ios-redirect-url-deep-link
Draft

Lyokone wants to merge 2 commits into
mainfrom
fix/auth-ios-redirect-url-deep-link

Conversation

@Lyokone

@Lyokone Lyokone commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Description

On iOS, the reCAPTCHA, multi-factor and OAuth redirects (app-<GOOGLE_APP_ID>://firebaseauth/link?deep_link_id=…) reach Flutter's deep linking, which is on by default since Flutter 3.27. With go_router, the app shows a "no routes for location /link?deep_link_id=…" page, although sign-in still succeeds.

Cause. FirebaseApp.configure() creates Auth eagerly, and Auth registers itself as a GoogleUtilities app delegate and scene delegate interceptor. When the redirect comes back, the interceptor runs first and consumes the URL: AuthURLPresenter.canHandle finishes the presentation and clears its callback matcher. Flutter then asks the plugin, whose Auth.auth().canHandle(url) now returns false, so Flutter pushes the URL as a deep link. This happens on both paths:

  • app delegate apps: FlutterAppDelegate application:openURL:options: → plugin returns false → sendDeepLinkToFramework;
  • UIScene apps: FlutterSceneDelegate scene:openURLContexts: → plugin returns false → the app delegate fallback skips the plugin (it conforms to FlutterSceneLifeCycleDelegate) → deep link.

Fix. The plugin's application(_:open:options:) and scene(_:openURLContexts:) also return true for any Auth callback URL: host firebaseauth, path /link, and the scheme the SDK builds redirects with (app- + GOOGLE_APP_ID with : replaced by -, or the reversed CLIENT_ID) of any configured app. This also works in apps that only configure named apps (Firebase.initializeApp(name:) + FirebaseAuth.instanceFor(app:)): the FirebaseApp.app() != nil check now only guards Auth.auth().canHandle, not the callback URL match. Other URLs are unaffected.

Not changed: the audit also said scene(_:openURLContexts:) isn't @objc, so Flutter never calls it. That doesn't reproduce. Swift infers @objc from the FlutterSceneLifeCycleDelegate conformance: the compiled plugin has the scene:openURLContexts: ObjC thunk, and the scene tests below only pass because Flutter calls that method.

Known limits:

  • scene(_:openURLContexts:) claims the whole set when any URL in it matches, so a non-Auth URL delivered in the same set gets no deep link. The previous canHandle code had the same shape.
  • The match is looser than the SDK's exact one (it ignores case, a trailing slash and the port), but only for Auth's own schemes.
  • Open PR fix(auth, iOS): guard URL handlers when no FirebaseApp is configured #18641 edits the same lines. Its FirebaseApp.app() guard is already on main, so main supersedes it.

Tests

iOS e2e in the auth example (integration_test/auth_callback_url_e2e_test.dart, run by e2e_test.dart in e2e-auth). A test-only channel (example/ios/Runner/AuthCallbackURLTestChannel.m, like RemoteNotificationTestChannel.swift in messaging) opens a URL the way iOS does, through UIApplication.delegate's application:openURL:options: or the scene delegate's scene:openURLContexts:. The Dart side records what Flutter deep linking pushes.

  • Each scene test then opens a non-Auth control URL, waits for it, and waits another second before checking it is the only URL pushed. The engine doesn't promise to deliver deep links in order, so the extra wait keeps a late Auth URL from passing unnoticed.
  • The app delegate tests rely on openURL returning true. The example uses UIScene, so FlutterAppDelegate has no root view controller and never pushes what it opens; the deep link check there cannot fail.
  • Both schemes are covered. The URL is built from the [DEFAULT] app's options; the reversed client ID tests are skipped, with a reason, when the options have no iOS client ID.
  • For the named-app case, the test creates a named app with its own client ID, and the channel makes FirebaseApp.app() return nil while it opens that app's callback URL. It does not delete [DEFAULT], which would replace its native Auth and drop the emulator settings for the rest of the suite.
Test (iOS 27 simulator) main first commit this PR
app delegate, app- scheme: openURL returns true fails: false passes passes
scene, app- scheme: not pushed fails: pushed passes passes
app delegate, reversed client ID: openURL returns true fails: false passes passes
scene, reversed client ID: not pushed fails: pushed passes passes
app delegate, named app only: openURL returns true fails: false fails: false passes
scene, named app only: not pushed fails: pushed fails: pushed passes

The reversed client ID rows used a local client ID; with the example's placeholder options those tests are skipped. I ran the full e2e_test.dart locally against the Auth emulator with this change: the six new tests pass. The failures are the validatePassword() tests, which need the live API, and updatePhotoURL can set the photoURL to null, which failed the same way on the first commit and is in code this PR doesn't touch. I built with Swift Package Manager; the CocoaPods build CI uses isn't verified locally.

Related Issues

Related to #17035 (closed; its workaround was FlutterDeepLinkingEnabled=false).

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • My PR includes unit or integration tests for all changed/updated/fixed behaviors (See Contributor Guide).
  • All existing and new tests are passing.
  • I updated/added relevant documentation (doc comments with ///).
  • The analyzer (melos run analyze) does not report any problems on my PR.
  • I read and followed the Flutter Style Guide.
  • I signed the CLA.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

  • No, this is not a breaking change.

Auth's own app and scene delegate interceptors consume reCAPTCHA, MFA and
OAuth redirect URLs before Flutter asks the plugin, so Auth.canHandle then
returned false and Flutter pushed the URL as a deep link. The plugin now
claims any Auth callback URL (app-<GOOGLE_APP_ID> or reversed client ID
scheme, host firebaseauth, path /link).
@gemini-code-assist

Copy link
Copy Markdown
Contributor
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

The URL handlers returned false before matching when no [DEFAULT] app was
configured, so iOS apps that only use Firebase.initializeApp(name:) with
FirebaseAuth.instanceFor(app:) still had reCAPTCHA, MFA and OAuth redirects
pushed as Flutter deep links. The FirebaseApp.app() check now only guards
Auth.auth().canHandle, and the callback URL match runs regardless.

The e2e tests now also cover the reversed client ID scheme (skipped when
the app has no iOS client ID) and the named-app case, and wait past the
control deep link instead of relying on deep links arriving in order.

This branch has not been deployed

No deployments
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.

1 participant