AUTH4D-140 Silent re-authorization (prompt=none) and browser session restore
Browser SDK tokens live in memory, so a page reload loses them. A returning player can now be
signed back in without any UI: the SDK sends the page to /oauth/authorize with prompt=none, and
the API answers with a code when the player’s hosted-login session and consent are still valid.
It is a top-level redirect, not a hidden iframe, so it works when third-party cookies are blocked.
This follows OIDC Core 3.1.2.6.
Discovery
GET /t/{tenantId}/.well-known/openid-configuration adds:
{ "prompt_values_supported": ["none"] }
OidcDiscoveryDocumentSchema accepts the field as optional, so earlier documents stay valid.
Authorization request
GET /oauth/authorize accepts one more optional parameter:
| Parameter | Values | Effect |
|---|---|---|
prompt |
none |
Answer at once with a code or an error. Never show a page. |
Every other check runs first and is unchanged: tenant, client, exact redirect_uri match, state
(16–512 characters), S256 PKCE, scopes and the browser origin. So an unregistered redirect URI or a
missing or invalid state still returns 400 (or the hosted error page for a browser navigation)
and is never redirected to. Native applications still get unauthorized_client.
A repeated prompt returns 400 invalid_request. Any other value, including login, consent
and lists such as none login, is redirected to the registered redirect URI as
error=invalid_request with the state.
Outcomes for prompt=none
The API reads the hosted-login session cookie (__Host-auth4_session) on the auth origin. Because
the request is a top-level navigation, the cookie is first-party and SameSite=Lax sends it.
| Condition | Redirect to redirect_uri |
|---|---|
| No cookie, or the session is unknown, expired, revoked or belongs to another project | error=login_required&state=… |
| The player is disabled | error=login_required&state=… |
| The player is an un-upgraded guest (only a guest identity) | error=login_required&state=… |
| Consent for this application is missing, revoked or lacks a requested scope | error=consent_required&state=… |
| Session state is unavailable | error=temporarily_unavailable&state=… |
| Otherwise | code=ac_…&state=… |
interaction_required is reserved: the API does not return it today, and clients should treat it
like login_required. Error redirects carry only error and state (no error_description) and
set no cookie. No hosted-login flow is started, and no consent is granted or changed.
A successful response is a normal one-time authorization code, bound to the request’s
redirect_uri, code_challenge, scopes and nonce. The ID token’s auth_time is the original
sign-in time. As with the interactive flow, the response also sets a fresh __Host-auth4_session
handle, bound to the session the code creates when it is redeemed. Signing out through the SDK
(which revokes that session) therefore ends the hosted-login session too, and the next
prompt=none returns login_required. The previous session is not revoked, so another open tab
keeps working until its own session ends.
HTTP/1.1 302 Found
Location: https://game.example/callback?code=ac_…&state=…
Set-Cookie: __Host-auth4_session=…; Path=/; Max-Age=…; HttpOnly; Secure; SameSite=Lax
Cache-Control: no-store
HTTP/1.1 302 Found
Location: https://game.example/callback?state=…&error=login_required
Cache-Control: no-store
Tokens never appear in a URL: the code is exchanged at POST /oauth/token with the PKCE
verifier, as before.
Browser SDK (@auth4/sdk-browser)
restoreSession(options?: RestoreSessionOptions): Promise<RestoreSessionResult>;
interface RestoreSessionOptions {
scope?: string | readonly string[]; // must already be consented; defaults as for login()
url?: string | URL; // the current page URL; defaults to window.location.href
signal?: AbortSignal;
}
type RestoreSessionResult =
| { status: "signed_in"; session: BrowserSession }
| { status: "signed_out"; reason: "login_required" | "consent_required" | "interaction_required" }
| { status: "redirecting" };
Call it on every page load:
- With a live in-memory session it resolves
signed_inat once. - On the redirect URI with an authorization response, it completes the callback (state, PKCE and
ID-token checks as in
handleCallback()) and removes the response from the address bar. A code givessigned_in. An interaction error answering the SDK’s ownprompt=nonerequest givessigned_outwith that reason. Any other error, or astatethe SDK did not issue, rejects ashandleCallback()does. - Anywhere else it stores the usual short-lived PKCE and state record in
sessionStorage, marked as silent, navigates the top-level page to the authorization endpoint withprompt=none, and resolvesredirecting. The player then lands on the redirect URI, where the next call finishes. - It rejects with
authorization_failedbefore navigating if discovery does not listnoneinprompt_values_supported. - A restored session is never a guest. If a token set carries the guest claim, it is dropped and
the result is
signed_outwithlogin_required.
Tokens stay in memory. Nothing new is written to storage, and the refresh option works as before.
const result = await auth4.restoreSession();
if (result.status === "signed_out") showSignInButton();
Tests
cd tests/integration && pnpm exec vitest run auth4d-140:auth4d-140.silent-authorization.test.ts: discovery; code without UI;login_requiredwithout a session, after logout, for disabled players and for un-upgraded guests (restorable after upgrade);consent_requiredfor new scopes, revoked consent and another application; the SDK restoring against the real API. Security: no redirect to unregistered or look-alike redirect URIs, state required and echoed exactly, other prompt values and duplicates rejected, S256 PKCE enforced and bound, no tokens in the redirect, and no cross-project sessions.auth4d-140.restore-session.test.ts: the SDK’s redirect, callback handling, signed-out reasons, forged state, guest refusal and the discovery check.
pnpm exec playwright test -c tests/e2e/auth4d-140.playwright.config.ts(ports 5260 and 5361): real reloads in Chromium restore the player with one silent top-level redirect and no iframe; sign-out then reload giveslogin_required; a forged callback exchanges nothing; no token reaches a URL,sessionStorage,localStorageordocument.cookie.
Source: docs/api-spec/auth4d-140.md