Skip to content

fix: route the OIDC callback deep link instead of 404ing on it - #51

Open
alexandre-vl wants to merge 1 commit into
Termix-SSH:mainfrom
alexandre-vl:fix/oidc-callback-deep-link-route
Open

fix: route the OIDC callback deep link instead of 404ing on it#51
alexandre-vl wants to merge 1 commit into
Termix-SSH:mainfrom
alexandre-vl:fix/oidc-callback-deep-link-route

Conversation

@alexandre-vl

@alexandre-vl alexandre-vl commented Aug 18, 2026

Copy link
Copy Markdown

Overview

termix-mobile://oidc-callback has no route. When Android delivers it as a plain intent rather than through WebBrowser.openAuthSessionAsync, expo-router resolves the path /oidc-callback, finds nothing and renders Unmatched Route — Page could not be found, at the end of a sign-in the server already logged as successful.

  • Added: app/oidc-callback.tsx route and app/utils/oidc-callback.ts hand-off
  • Fixed: OS-delivered OIDC deep link ending on the Unmatched Route screen
  • Fixed: termix-mobile:// redirect escaping the embedded WebView to the OS

Changes Made

Three changes, one per way the link can arrive:

  • app/oidc-callback.tsx — gives the path a real route, so an OS-delivered link lands in the app instead of the 404 screen, then redirects on to the tabs. The URL is rebuilt from useLocalSearchParams rather than read back from Linking, so nothing depends on Hermes parsing a custom scheme. It is parked during render, not in an effect: <Redirect> navigates from its own effect and child effects flush before the parent's.

  • AuthFlow.tsx, WebViewonShouldStartLoadWithRequest now intercepts termix-mobile:// navigations. A server that was handed an appCallbackUrl answers /users/oidc/callback with that redirect rather than the postMessage handoff the embedded login expects, and Android would otherwise fire it at the OS as an intent.

  • AuthFlow.tsx, OidcStep init — drains a link parked by the route before AsyncStorage.removeItem("jwt"), so a callback that arrived before the sign-in screen existed completes rather than being discarded and sending the user back to the IdP.

app/utils/oidc-callback.ts holds the parked link and the already-handled check. Both paths can see the same intent, so it is claimed once — a duplicate confirmation that failed would clear the JWT the first one had just stored. The callback URL carries a bearer token, so nothing is retained: the parked value is cleared when consumed, and the handled check compares fingerprints rather than URLs, over a bounded list.

Related Issues

Screenshots / Demos

Verified statically only — I have no Android device or emulator to exercise the flow on, so the runtime behaviour of onShouldStartLoadWithRequest against a custom-scheme navigation has not been confirmed on a build.

tsc --noEmit   5 errors before, 5 after (pre-existing: WarpgateDialog ×3, TunnelManager ×2)
prettier       All matched files use Prettier code style
expo lint      0 errors, no warnings on the touched files

Checklist

  • Code follows project style guidelines
  • Supports mobile and desktop UI/app (if applicable)
  • I have read Contributing.md

`termix-mobile://oidc-callback` is normally captured by
WebBrowser.openAuthSessionAsync and never reaches the router. When Android
delivers it as a plain intent instead — a Custom Tab handing the redirect to the
OS, or a cold start — expo-router resolves it as the path "/oidc-callback",
finds no matching file and renders Unmatched Route, at the end of a sign-in the
server considered successful.

Three changes, one per way the link can arrive:

- app/oidc-callback.tsx gives the path a route, so an OS-delivered link lands in
  the app rather than on the 404 screen, and redirects on to the tabs.
- The WebView now intercepts termix-mobile:// navigations through
  onShouldStartLoadWithRequest. A server that was handed an appCallbackUrl
  answers the OIDC callback with that redirect rather than the postMessage
  handoff the embedded login expects, and Android would otherwise fire it at the
  OS as an intent.
- OidcStep drains a link parked by the route before wiping the stored jwt, so a
  callback that arrived before the sign-in screen existed completes instead of
  being discarded and sending the user back to the IdP.

The parked link and the already-handled check live in app/utils/oidc-callback.ts.
Both paths can see the same intent, so it is claimed once — a duplicate
confirmation that failed would clear the JWT the first one had just stored. The
callback URL carries a bearer token, so nothing is retained: the parked value is
cleared when consumed and the handled check compares fingerprints.
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.

[BUG] Android: OIDC deep link falls through to expo-router "Unmatched Route" on re-sign-in

1 participant