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>
43 lines
1.4 KiB
JavaScript
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
|