Authentication specification
Status: Google OIDC source candidate; email/password remains the deployed baseline.
Product contract
Soldi supports email/password authentication and may expose Google only when the current Worker has a complete, exact provider configuration. “Sign in” and “Join Soldi” are distinct:
- Sign in: a Google
submust already exist inoauth_identities. Email equality is never sign-in or linking authority. - Join Soldi: the current
TERMS_VERSIONacceptance is signed into the outbound state before leaving Soldi. A new account starts with zero purchased and promotional cents, remains unactivated until $500 of verified Stripe funding, and receives a random PBKDF2 password sentinel that cannot be used for password login. The existing recovery flow is the supported way to establish a password. - Existing-email collision: Soldi creates no account and inserts no identity. It issues a five-minute, HttpOnly signed link intent; the user must verify the existing account password before Soldi inserts the Google identity.
OIDC security contract
- Server-side Authorization Code flow, scopes exactly
openid email profile. - Exact environment redirect URI, signed ten-minute
state, OIDCnonce, PKCE S256, and a separately signed same-browser HttpOnly flow cookie. - Redirect targets pass the shared same-origin
sanitizeNextPathboundary before signing. - ID tokens are verified with
joseagainst Google JWKS, accepted Google issuers, the configured client ID audience, expiry, nonce, immutablesub, and booleanemail_verified. - Callback
state_idand SHA-256 code claims are inserted uniquely before exchange. Duplicate state, callback, or code use fails closed. - Google access, refresh, and ID tokens are never stored or placed in a Soldi SPA URL.
oauth_identitiesstores only provider, provider subject, Soldi user, verified-email snapshot, and timestamps. - Successful OAuth authentication issues the same signed session and CSRF cookies as password authentication. Demo and fixture identity rejection remains active in production.
Configuration contract
The readiness endpoint is GET /api/v1/auth/oauth/readiness. It returns enabled only when SESSION_SECRET, GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET, and the exact redirect URI are present.
- Staging:
https://staging.soldi.cc/api/v1/auth/oauth/google/callback - Production:
https://app.soldi.cc/api/v1/auth/oauth/google/callback - Development: an explicitly configured
http://localhostor loopback callback at the same path
The client ID and client secret are controller-required Cloudflare secret bindings. The redirect URI is a checked-in exact plain-text binding. Configuration names in source are not provider or deployment proof.
Data contract
Migration 0046_oauth_identities.sql creates:
oauth_identities, unique by(provider, provider_subject)and(user_id, provider), with a user lookup index.oauth_callback_claims, unique by signed state ID and SHA-256 code hash, with an expiry index.
No existing user, wallet, activation, consent, or password-recovery column is replaced.
Acceptance gates
- Protocol tests cover linked sign-in, terms-stamped signup, terms rejection, explicit password linking, state/code replay, state mismatch/expiry, nonce, issuer, audience, verified email, redirects, configuration, and token non-persistence.
- Login source tests cover fail-closed visibility, Sign in/Join separation, terms gating, and the explicit link prompt.
- Local browser acceptance covers Sign in and Join at
390×844and1440×900. It is source/UI evidence only; no live Google login is required. - Protected staging proof begins only after Cameron completes the provider checklist in
docs/runbooks/google-oauth.mdand authorizes the exact controller path.