AUTH4D-69 Unity WebGL guest upgrade
Unity WebGL builds can now upgrade guests. This is an SDK change only and adds no endpoints. In a
WebGL build, Auth4Client.UpgradeGuestAsync uses the hosted /login/upgrade window protocol
(auth4d-23.md), as the browser SDK does (auth4d-66.md). Native platforms keep using the upgrade
intent (auth4d-67.md, auth4d-68.md).
Routing
Auth4ClientOptions.UpgradeWindow (an IAuth4UpgradeWindow) sits alongside BrowserLauncher.
UpgradeGuestAsync picks one this way:
- If
UpgradeWindow.IsSupportedis true, it uses the popup window.WebGLUpgradeWindowreports true only in a WebGL player, never in the editor. - Otherwise, if
BrowserLauncher.IsSupportedis true, it uses the upgrade intent and the system browser. - Otherwise it throws
PlatformNotSupportedExceptionand sends no request.
In window mode no upgrade intent is created. A signed-out client throws login_required. A
non-guest session throws InvalidOperationException. In both cases nothing opens.
Opening the window
The window is opened before UpgradeGuestAsync first awaits, so a browser user gesture still
applies. The bridge (Runtime/Unity/Plugins/WebGL/Auth4Upgrade.jslib) opens:
window.open(
"{authOrigin}/login/upgrade?tenant_id={tenantId}&client_id={clientId}",
"auth4-guest-upgrade",
"popup,width=480,height=720",
);
The URL carries no token, and the window is opened without noopener. The bridge refuses any URL
whose origin isn’t exactly the auth origin or whose path isn’t /login/upgrade.
Unity WebGL handles input a frame after the browser event has returned, so the bridge checks
navigator.userActivation.isActive. If the gesture is still active, it opens the window at once.
Otherwise it waits for the next pointerup or touchend on the page, in the capture phase, and
opens the window inside that event. Games should start the upgrade from IPointerDownHandler, so
that next event is the release of the same press. A null window ends the upgrade with
PopupBlocked.
Window protocol (opener side)
The bridge ignores every message event unless event.origin equals the auth origin exactly and
event.source is the window it opened. It relays these messages to C#:
| Message from the window | Client behaviour |
|---|---|
{type:"auth4:guest-upgrade:ready", tenant_id, client_id} |
If the tenant and client match, C# gets a fresh guest access token and the bridge posts {type:"auth4:guest-upgrade:token", access_token} to the window, targeting the auth origin. |
{type:"auth4:guest-upgrade:complete", result} |
Validates result and replaces the token set (see below). |
{type:"auth4:guest-upgrade:error", error:"identity_in_use"} |
Throws Conflict and keeps listening. |
{type:"auth4:guest-upgrade:error", error:"invalid_session"} |
Throws SessionEnded. |
{type:"auth4:guest-upgrade:cancelled"} |
Throws Denied (“Not now”). |
The bridge checks popup.closed every 500 ms, and a closed window ends the upgrade with
WindowClosed. A token is sent for a ready only while the guest session that started the upgrade
is still live and still a guest, and the sign-out generation hasn’t changed. Otherwise the upgrade
ends with SessionEnded and nothing is sent. Cancelling the CancellationToken closes the window
and throws Cancelled. After Conflict the client keeps answering the window until it closes or
sends another final message. A later complete still replaces the token set and raises
SessionChanged. A new UpgradeGuestAsync call stops the old listener.
Completion
result is the POST /t/{tenantId}/guest/upgrade response. It goes through the same commit as the
native flow:
- The token set must be valid (
Bearer, a positiveexpires_inof at most a day). user_idmust equal the guest’s user ID, andidentitymust be present. Otherwise the returned credentials are revoked, the local session is cleared, and the client throwsinvalid_response.- If the guest signed out in the meantime, the new credentials are revoked and the client throws
InvalidOperationException. - Storage and memory are switched under the client’s lock, and
SessionChangedfires once with the non-guest session. The user ID doesn’t change.
Token handling
- No token appears in a URL or in
localStorage,sessionStorageor cookies. - Each string that the bridge passes to C# is copied into the wasm heap for a single callback. The payload, which may contain the upgraded token set, is zeroed before it is freed.
Auth4UpgradeWindowMessage.ToString()never includes the payload.- In WebGL the game must use a browser client whose allowed origins include the page origin, because
the hosted page posts only to registered origins. Guest sign-in from the page also needs that
origin in
CLIENT_ORIGINS(auth4d-66.md).
Progress and errors
Progress is reported as OpeningBrowser, then WaitingForPlayer once the window opens, then
Completed. In window mode ExpiresAt is DateTimeOffset.MaxValue and IntervalSeconds is 0.
This ticket adds the GuestUpgradeFailure values PopupBlocked (popup_blocked), WindowClosed
(window_closed) and SessionEnded (invalid_session).
Tests
dotnet test packages/sdk-unity/Tests~/Auth4.Protocol.Tests/Auth4.Protocol.Tests.csproj:UpgradeWindowTests.cscovers routing, the synchronous open, the handshake, every final message, continuing after a conflict, sign-out, invalid results and cancellation.pnpm exec playwright test -c tests/e2e/auth4d-69.playwright.config.ts: a harness page (packages/sdk-unity/Tests~/WebGL) loads the real.jslibthrough a minimal Emscripten shim. It runs against the built hosted page on port 5169, with a mocked API and a routed game originhttp://localhost:5369. The suite covers:- a full email upgrade, with no token in URLs, storage or the heap;
- the pointer-up fallback and cancel-to-close;
- forged messages from the game window and from another frame on the auth origin;
identity_in_usefollowed by a closed window;- a blocked popup;
- refused URLs.
Source: docs/api-spec/auth4d-69.md