Files
TREK/client/src/utils/signedOut.ts
T
Maurice 4854377603 fix(oidc): stop the login page signing a user back in against their will
Two reports, one line: the automatic bounce to the provider in useLogin,
which fires whenever password login is off. Both reporters are on an
OIDC-only install, which is what arms it.

#2126, the redirect loop. The re-entry guard sat inside the oidc_code
branch, but the exchange strips oidc_code from the URL before it
navigates away. Any re-run of the effect during that window - and t is a
dependency, so a language change is enough, which every non-English
install does on the login page - read an empty search, missed the guard,
fell through to the config probe and bounced to the provider. The
provider answered silently, landed back on /login?oidc_code=, and round
it went. Each pass mints a valid session, which is why closing the tab
and reopening lands the reporter logged in. The guard is now the first
statement of the effect, so no re-run can reach the redirect.

#2123, logout that logs you back in. The brake for the bounce was
noRedirect in router location state, and it does not survive the trip:
logout flips isAuthenticated on a sync lane while the router defers its
location update, so ProtectedRoute renders once on the old route and
emits <Navigate to=/login replace> with no state, overwriting the entry
the logout handler had just pushed. Location state cannot cross a full
document load either, and api/client.ts performs one on any 401. The
fact now lives in sessionStorage, set in authStore.logout() so all seven
sign-out call sites are covered, and cleared on any successful auth and
on the manual SSO links. Per tab, so a fresh tab still single-signs-on -
that being the point of single sign-on.

Both tests were confirmed red without their fix and green with it. The
first needed rewriting to do that: the obvious version held the page
mounted past the window the race lives in and passed either way.

Not included: RP-initiated logout at the provider. TREK clears its own
session correctly, and ending the provider's is a separate decision -
it needs an end_session_endpoint the provider may not advertise, a
post_logout_redirect_uri registered on their side, and an answer to
whether signing out of TREK should sign you out everywhere.
2026-08-29 01:30:26 +02:00

59 lines
2.2 KiB
TypeScript

/**
* "This tab just signed out" — a fact that has to survive more than a render.
*
* On an OIDC-only install the login page bounces to the provider on its own
* (`useLogin`), which is what makes signing out impossible: the provider still
* has a live session, answers the silent authorize immediately, and the user is
* back in TREK without having asked to be (#2123).
*
* The brake for that bounce used to be `noRedirect` in react-router location
* state, and it does not survive the trip. `authStore.logout()` flips
* `isAuthenticated` on a synchronous lane while BrowserRouter defers its own
* location update, so React commits one render still on the old route;
* ProtectedRoute sees a logged-out user and emits `<Navigate to="/login"
* replace />` with no state, which overwrites the history entry the logout
* handler had just pushed. Location state also cannot cross a full document
* load, and `api/client.ts` performs one on any 401.
*
* sessionStorage is the right lifetime for it: it outlives both, and it is gone
* when the tab is. A brand-new tab should auto-SSO again — that is the point of
* single sign-on. What must not happen is auto-SSO in the tab where somebody
* just pressed "Sign out".
*
* Every accessor is wrapped: Safari in private mode throws on sessionStorage,
* and losing the brake must degrade to today's behaviour rather than to a blank
* page.
*/
const KEY = 'trek_signed_out'
/** Called from `authStore.logout()`, so all seven sign-out call sites are covered at once. */
export function markSignedOut(): void {
try {
sessionStorage.setItem(KEY, '1')
} catch {
/* private mode — the location-state brake still covers the common path */
}
}
/**
* Cleared the moment the user asks to come back: any successful authentication,
* and the click on the manual "Sign in with SSO" link. Without that, an
* OIDC-only user would be stranded on a login page that refuses to auto-SSO for
* the life of the tab.
*/
export function clearSignedOut(): void {
try {
sessionStorage.removeItem(KEY)
} catch {
/* ignore */
}
}
export function wasSignedOut(): boolean {
try {
return sessionStorage.getItem(KEY) === '1'
} catch {
return false
}
}