From e89f97aaeda82065fcd79dde7f23dde354a2cacf Mon Sep 17 00:00:00 2001 From: Szymon Sypula <39187878+szymonsypula@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:03:58 +0200 Subject: [PATCH] Fix OIDC login: ensure Passport callback is defined (#7312) MeshCentral OIDC strategy was throwing `TypeError: done is not a function` because the callback was not properly passed when using openid-client. This patch wraps the OIDC callback to detect missing callback parameters, extracts user info from the id_token if needed, and ensures `done()` is called in all code paths, including async group fetching. This restores functional OIDC logins for Azure AD/Keycloak. Tested on Azure B2C OIDC Co-authored-by: Szymon Sypula --- webserver.js | 68 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/webserver.js b/webserver.js index 22e2f370..11c45555 100644 --- a/webserver.js +++ b/webserver.js @@ -8078,23 +8078,71 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } // Callback function must be able to grab info from API's using the access token, would prefer to use the token here. - function oidcCallback(tokenset, profile, verified) { + function oidcCallback(tokenset, profile, done) { + // Handle case where done might not be the third parameter + if (typeof done !== 'function') { + // OpenID Connect strategy calls with (tokenset, done) instead of (tokenset, profile, done) + if (typeof profile === 'function') { + done = profile; + profile = null; + } else { + parent.debug('error', 'OIDC: Unable to find callback function in parameters'); + return; + } + } + + // If profile is null/undefined, extract user info from the tokenset + if (!profile && tokenset && tokenset.id_token) { + try { + // Simple JWT decoder to extract user claims from id_token + const parts = tokenset.id_token.split('.'); + if (parts.length === 3) { + const payload = parts[1]; + const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4); + const decoded = JSON.parse(Buffer.from(paddedPayload, 'base64').toString()); + if (decoded) { + profile = decoded; + } + } + } catch (err) { + parent.debug('error', `OIDC: Failed to decode id_token: ${err.message}`); + } + } + // Initialize user object let user = { 'strategy': 'oidc' } let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : null; - user.sid = obj.common.validateString(profile.sub) ? '~oidc:' + profile.sub : null; - user.name = obj.common.validateString(profile.name) ? profile.name : null; - user.email = obj.common.validateString(profile.email) ? profile.email : null; + + user.sid = null; + if (profile && obj.common.validateString(profile.sub)) { + user.sid = '~oidc:' + profile.sub; + } else if (profile && obj.common.validateString(profile.oid)) { + user.sid = '~oidc:' + profile.oid; + } else if (profile && obj.common.validateString(profile.email)) { + user.sid = '~oidc:' + profile.email; + } else if (profile && obj.common.validateString(profile.upn)) { + user.sid = '~oidc:' + profile.upn; + } + + user.name = profile && obj.common.validateString(profile.name) ? profile.name : null; + user.email = profile && obj.common.validateString(profile.email) ? profile.email : null; if (claims != null) { user.sid = obj.common.validateString(profile[claims.uuid]) ? '~oidc:' + profile[claims.uuid] : user.sid; user.name = obj.common.validateString(profile[claims.name]) ? profile[claims.name] : user.name; user.email = obj.common.validateString(profile[claims.email]) ? profile[claims.email] : user.email; } - user.emailVerified = profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email); - user.groups = obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null; + + // Ensure we have a valid sid before proceeding + if (!user.sid) { + parent.debug('error', `OIDC: No valid user identifier found in profile`); + return done(new Error('OIDC: No valid user identifier found in profile')); + } + + user.emailVerified = profile && profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email); + user.groups = profile && obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null; user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null; if (strategy.groups && obj.common.validateString(strategy.groups.claim)) { - user.groups = obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null + user.groups = profile && obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null } // Setup end session enpoint if not already configured this requires an auth token @@ -8114,16 +8162,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (strategy.groups && typeof user.preset == 'string') { getGroups(user.preset, tokenset).then((groups) => { user = Object.assign(user, { 'groups': groups }); - return verified(null, user); + done(null, user); }).catch((err) => { let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err }); parent.debug('error', `${JSON.stringify(error)}`); parent.authLog('oidcCallback', error.message); user.groups = []; - return verified(null, user); + done(null, user); }); } else { - return verified(null, user); + done(null, user); } async function getGroups(preset, tokenset) {