Skip to content

fix: clear the web sign-in tokens before the app loads; auto-connect only to a saved server - #596

Merged
91jaeminjo merged 5 commits into
mainfrom
fix/web-callback-token-clear
Oct 2, 2026
Merged

91jaeminjo merged 5 commits into
mainfrom
fix/web-callback-token-clear

Conversation

@91jaeminjo

Copy link
Copy Markdown
Collaborator

Summary

After a web sign-in, the callback's tokens leave the address bar as the page
starts parsing, before the app downloads, instead of once Flutter has booted.
The page-load clear stays main's: every query is dropped, so no outside link
can hand the app one. A callback that can't be decoded shows an error instead
of stopping the app, and an auto-connect address connects only to a saved
server.

What changed

  • Early clear (2163bd0f). An inline script after <title> in
    web/index.html moves the #/auth/callback?… query into
    window.soliplexCallbackQuery and rewrites the URL. captureNow reads that
    stash. When the script is missing, it reads the URL and logs one error with
    no values, so a fork without the script still signs in.
    • clearCallbackUrl keeps main's behaviour: it drops location.search
      and every hash query and keeps the route.
    • It and the script build the URL as origin + pathname + route, so a
      pathname such as //evil.example/ can't make replaceState
      cross-origin.
    • The URL logic is urlWithoutQueries, a pure function with tests.
    • Only fragment-form backends are supported (v0.82.2, v0.83.2,
      v0.84+).
  • Backend settings docs (4279dda3). docs/developer-setup.md lists what
    the backend's OIDC config and the IdP client must allow:
    • openid in scope;
    • the frontend origin policy and its consent page;
    • accepted_azp_list for native sign-in;
    • Web Origins for web token refresh.
  • Malformed callback (d044822e). A query with bad percent-encoding or
    UTF-8 (%FF) used to throw at boot. It now yields WebCallbackMalformed,
    and the callback screen shows "The sign-in response could not be read". The
    warning carries the failure's type, not the query.
  • Known-server gate (0aea2202). The home route's ?url= can come from
    outside the app: iOS deep linking is on by default and the app registers
    ai.soliplex.client, and on web a same-tab hash change doesn't reload.
    • Only a saved server is filled in and connected, at the address it was
      added with.
    • Any other address is ignored, with a warning that carries no value.
  • Fork support and docs (140bc0fa).
    • CallbackParams is exported, and the fork example captures, clears and
      passes it.
    • The docs say where scope and accepted_azp_list live, which http://
      origins are refused, and that an iOS deep link reaches a declared path.

Library consumers: CallbackParams is exported. Forks with their own
web/index.html need the script in docs/authoring-a-flavor.md; without it,
sign-in still works and the app logs an error.

Behaviour worth knowing

  • A reload drops every query, as on main: the lobby's ?server=,
    diagnostics' ?run= and the quiz's ?from=. Reload-stable parameters are a
    follow-up.
  • The browser still records the callback URL, tokens included, in its
    history.
    Placing the script after <title> makes Chrome's history list
    show the title rather than the tokens. Only a backend change keeps the
    tokens out of the URL.
  • Web sign-in against a backend that returns the tokens in the query string
    (before v0.82.2 / v0.83.2) no longer works.
  • A returnTo keeps main's handling. Binding it to the server it was
    issued for is the next PR, with the rest of the route-input hardening.

Tests

  • New:
    • urlWithoutQueries: search, hash queries, the callback route, and a
      //host pathname.
    • The malformed callback, on the stash path and the URL path. Its warning
      carries no part of the query.
    • The gate: a saved server connects at its stored URL. An unknown address, including
      @ userinfo and scheme-less forms that read as the saved host, is
      neither filled in nor connected, and its warning carries no part of it. An ignored address
      with an empty list falls through to the default URL.
  • Adapted: main's auto-connect test now registers its server first.
  • Removed: "autoConnectUrl takes precedence over defaultBackendUrl". It can't
    fail under the gate: auto-connect needs a saved server, and the default URL
    only fills an empty list.

Test Plan

  • The full suite passes (2832), with only the existing
    thread_view_state_test disposed-signal trace.
  • flutter analyze reports no issues, dart format is clean,
    markdownlint is clean, and flutter build web succeeds.
  • Every commit analyzes cleanly and passes test/modules/auth.
  • Manual web pass (after the follow-up hardening PR merges):
    1. A normal sign-in leaves the address bar clean, with no error.
    2. With the script removed, Diagnostics → Logs shows one error with no
      values.
    3. Going Back from the IdP during a re-sign-in shows a plain home screen.
    4. Reloading #/?url=… doesn't connect.
    5. Reloading #/lobby?server=x drops the query.
    6. http://localhost:9001//evil.example/#/auth/callback?x=1 launches.
    7. A callback containing %FF shows its error message.

🤖 Generated with Claude Code

91jaeminjo and others added 5 commits October 1, 2026 22:09
An inline script at the top of `web/index.html` moves the
`#/auth/callback?…` query into `window.soliplexCallbackQuery` and
rewrites the URL before any other resource loads. `captureNow` reads
that stash, and falls back to the URL with a value-free error log when
the script is missing, so a fork without it still signs in.

The script follows `<title>`, so the history entry Chrome records for
the callback URL shows the title rather than the tokens; the entry still
holds them, and only a backend change keeps them out of the URL.

`clearCallbackUrl` keeps main's page-load clear: it drops
`location.search` and every hash query and keeps the route, so no
outside link can hand the app a query. It and the script build the URL
as origin + pathname + route, so a pathname such as `//evil.example/`
can't make `replaceState` cross-origin. Query-string callbacks and the
`access_token` key are no longer read; only fragment-form backends are
supported. `docs/authoring-a-flavor.md` documents the script for forks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`docs/developer-setup.md` lists what the backend's OIDC config and the
identity provider's client must allow: `openid` in the scope, the
frontend origin policy and its consent page, `accepted_azp_list` for
native sign-in, and the client's Web Origins for web token refresh.

The web entry page note says the history list, not the entry, shows the
page title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A callback query with malformed percent-encoding or UTF-8 (`%FF`) threw
from `Uri.splitQueryString` at boot and stopped the app from starting.
It now yields `WebCallbackMalformed`, which the callback screen shows as
"The sign-in response could not be read", and logs a warning that
carries the failure's type, not the query.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The home route's `?url=` can come from outside the app: iOS deep linking
is on by default and the app registers `ai.soliplex.client`, and on web
a same-tab hash change reaches the route without a reload. Only a saved
server is filled in and connected, at the address it was added with;
any other address is ignored, with a warning that carries no value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`CallbackParams`, the type of `standardFlavor`'s `callbackParams`, is
exported, so the fork example in `docs/authoring-a-flavor.md` can
capture, clear and pass the callback. The docs say where `scope` and
`accepted_azp_list` live in the backend's OIDC config, which `http://`
origins the backend refuses, and that iOS deep links reach a declared
path; comments name every case they claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@91jaeminjo
91jaeminjo merged commit 795b4e0 into main Oct 2, 2026
6 checks passed
@91jaeminjo
91jaeminjo deleted the fix/web-callback-token-clear branch October 2, 2026 03:21
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