Skip to content

Fix WSJ Custom Tab redirect loop#8520

Draft
kzar wants to merge 6 commits into
developfrom
kzar/bugfix/cct-app-loop
Draft

Fix WSJ Custom Tab redirect loop#8520
kzar wants to merge 6 commits into
developfrom
kzar/bugfix/cct-app-loop

Conversation

@kzar
Copy link
Copy Markdown
Collaborator

@kzar kzar commented May 11, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/inbox/1200776578788342/item/1211781703927439/story/1214682781164811?focus=true

Description

If the user sets the DuckDuckGo browser as the default, and then taps to open an article link inside the WSJ app, they can get trapped in an endless redirect loop, where the WSJ app opens the DDG browser, which in turn opens the WSJ app again, etc.

Let's fix that by mirroring Chromium's behaviour[1]:

  1. Block HTTP App Link hand-off unless the user initiated the navigation (e.g. by clicking on a link, or entering a URL in the URL bar). This is what prevents the WSJ redirection loop, since the navigations aren't user initiated, and so the article is loaded inside the DDG Custom Tab instead.
  2. Add a "trusted caller" carve-out, to avoid breaking some OAuth flows. If an app opens a Custom Tab, App Links that point back to the same app should be allowed without user interaction.

Notes:

  • The "trusted caller" carve-out doesn't apply for the WSJ app, since it does not take care to propagate its session token into its launch intent (skips CustomTabsIntent.Builder(session)). That is the only thing preventing the redirect loop also happening in Chromium.
  • The isUserQuery() check allows navigations that were initiated by the user via the address bar to then be opened in an app.
  • Non-HTTP App Links are handled via handleNonHttpAppLink which isn't changed here. Chromium seems to force a user prompt for non-user-initiated non-HTTP navigations, we could consider doing the same in the future.

Steps to test this PR

  1. DDG as default browser, Settings → Permissions → "Open Links in Apps" = "Always"

Check that the WSJ redirection loop is fixed

  1. Install the WSJ Android app, and sign in to a paid account.
  2. In the WSJ app, open articles until you find one that opens in the custom tab (with DDG browser).
  3. Ensure you can then read the article OK, instead of being redirected in an infinite loop.

Check that OAuth flows still work

  1. Install this Android OAuth demo app.
  2. Launch the app, and ensure "Require user gesture for auth" is disabled, and "Use browser" is set to "DuckDuckGo (Custom tab)"
  3. Press "START AUTHORIZATION".
  4. Ensure you are taken back to the "Authorization granted - REFRESH TOKEN / SIGN OUT" screen of the demo app (not left on the "Authorisation Successful!" webpage inside the Custom Tab).

1 - https://source.chromium.org/chromium/chromium/src/+/09aafe21bc1b6c78e92fa4197d4e3c8401caaee9:components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java;l=1230-1235

The Wall Street Journal (WSJ) app sometimes loads articles inside a Chrome
Custom Tab (CCT)[1]. When it does, if the user's default browser was set to
DuckDuckGo, they could sometimes be trapped in a CCT -> App Link redirection
loop, as their phone switched from the DuckDuckGo browser, back to the WSJ, and
back again repeatedly.

It turns out, that Chromium has some logic[2] to ensure that such navigations
are user-initiated before opening the registered app for it. Let's do the same.

Notes:
 - The isUserQuery() check allows navigations that were initiated by the user
   via the address bar to then be opened in an app.
 - Non-HTTP App Links are handled via `handleNonHttpAppLink` which isn't changed
   here. Chromium seems to force a user prompt for non-user-initiated non-HTTP
   navigations, we could consider doing the same in the future.

1 - https://developer.chrome.com/docs/android/custom-tabs
2 - https://source.chromium.org/chromium/chromium/src/+/09aafe21bc1b6c78e92fa4197d4e3c8401caaee9:components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java;l=1230-1235
@kzar kzar force-pushed the kzar/bugfix/cct-app-loop branch from 839a1c7 to fa6117e Compare May 11, 2026 18:38
@kzar kzar requested review from anikiki and removed request for CrisBarreiro May 13, 2026 15:59
@kzar kzar assigned anikiki and unassigned CrisBarreiro May 13, 2026
@kzar kzar changed the title Avoid launching apps for HTTP navigations not initiated by the user Fix WSJ <-> Custom Tab redirect loop May 13, 2026
@kzar kzar changed the title Fix WSJ <-> Custom Tab redirect loop Fix WSJ Custom Tab redirect loop May 13, 2026
@kzar kzar force-pushed the kzar/bugfix/cct-app-loop branch from 090ad55 to e48c5a2 Compare May 13, 2026 16:27
The previous commit blocked HTTP App Link hand-off when no user gesture was
associated with the navigation. That avoids the WSJ <-> Custom Tab redirect loop
problem, but it also blocks some legitimate OAuth-style flows, where the opened
page does not require user interaction (e.g. where the user was already signed
in).

Chromium handles this with a "trusted-caller" carve-out[1]. If an app opens a
Custom Tab, and then that Custom Tab opens an App Link that points back to that
same app, let it through even without user interaction.

Let's add similar logic here:

 - DuckDuckGoCustomTabService.newSession() records (token -> client package) in
   a new CustomTabsSessionRegistry. cleanUpSession() clears it.
 - IntentDispatcherViewModel looks up the verified package when handling a CCT
   intent, and passes it down through CustomTabActivity, BrowserTabFragment,
   and into BrowserTabViewModel.setIsCustomTab(...).
 - The gate in BrowserTabViewModel.handleAppLink gains `|| isTrustedCaller`,
   where isTrustedCaller is true only in CCT mode and only when the registered
   client package equals the App Link's target package.

1 - https://source.chromium.org/chromium/chromium/src/+/09aafe21bc1b6c78e92fa4197d4e3c8401caaee9:components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java;l=1203-1208
@kzar kzar force-pushed the kzar/bugfix/cct-app-loop branch from e48c5a2 to b7895b6 Compare May 13, 2026 16:37
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