AUTH4D-66 browser SDK guest sign-in and guest upgrade
@auth4/sdk-browser adds signInAsGuest() and upgradeGuest(). This is an SDK change only. It
uses the existing POST /t/{tenantId}/guest endpoint (auth4d-15.md) and the hosted
/login/upgrade window protocol (auth4d-23.md), and adds no endpoints.
Guest sign-in
signInAsGuest({ signal? }) replaces any current session and sends:
POST {issuer}/guest
Content-Type: application/json
Accept: application/json
{"client_id":"cli_…","browser_flow_id":"flw_<43 random base64url characters>"}
The request uses credentials: "omit", so the browser stores none of the API’s cookies, and a fresh
browser_flow_id per call. The response must carry a user_id and a valid browser token set (a
bearer access token that lives at most five minutes). The access token’s sub must equal user_id.
The resolved BrowserSession is { user: { sub: user_id }, expiresAt, isGuest }. isGuest is true
only when the access token carries auth4_guest: true (auth4d-64.md). The SDK reads that claim
without verifying the signature, because the token came straight from the issuer over TLS. Resource
servers still verify access tokens with the server SDK. The response’s id_token is not kept. Its
refresh_token is kept in memory only, and only with the refresh: true option (auth4d-65.md).
Every session, not just guest sessions, now reports isGuest, and the value is re-read on each
renewal.
The guest endpoint is not a client-CORS path. A cross-origin page needs its origin in the
Worker’s CLIENT_ORIGINS variable (comma-separated), which feeds the boundary’s auth CORS allow-list.
Without that, the boundary returns 403 origin_denied and the SDK rejects with
guest_sign_in_failed.
Required API addition (not implemented by this ticket)
Customer origins should not need a deployment variable. POST /t/{tenantId}/guest should get
per-client CORS like /oauth/token and /oauth/revoke (AUTH4D-63). The boundary would leave the
path to the guest feature. The feature would then accept an Origin only when it is one of the
named browser client’s registered origins, and send Access-Control-Allow-Origin without
credentials.
Failures reject with guest_sign_in_failed, including HTTP errors such as 429 slow_down, invalid
JSON, a missing user_id, a subject mismatch, or an invalid token set. The client then has no
session.
Guest upgrade (opener side)
upgradeGuest({ signal? }) requires a live session with isGuest: true. Without one it rejects
with invalid_session or not_guest and opens nothing. Otherwise it synchronously calls:
window.open(
"{authOrigin}/login/upgrade?tenant_id={tenantId}&client_id={clientId}",
"auth4-guest-upgrade",
"popup,width=480,height=720",
);
{authOrigin} is the issuer’s origin. The window is opened without noopener, and the URL carries
no token. The method must be called directly from a user gesture. A null window rejects with
popup_blocked. While an upgrade is pending, another call focuses the window and returns the same
promise.
The SDK ignores every message event unless event.origin equals the auth origin exactly and
event.source is the upgrade window. It then handles these messages:
| Message from the window | SDK behaviour |
|---|---|
{type:"auth4:guest-upgrade:ready", tenant_id, client_id} |
If the tenant and client match, it posts {type:"auth4:guest-upgrade:token", access_token} to the window with target origin set to the auth origin. It answers each ready, which covers the Discord round trip. |
{type:"auth4:guest-upgrade:complete", result} |
Validates result (see below), replaces the token set and resolves. |
{type:"auth4:guest-upgrade:error", error:"identity_in_use"} |
Rejects with identity_in_use and keeps listening (see below). |
{type:"auth4:guest-upgrade:error", error:"invalid_session"} |
Rejects with invalid_session. |
{type:"auth4:guest-upgrade:cancelled"} |
Rejects with cancelled. |
The token sent for a ready message comes from getAccessTokenSilently(), so it can be renewed
first. It is sent only while the session that started the upgrade is still live and still a guest.
After a logout or a session change, the SDK rejects with invalid_session and sends nothing. The SDK
polls window.closed every 500 ms, and a closed window rejects with window_closed. Aborting the
signal closes the window and rejects with cancelled.
Completion
result is the POST /t/{tenantId}/guest/upgrade response: user_id, identity and the
replacement token set. user_id and the access token’s sub must both equal the guest’s user ID,
and identity.identity_id and identity.provider must be strings. Otherwise the SDK rejects with
upgrade_failed and keeps the guest token set. A valid result replaces the access token, expiry,
isGuest and (with refresh: true) refresh token in one synchronous step. That step also advances
the session generation, so a renewal still in flight for the revoked guest session discards its
result. The promise resolves with:
{
session: { user: { sub }, expiresAt, isGuest: false },
identity: { identityId, provider },
}
The tokens stay inside the client. Read them with getAccessToken() or getAccessTokenSilently().
The SDK also emits a session event { type: "upgraded", session }, which carries no token values.
After identity_in_use the hosted window offers to link a different credential, so the SDK keeps
answering that window until it closes or sends another final message. A later completion still
replaces the token set and emits upgraded, even though the promise has already rejected.
Errors
New Auth4BrowserError codes: guest_sign_in_failed, not_guest, invalid_session,
identity_in_use, popup_blocked, window_closed and upgrade_failed. cancelled also covers
“Not now”.
Tests
pnpm exec vitest run packages/sdk-browserrunspackages/sdk-browser/src/guest.test.ts, which uses a mocked window, popup andpostMessage.pnpm exec playwright test -c tests/e2e/auth4d-66.playwright.config.tsbuilds the login page intoapps/login/.wrangler/auth4d-66-distand serves it on port 5199. A routed game page onhttp://localhost:5299runs the transpiled SDK, and the API is mocked. The suite covers the full email upgrade,identity_in_use, “Not now”,invalid_sessionand a closed window.
Source: docs/api-spec/auth4d-66.md