AUTH4D-124 Steam on hosted login, device approval, guest upgrade and the console

This change makes Steam sign-in (AUTH4D-123 (repository file docs/api-spec/auth4d-123.md)) visible to players and developers. The hosted login, the device approval page and the guest upgrade page offer “Continue with Steam” or “Link a Steam account” when the project has Steam on. The console sign-in methods screen gets a Steam card. The only API changes are the ones the device approval page needs. They follow the Discord path from AUTH4D-120 (repository file docs/api-spec/auth4d-120.md).

Device verification data

For decision: "login_required", login_methods in GET /device/verify gains steam. As on the login context, the key is present (and true) only when Steam is on for the project:

{
  "decision": "login_required",
  "login_methods": { "email": true, "discord": false, "steam": true }
}

steam is optional in DeviceVerifyResponseSchema, so earlier responses still parse.

Start Steam from a device sign-in flow

GET /t/{tenantId}/identity/steam/start?client_id=…&browser_flow_id=… now accepts a live device sign-in flow when browser_flow_id names no OAuth authorization flow. The checks are the same as for Discord. The __Host-auth4_flow_{flowId} cookie must match, the flow must belong to this tenant and native client, and its code must still be pending. Otherwise start returns 403 access_denied. If Steam is off it also returns 403 access_denied. The Steam state records deviceApproval: true, and the request to Steam is unchanged: same openid.realm, and a openid.return_to that carries only the state.

Callback

The callback’s checks are unchanged. A native client is accepted for guest-linking and device sign-in states. For a device sign-in state it redirects to the hosted device page instead of /login:

HTTP/1.1 302 Found
Location: https://auth.auth4.dev/login/device?tenant_id=ten_…&client_id=cli_…&flow_id=flw_…&proof_id=prf_…&proof_purpose=steam_login
Cache-Control: no-store, max-age=0
Referrer-Policy: no-referrer

A cancelled, rejected or failed Steam sign-in returns error=access_denied or error=temporarily_unavailable instead of the proof. The user code is never in either URL.

Resume

POST /device/sign-in/resume accepts a proof with purpose steam_login as well as discord_login. All other rules are the same as in AUTH4D-120: Origin, flow cookie, tenant, client and flow binding, one-time proofs and the per-flow rate limit. Without a proof the endpoint reads the pending code back (status: "login_required"), so the player can choose another method.

Hosted pages

  • /login shows “Continue with Steam” after Discord when login_methods.steam is true. After the round trip the consent screen says “Signed in with Steam.” and passes the proof to POST /oauth/authorize/continue with proof_purpose: "steam_login".
  • /login/device shows the method choice when Discord or Steam is on. Before it leaves for Steam it removes any user_code from its own address, as for Discord.
  • /login/upgrade (browser and native intent modes) shows “Link a Steam account”. The callback returns to /login with the proof, which hands it back to the pending upgrade.

An error return does not say which provider was used. So before leaving, the page stores the provider name (discord or steam, not a secret) in session storage under auth4.provider.{flowId}. It reads and deletes the entry on return, so the error message names the right provider. Without storage the message names Discord.

Discord and Steam buttons share one style (button-provider). Each shows a decorative icon from @auth4/brand as a CSS background, so the accessible name is the label alone. The Steam icon is a neutral rising-steam glyph, not Valve’s logo, and is always next to the word “Steam”. Vite emits the icons as same-origin files, which the CSP allows (img-src 'self').

Console

The sign-in methods screen has a Steam card with an “Enable Steam sign-in” checkbox. It explains that no credentials are needed and where players will see Steam. Saving sends PATCH /management/tenants/{tenantId} with {"providers": {"steam": {"enabled": …}}}, and only when the value changed. As with the other project settings, only owners can change it. Changing it needs tenant:write, which admins and viewers do not have, so they see the card read-only. A project view without providers.steam, from before AUTH4D-123, shows Steam as off.

Tests

  • tests/integration/auth4d-124.device-steam.test.ts: device verify reports steam, a device flow through a stubbed Steam to /login/device, resume and approval, cancel and rejected assertions, Steam off, and a missing flow cookie.
  • tests/e2e/auth4d-124.steam-login.spec.ts (login preview, port 5241): a stubbed Steam round trip on /login, /login/device and native /login/upgrade, the cancel message, Steam hidden when off, and the 320px layout.
  • tests/e2e/auth4d-124.steam-console.spec.ts (console preview, port 5242): an owner enabling and disabling Steam, with the mouse and the keyboard, read-only for admins and viewers, a view with no Steam setting, and the 320px layout.
cd tests/integration && pnpm exec vitest run auth4d-124.device-steam.test.ts
pnpm exec playwright test -c tests/e2e/auth4d-124.playwright.config.ts

Source: docs/api-spec/auth4d-124.md