Files
website/server/src/auth/providers/google.provider.js
wtclaude fbb4b0bd91
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 29s
PR Checks / client-build (pull_request) Successful in 31s
PR Checks / server-tests (pull_request) Successful in 10m34s
feat(auth): unique, changeable, verifiable email addresses (engagement Phase 1b)
Makes `users.email` unique, de-duplicates the addresses an upgrade will find,
and builds the self-service change-and-verify flow that did not exist.

The uniqueness index is on a generated `email_norm AS (LOWER(email)) STORED`
column under `utf8mb4_bin`, NOT on `email` under a `_ci` collation as the plan
specified. Every case-insensitive collation this server offers is also
accent-insensitive: `josé@x.com` and `jose@x.com` compare equal, and those are
two different mailboxes. The plan's index would have refused the second address
forever and the de-duplication would have nulled a legitimate account's.

A requested address is STAGED in `email_pending` and only a tokened link
installs it, so a typo cannot silently redirect account-recovery mail.

`isDuplicateUsername()` now distinguishes the two indexes. All five call sites
branch on it; each answers differently on purpose, because a public form, an
IdP callback, a half-completed invite and an admin screen do not owe the same
person the same amount of truth.

SSO reads the IdP's actual `email_verified`/`verified` claim instead of
inferring verification from an address merely being present.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-29 01:53:50 -05:00

43 lines
1.4 KiB
JavaScript

// Built-in Google provider (OAuth2 / OpenID Connect). Endpoints are hardcoded —
// admins configure only Enabled + Client ID + Client Secret. Uses the OIDC
// userinfo endpoint; `sub` is Google's stable per-user id.
const OAuth2Provider = require('./oauth2.provider')
class GoogleProvider extends OAuth2Provider {
constructor(config = {}) {
super({ kind: 'google', name: 'Google', ...config, id: config.id || 'google' })
}
authEndpoint() {
return 'https://accounts.google.com/o/oauth2/v2/auth'
}
tokenEndpoint() {
return 'https://oauth2.googleapis.com/token'
}
userinfoEndpoint() {
return 'https://openidconnect.googleapis.com/v1/userinfo'
}
scopeString() {
return 'openid email profile'
}
authParams() {
// Online access (no refresh token needed for login), and let the user pick
// an account rather than silently reusing a signed-in one.
return { access_type: 'online', prompt: 'select_account' }
}
normalizeProfile(p = {}) {
return {
subject: p.sub,
email: p.email || null,
// Google's OIDC userinfo carries the standard `email_verified` claim. Read
// it rather than inferring verification from the mere presence of an
// address, which is what this code used to do (ENGAGEMENT.md §0.6/1b).
emailVerified: p.email_verified === true || p.email_verified === 'true',
name: p.name || p.email || null,
}
}
}
module.exports = GoogleProvider