From a9e620aab801ff69851040d92c5f2259f94c8afb Mon Sep 17 00:00:00 2001 From: Enes Uysal Date: Wed, 12 Aug 2026 15:42:29 +0300 Subject: [PATCH 1/2] feat(acp): add option to skip email verification for SSO registrations trustEmailVerified only confirms an email when the provider actually sends an email_verified claim. Providers that omit it entirely, such as Microsoft's OIDC userinfo endpoint, leave every SSO user unverified with no way to opt out. --- lib/controllers.js | 2 +- library.js | 35 ++++++++++++++++--- .../partials/edit-oauth2-strategy.tpl | 5 +++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/controllers.js b/lib/controllers.js index a9c81fc..7dacad1 100644 --- a/lib/controllers.js +++ b/lib/controllers.js @@ -74,7 +74,7 @@ Controllers.editStrategy = async (req, res) => { payload.enabled = !!req.body.enabled; - const checkboxes = ['forceUsernameViaEmail', 'usernameViaEmail', 'trustEmailVerified', 'syncFullname', 'syncPicture']; + const checkboxes = ['forceUsernameViaEmail', 'usernameViaEmail', 'trustEmailVerified', 'skipEmailVerification', 'syncFullname', 'syncPicture']; checkboxes.forEach((prop) => { payload[prop] = payload.hasOwnProperty(prop) && payload[prop] === 'on' ? 1 : 0; }); diff --git a/library.js b/library.js index 961bb15..887e9ed 100644 --- a/library.js +++ b/library.js @@ -211,18 +211,45 @@ OAuth.getAssociations = async () => { })); }; +OAuth.isEmailTrusted = (strategy, payload) => { + if (parseInt(strategy.skipEmailVerification, 10)) { + return true; + } + + return !!parseInt(strategy.trustEmailVerified, 10) && + (payload.email_verified || payload.email_verified === true); +}; + +OAuth.confirmEmailIfTrusted = async (payload, uid) => { + const strategy = await OAuth.getStrategy(payload.name); + if (!payload.email || !OAuth.isEmailTrusted(strategy, payload)) { + return; + } + + const { email, 'email:confirmed': confirmed } = await user.getUserFields(uid, ['email', 'email:confirmed']); + if (parseInt(confirmed, 10) === 1 || (email && email.toLowerCase() !== payload.email.toLowerCase())) { + return; + } + + if (!email) { + await user.setUserField(uid, 'email', payload.email); + } + + await user.email.confirmByUid(uid); + winston.verbose(`[plugin/sso-oauth2-multiple] Confirmed email for uid ${uid} via ${payload.name}`); +}; + OAuth.login = async (payload) => { let uid = await OAuth.getUidByOAuthid(payload.name, payload.oAuthid); if (uid !== null) { // Existing User + await OAuth.confirmEmailIfTrusted(payload, uid); return ({ uid }); } - const { trustEmailVerified } = await OAuth.getStrategy(payload.name); + const strategy = await OAuth.getStrategy(payload.name); const { email } = payload; - const email_verified = - parseInt(trustEmailVerified, 10) && - (payload.email_verified || payload.email_verified === true); + const email_verified = OAuth.isEmailTrusted(strategy, payload); // Check for user via email fallback diff --git a/static/templates/partials/edit-oauth2-strategy.tpl b/static/templates/partials/edit-oauth2-strategy.tpl index dcc4f65..50f9f6b 100644 --- a/static/templates/partials/edit-oauth2-strategy.tpl +++ b/static/templates/partials/edit-oauth2-strategy.tpl @@ -121,6 +121,11 @@ +
+ + +
+
From 50e05c76aa11bb4beaa438ce46d5af55ad549135 Mon Sep 17 00:00:00 2001 From: Enes Uysal Date: Wed, 12 Aug 2026 16:28:59 +0300 Subject: [PATCH 2/2] fix(login): keep skipEmailVerification out of the email fallback Matching an incoming address against an existing account is an account takeover vector when the provider has not actually verified it, so that path stays keyed off a genuine email_verified claim. --- library.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/library.js b/library.js index 887e9ed..dc456bf 100644 --- a/library.js +++ b/library.js @@ -211,13 +211,17 @@ OAuth.getAssociations = async () => { })); }; +OAuth.isEmailVerifiedByProvider = (strategy, payload) => ( + !!parseInt(strategy.trustEmailVerified, 10) && + (payload.email_verified || payload.email_verified === true) +); + OAuth.isEmailTrusted = (strategy, payload) => { if (parseInt(strategy.skipEmailVerification, 10)) { return true; } - return !!parseInt(strategy.trustEmailVerified, 10) && - (payload.email_verified || payload.email_verified === true); + return OAuth.isEmailVerifiedByProvider(strategy, payload); }; OAuth.confirmEmailIfTrusted = async (payload, uid) => { @@ -253,7 +257,7 @@ OAuth.login = async (payload) => { // Check for user via email fallback - if (email && email_verified) { + if (email && OAuth.isEmailVerifiedByProvider(strategy, payload)) { uid = await user.getUidByEmail(payload.email); }