[C11] Session Re-authentication

Summary

When an operator’s sign-in lapses, the next backend call fails with an authentication error. The portal re-authenticates without being asked and returns the operator to the screen they were on, with its filters, page and tab. A small notice, Re-authenticating…, covers the round trip, so the browser’s redirects do not read as a crash.

This is a shell concern, like C01. No screen handles it, and no screen may opt out.

What exists today. The SPA already reacts to a 401, or to an X-Token-Expired: true response header, by sending the browser to the gateway’s sign-in entry point. It does not record where the operator was, so a successful sign-in lands on the organisation home. It also shows nothing while the redirect runs, and several failing calls on one screen each start a redirect of their own. This page specifies the replacement.

Actor & Context

Actor: any signed-in operator.
Frequency: whenever a session lapses. The gateway session times out after 30 idle minutes. The backend token is refreshed silently while the identity provider’s refresh token is valid, and lapses when it is not.
Precondition: the operator was signed in, and the browser still holds the identity provider’s single sign-on session. When that session has lapsed too, the identity provider shows its own sign-in prompt mid-flow, and the flow below still completes.
Entry point: none. Any backend call that fails authentication starts it.

Triggers

A backend call starts re-authentication when its response is either of these:

  • 401 Unauthorized.

  • Any error status that carries X-Token-Expired: true. admin-service emits this header when it receives an expired backend token that the gateway could not refresh.

Three cases do not start it:

  • 403 Forbidden. The operator is signed in but lacks the capability. The screen’s own not-authorised state handles it.

  • The session probe GET /api/session/current. A 401 there means "not signed in" and renders the public landing page (C03). Treating it as expiry would loop.

  • A call made while re-authentication is already under way. See Single flight.

The gateway must answer an unauthenticated API call with 401, not with a redirect. A 302 to the identity provider is followed silently by the browser’s request machinery. That redirect then fails as a cross-origin error with status 0, which the SPA cannot tell apart from a network fault. For /api/ and /services/, the gateway’s authentication entry point returns 401. Browser navigations to the sign-in entry point still redirect as they do now.

Main Flow

  1. A backend call fails with one of the [triggers].

  2. The shell records the return location: the router URL of the screen in view, including its path, query string and fragment.

  3. The shell shows the Re-authenticating… notice. See The notice.

  4. After a short pause (about 400 ms), so that the notice is legible, the shell sends the browser to the gateway’s sign-in entry point.

  5. The gateway and the identity provider complete the sign-in. With a live single sign-on session this happens without any prompt, in about one to two seconds.

  6. The SPA reloads. Before it renders a routed screen, the shell reads the return location and finds one waiting.

  7. The shell shows the notice again, with the same text, while the session probe resolves.

  8. Once the session probe succeeds, the shell navigates to the return location, clears it, and dismisses the notice.

  9. The screen loads its data afresh. Filters, sort, page and tab come back because they live in the URL (filterable list).

Single flight

A screen often has several calls in flight, and they all fail together. Only the first failure starts re-authentication. Failures that arrive while the notice is showing are absorbed: they show no error state or toast of their own, and they record no return location of their own.

The notice

  • A small, centred, modal card over a dimmed content area, in the style of the portal’s other modal surfaces. It carries a spinner and the single line Re-authenticating….

  • It has no close button and no action. The operator cannot act on the screen behind it, because that screen’s data is about to be replaced.

  • It is announced to assistive technology once, as a polite live-region status message.

  • It stays in place until the redirect leaves the page, and again after return until the restored screen has rendered. The operator sees one continuous notice, apart from the browser’s own page transition.

Alternative Flows

  • AF-1 — Sign-in does not complete. The return location carries the time it was recorded. If the SPA starts with a return location older than two minutes, or a second expiry occurs within 30 seconds of a return, the shell abandons the attempt. It clears the return location and shows the public landing page with the line Your session could not be renewed. Sign in to continue. This stops a redirect loop when the identity provider keeps refusing. If the screen refuses to be left, for example because it holds unsaved scans, the notice stays up instead and shows the same line with a Sign in button. The operator sees the screen, dimmed, and chooses when to leave it.

  • AF-2 — The operator cancels at the identity provider’s prompt. The identity provider returns an error to the gateway, and the gateway shows its error page. On the next visit, AF-1’s age check discards the stale return location.

  • AF-3 — Expiry during a command. A mutating call (POST, PUT, PATCH, DELETE) is not replayed after sign-in, because replaying it could apply a change twice. The operator returns to the screen. If the screen held unsaved input, for example a half-built return in T03, that input is lost unless the screen persists its own draft. Screens that keep long-lived drafts must persist them locally.

  • AF-4 — Different user after sign-in. The identity provider may sign in a different account. The return location is honoured only when the new session belongs to the same user who recorded it. Otherwise the shell discards it and opens the organisation home.

  • AF-6 — Signed-out visitor opens a deep link. A bookmark or a shared link such as /o/4/numbers/list?state.in=F is opened with no session. The session probe answers 401, which is not a trigger, so the public landing page shows (C03). As it turns the visitor away, the shell records the requested URL as the return location, in the same form and under the same rules as the main flow. When the visitor signs in from the landing page, they land on the requested screen, not the organisation home. The notice on return reads Signing in…, because the visitor was never signed in, and this first sign-in does not arm the loop guard. The record carries no user, because none is known yet, so the same-user check does not apply. The two-minute age limit and the path validation do.

  • AF-5 — Organisation no longer accessible. The restored URL names an organisation the session cannot reach. C01’s ordinary not-available handling applies. Re-authentication does nothing special.

Acceptance Criteria

  • A 401, or an error carrying X-Token-Expired: true, from any backend call except the session probe starts re-authentication. A 403 does not.

  • Several concurrent failures start exactly one redirect. None of them shows its own error state.

  • The Re-authenticating… notice appears before the redirect and is shown again on return until the restored screen renders. It is modal, has no actions, and is announced once as a status message.

  • After a successful silent sign-in, the operator is on the same route, with the same query string, that they left.

  • The return location is accepted only as a same-origin path that begins with a single /. Any other value is discarded, so it cannot be used as an open redirect.

  • A return location older than two minutes, or a second expiry within 30 seconds of a return, ends at the public landing page with an explanation. It never loops.

  • A return location recorded by one user is never applied to a different user’s session.

  • Mutating calls are never replayed.

  • A signed-out visitor who opens a deep link lands on that screen after sign-in, not on the organisation home.

  • With the gateway session removed, an API call receives 401 from the gateway, not a redirect.

API Surface

Call Role

Any /api/ or /services/ call

Its 401, or its X-Token-Expired: true header, is the trigger.

GET /api/session/current

The session probe. It is excluded as a trigger, and on return it confirms the new session and the user’s identity.

GET /oauth2/authorization/entra

The gateway’s sign-in entry point. It is a full browser navigation, not an API call.

Design Notes

  • Where the return location lives. It is kept in the browser’s sessionStorage, so it is per tab and does not outlive the tab. It holds the path, the time it was recorded and the user’s identity. It is not passed through the gateway, because then the gateway would have to validate a redirect target. The SPA validates it against the same-origin rule above, and only ever hands it to the router.

  • Why a pause before the redirect. Without it the notice flashes for a frame and the redirect reads as a crash. The pause is a little over the threshold at which a message can be read, and is short beside the round trip itself.

  • Why not silent renewal in a hidden frame. Renewal in a hidden iframe, or a popup, would preserve unsaved input. But the identity provider blocks framing, and third-party cookie restrictions make hidden-frame renewal unreliable. A full redirect with the URL restored covers every screen whose state lives in the URL, which the list pattern already requires.

Out of Scope

  • The signing-out flow.

  • Extending session lifetimes. That is a configuration choice on the gateway and the identity provider.

  • Draft persistence for individual screens. AF-3 sets the expectation, and each screen owns its own draft.

Design Anchors