Optional "Sign in with Google" with auto-provisioning, fully config-gated: without GOOGLE_CLIENT_ID/SECRET and OAUTH_REDIRECT_URL the routes 404 and the button is hidden, so deployments without SSO are unaffected. - migration 028: users.google_sub (unique) + email; password_hash nullable for OAuth-only accounts - src/auth/google-oauth.js: lazy google-auth-library, ID-token verify, GOOGLE_ALLOWED_DOMAIN enforcement, requires email_verified === true - auth routes: /auth/google (state-CSRF redirect), /auth/google/callback, /auth/google/enabled; reuses establishSession - web-ui: "Sign in with Google" on the login screen (shown only when enabled), friendly callback error handling - .env.example documents all new vars Security hardening (from review of this + the TOTP work): - resolveGoogleUser links ONLY by google_sub, never by email — a Google login can never seize a pre-existing local account (account-takeover fix) - a Google-linked account with TOTP still requires the second factor (ticket in session, /?mfa=1 step) instead of bypassing it - /login/totp now applies the per-IP login backoff - recovery-code consumption is atomic (WHERE used_at IS NULL + rowCount) - concurrent first-login race on google_sub is caught and re-resolved - tests: google-oauth config helpers + google-link takeover/dedup regression Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13 lines
728 B
SQL
13 lines
728 B
SQL
-- Migration 028 — Google OAuth (OIDC) sign-in.
|
|
--
|
|
-- google_sub is Google's stable subject identifier — the join key for a linked
|
|
-- or auto-provisioned account (unique, but NULL for password-only users).
|
|
-- email is captured for display + domain checks. password_hash becomes nullable
|
|
-- so an OAuth-only account can exist without a local password; such an account
|
|
-- simply can't use the password login path until an admin sets one.
|
|
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS google_sub TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS email TEXT;
|
|
ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_google_sub ON users(google_sub) WHERE google_sub IS NOT NULL;
|