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.5 KiB
JavaScript
43 lines
1.5 KiB
JavaScript
// Generic, fully-configurable OAuth2 / OIDC provider for custom IdPs (Authentik,
|
|
// Keycloak, Okta, Azure AD, Zitadel, …). Unlike the built-ins, its endpoints and
|
|
// scopes come from the stored config. Profile mapping follows OIDC conventions
|
|
// with sensible fallbacks for plain OAuth2 userinfo shapes.
|
|
|
|
const OAuth2Provider = require('./oauth2.provider')
|
|
|
|
class GenericOidcProvider extends OAuth2Provider {
|
|
constructor(config = {}) {
|
|
super({ kind: config.kind || 'oidc', ...config })
|
|
this.authorizeUrl = config.authorizeUrl ?? config.authorize_url ?? null
|
|
this.tokenUrl = config.tokenUrl ?? config.token_url ?? null
|
|
this.userinfoUrl = config.userinfoUrl ?? config.userinfo_url ?? null
|
|
this.scopes = config.scopes || 'openid email profile'
|
|
}
|
|
|
|
authEndpoint() {
|
|
return this.authorizeUrl
|
|
}
|
|
tokenEndpoint() {
|
|
return this.tokenUrl
|
|
}
|
|
userinfoEndpoint() {
|
|
return this.userinfoUrl
|
|
}
|
|
scopeString() {
|
|
return this.scopes
|
|
}
|
|
normalizeProfile(p = {}) {
|
|
return {
|
|
subject: p.sub || p.id || p.user_id || p.uid || null,
|
|
email: p.email || null,
|
|
// The standard OIDC claim. An IdP that omits it has not asserted anything,
|
|
// so the address stays unverified and the user proves it the ordinary way —
|
|
// absent is treated as false, never as true.
|
|
emailVerified: p.email_verified === true || p.email_verified === 'true',
|
|
name: p.name || p.preferred_username || p.username || p.email || null,
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = GenericOidcProvider
|