AUTH4D-266 verified email step: the hosted-login “Add your email” page
Hosted login shows this page when POST /oauth/authorize/continue parks a sign-in for the
verified email step (auth4d-262.md (repository file docs/api-spec/auth4d-262.md)). The page asks for an address, mails a code
and checks it through the step endpoints (auth4d-263.md (repository file docs/api-spec/auth4d-263.md)). This ticket adds the
page, two page-text fields for it, and a restart option on abandon.
The page
/login/verify-email?tenant_id=…&client_id=…, one more copy of the hosted-login shell (ROUTES
in apps/login/vite.config.ts).
- Opening. When
continueanswers{"interaction":{"type":"verify_email","step":"est_…"}}, the sign-in screen opens the step in place and moves the address bar to the page URL withhistory.replaceState. The step handle is kept in that history entry’s state, so a reload of the same tab continues the step. It is never in the URL,sessionStorageorlocalStorage. The HttpOnly__Host-auth4_email_stepcookie binds the step to the browser. Opened without the handle (another tab, a copied link, or the302answer of a non-JSONcontinue), the page says the step has expired. - Address.
statedecides the first screen: the address form, or the code form when a code was already sent (it then shows only the masked address). The address goes only in the body ofsend. - Code. The code form is the email-code screen of hosted sign-in. Resend calls
resend, which mails the address of the live code, so the page never needs the address again. Resend is disabled during the 60-second cooldown, with a visible countdown. A refused resend (slow_down) restarts the countdown withretry_after, andcode_expiredreturns to the address form. - Confirm. A
redirect_toanswer opens the application’s redirect URI. A Flows denial or a terms screen are shown as aftercontinue. Until the next ticket replacesconfirm’s bare204, the page then says the address is verified and asks the player to start again. - Use a different sign-in method. Shown on both forms. It calls
abandonwithrestart: trueand opens the sign-in page it answers, which lists the methods again for the same authorization request. - Errors are hosted login’s own wording for the error codes. Server descriptions are never
shown.
step_expired(and403) show the expired screen.
The page meets the hosted-login rules: one h1, focus moves to it on every screen, role="status"
and role="alert" regions, a visible focus ring, no horizontal scrolling at 320px, and the strict
Content Security Policy (no inline script or style).
Page text
Two more per-application page-text fields (AUTH4D-166) in BrandingSchema and
BrandingUpdateSchema, stored in migration 0038 (page_verify_email_title,
page_verify_email_intro):
| Field | Limit | Replaces |
|---|---|---|
verify_email_title |
60 | The heading, “Add your email” |
verify_email_intro |
280 | The lead, “{App} needs a verified email address for …” |
They follow the PageTextSchema rules (plain text, no <>{}, no links, no runs of six digits)
and are set with the existing PATCH /management/tenants/{tenantId}/clients/{clientId}
branding object, where null clears a field. The console shows them in the Page text form, in
an “Add your email page” group. GET /t/{tenantId}/login/context serves them in branding when
set. The audit event management.client.presentation.updated names them as
branding.verify_email_title and branding.verify_email_intro and never holds the text. The
code entry, the countdown and every message stay fixed.
abandon with restart
POST /t/{tenantId}/email-step/abandon accepts an optional restart: true (any other value is
400 invalid_request):
{ "client_id": "cli_…", "step": "est_…", "restart": true }
The step is consumed, its code dropped and email_step.abandoned written, exactly as without
restart. The step and old flow cookies are cleared. Instead of returning to the application, the
parked request (client, redirect URI, state, PKCE challenge, scopes, nonce, prompt=consent) is
stored as a new hosted-login flow, bound by a new __Host-auth4_flow_<id> cookie, and the answer
is its sign-in page:
{ "redirect_to": "https://auth.auth4.dev/login?tenant_id=ten_…&client_id=cli_…&flow_id=flw_…" }
The player the step was for is not signed in on that flow, so any method starts afresh. Without
restart, abandon still sends the application access_denied.
Source: docs/api-spec/auth4d-266.md