AUTH4D-302 verified email step on guest upgrades
Guest upgrades (auth4d-15.md (repository file docs/api-spec/auth4d-15.md), native intents in auth4d-67.md (repository file docs/api-spec/auth4d-67.md)) now
run the application’s verified email step (auth4d-260.md (repository file docs/api-spec/auth4d-260.md),
auth4d-262.md (repository file docs/api-spec/auth4d-262.md)) like every other sign-in path. The step is judged as a sign-in with
the linked credential, on the guest’s identities after the link: under the default when_missing
a guest who links Steam or Epic Games (no address) is asked for one; off, an upstream-verified
address (for example Google) and a guest who already has an Auth4-verified address skip it. Email,
password, SMS and passkey links never ask.
When the step applies, the credential is linked to the guest, but no replacement session is
created, no token is issued, the guest session keeps working and guest.upgraded is neither
audited nor published. The upgrade is parked as a guest_upgrade step in the tenant’s Durable
Object for ten minutes, keyed by tenant and the SHA-256 of the step handle. It holds the tenant,
application, guest user, the guest session it will replace, the linking flow, the linked identity,
the sign-in facts, the reason and the SHA-256 of the __Host-auth4_email_step cookie’s secret. It
never holds a token, a code, a poll secret or a native intent handle (only its SHA-256).
POST /t/{tenantId}/guest/upgrade
The request is unchanged. When the step applies, the proof and the linking intent are consumed and
the answer is 200 with Cache-Control: no-store and the __Host-auth4_email_step cookie
(EmailStepRequiredResponseSchema), always in JSON:
{ "interaction": { "type": "verify_email", "step": "est_…", "expires_in": 600 } }
Every other answer is as before. The hosted upgrade window opens the step’s “Add your email”
screen in place, with the address bar left on /login/upgrade.
POST /t/{tenantId}/guest/upgrade-intent/complete
The request is unchanged. When the step applies, the answer is
200 {"status": "verify_email", "interaction": {…}} (GuestUpgradeIntentEmailStepSchema) with the
step cookie, and the intent stays pending: /guest/upgrade-intent/token keeps answering
authorization_pending, and /begin, /complete and /deny answer 409 access_denied until the
step ends.
Step endpoints
POST /t/{tenantId}/email-step/{state,send,resend,confirm,abandon} work as in
auth4d-263.md (repository file docs/api-spec/auth4d-263.md) and take the guest’s client_id (a browser or native
application). They need the step cookie of the browser the upgrade was parked in.
-
confirmproves the code and attaches the address to the guest as anemailidentity (auth4d-264.md (repository filedocs/api-spec/auth4d-264.md)). A guest is never merged into another account, so an address another player owns always answers409 email_unavailable, whateverlinkVerifiedEmailsays; the step stays open for another address. The gate is checked again, then the upgrade finishes:- browser: the replacement session is created, the guest session revoked,
guest.upgradedaudited and published once (keyed on the replaced session), and the answer is200 {"status": "upgraded", "upgrade": {…}}(GuestEmailStepUpgradedSchema).upgradeis the/guest/upgradeanswer:user_id(the guest’s, unchanged),identity, the token set andsession_id. The ID token carriesidp(for examplesteam). Guest sessions are never granted theemailscope, so under the AUTH4D-265 (repository filedocs/api-spec/auth4d-265.md) rule the proven address is stored but not emitted (auth4d-332.md (repository filedocs/api-spec/auth4d-332.md)). The hosted window posts it to its opener only. - native: the guest session is revoked,
guest.upgradedaudited and published once, and the intent becomes completable. The answer is200 {"status": "approved", "identity": {…}}(GuestEmailStepApprovedSchema); no token. The game’s next poll of/guest/upgrade-intent/tokenreturns the token set with the same claims.
Both answers clear the step cookie.
Status Body When 401 {"error":"invalid_session"}The guest session ended or the guest was disabled meanwhile 403 {"error":"access_denied"}The step still applies 400 {"error":"expired_token"}Native: the upgrade intent expired meanwhile 410 {"error":"step_expired"}Replayed, expired, or another browser’s cookie - browser: the replacement session is created, the guest session revoked,
-
abandonends the step without upgrading. The guest session keeps working; the linked credential stays on the guest, so linking it again asks again rather than linking it twice. A native intent becomes pending again. The answer is{"redirect_to": "{AUTH_ORIGIN}/login/upgrade?tenant_id=…&client_id=…"}with the step cookie cleared;restartis accepted and ignored. The hosted page shows its methods again in place.
An expired step leaves the guest exactly as an abandoned one does.
Audit and events
The step writes the email_step.* events with the guest’s client_id and the linked method’s
idp, and no code or whole address. guest.upgraded is written and published once, only when the
upgrade finishes.
Source: docs/api-spec/auth4d-302.md