AUTH4D-13 email-code authentication
Email-code authentication is available to enabled browser clients on active customer tenants with
the tenant’s email provider enabled. Requests use the configured auth origin and the HttpOnly
__Host-auth4_flow_{flowId} cookie created by the authorization flow. The same cookie must be
present for both endpoints. Challenges and proofs use tenant-scoped SQLite Durable Object state;
codes are stored only as HMACs keyed by AUTH_PRIVACY_KEY.
Request and confirm
POST /t/{tenantId}/email/verification/start
Origin: {AUTH_ORIGIN}
Cookie: __Host-auth4_flow_{flowId}=...
Content-Type: application/json
{"client_id":"cli_…","browser_flow_id":"flw_…","email":"player@example.test"}
A valid request returns HTTP 202 with the same response for an existing address, a new address, or an address for a tenant with signup disabled:
{ "expires_in": 600, "resend_after": 60 }
The email contains a uniformly generated six-digit code. Each code expires after ten minutes. A
resend after the cooldown replaces the earlier code with a new challenge and does not replay an
expired delivery. Local development and tests can use the @auth4/email local mailbox adapter.
POST /t/{tenantId}/email/verification/confirm
Origin: {AUTH_ORIGIN}
Cookie: __Host-auth4_flow_{flowId}=...
Content-Type: application/json
{"client_id":"cli_…","browser_flow_id":"flw_…","email":"player@example.test","code":"381042"}
Successful confirmation resolves or creates the tenant-local verified email identity and returns a one-time proof handle:
{ "proof": { "proof_id": "prf_…", "expires_in": 300 } }
The proof is bound to the tenant, client, authorization flow, email-verification purpose, user, and
identity. Pass it to POST /oauth/authorize/continue with proof_purpose=email_verification and
the same browser cookie. OAuth continuation validates and consumes the live flow and proof. The
proof cannot be used for identity linking or for a different flow, client, or tenant. Invalid,
expired, replayed, mismatched, signup-disabled unknown-account, and exhausted-attempt codes all
return HTTP 400 with {"error":"invalid_code"}.
Abuse limits and account policy
The atomic rate-limit authority enforces one send per flow/address per 60 seconds, five sends per address per tenant per hour, ten sends per IP per hour, 500 sends per tenant per hour, and 20,000 sends globally per hour. Verification permits at most five guesses per flow/address challenge and 20 guesses per IP per hour. Rate-limit subjects are HMAC-pseudonymized before reaching Durable Objects. Missing privacy or atomic-state configuration fails closed.
The frozen D1 schema has no signup-enabled column. The feature accepts a trusted signup-policy callback and currently defaults to enabled, matching the existing tenancy policy adapter’s default. When that callback disables signup, existing email identities can still sign in; a verified address without an existing identity receives the same invalid-code response as a failed code and creates no user.
Runtime composition handover
The feature calls the existing Auth4Services.email.sendVerificationCode port. The API runtime must
provide that port with the AUTH4D-10 outbox, tenant-bound @auth4/crypto cipher, queue producer,
and provider settings before deployed requests can send mail; the current shared runtime adapter is
still fail-closed. The current API factory also uses the default-enabled signup policy until a
trusted tenant setting adapter is supplied.
Source: docs/api-spec/auth4d-13.md