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 <szymon.sypula@dieboldnixdorf.com>

Szymon Sypula committed Oct 24, 2025 at 15:03 UTC e89f97aaeda82065fcd79dde7f23dde354a2cacf
1 file changed +58 -10
webserver.js
+58 -10
@@ -8078,23 +8078,71 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8078 }
8079
8080 // Callback function must be able to grab info from API's using the access token, would prefer to use the token here.
8081 - function oidcCallback(tokenset, profile, verified) {
8081 + function oidcCallback(tokenset, profile, done) {
8082 + // Handle case where done might not be the third parameter
8083 + if (typeof done !== 'function') {
8084 + // OpenID Connect strategy calls with (tokenset, done) instead of (tokenset, profile, done)
8085 + if (typeof profile === 'function') {
8086 + done = profile;
8087 + profile = null;
8088 + } else {
8089 + parent.debug('error', 'OIDC: Unable to find callback function in parameters');
8090 + return;
8091 + }
8092 + }
8093 +
8094 + // If profile is null/undefined, extract user info from the tokenset
8095 + if (!profile && tokenset && tokenset.id_token) {
8096 + try {
8097 + // Simple JWT decoder to extract user claims from id_token
8098 + const parts = tokenset.id_token.split('.');
8099 + if (parts.length === 3) {
8100 + const payload = parts[1];
8101 + const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4);
8102 + const decoded = JSON.parse(Buffer.from(paddedPayload, 'base64').toString());
8103 + if (decoded) {
8104 + profile = decoded;
8105 + }
8106 + }
8107 + } catch (err) {
8108 + parent.debug('error', `OIDC: Failed to decode id_token: ${err.message}`);
8109 + }
8110 + }
8111 +
8112 // Initialize user object
8113 let user = { 'strategy': 'oidc' }
8114 let claims = obj.common.validateObject(strategy.custom.claims) ? strategy.custom.claims : null;
8085 - user.sid = obj.common.validateString(profile.sub) ? '~oidc:' + profile.sub : null;
8086 - user.name = obj.common.validateString(profile.name) ? profile.name : null;
8087 - user.email = obj.common.validateString(profile.email) ? profile.email : null;
8115 +
8116 + user.sid = null;
8117 + if (profile && obj.common.validateString(profile.sub)) {
8118 + user.sid = '~oidc:' + profile.sub;
8119 + } else if (profile && obj.common.validateString(profile.oid)) {
8120 + user.sid = '~oidc:' + profile.oid;
8121 + } else if (profile && obj.common.validateString(profile.email)) {
8122 + user.sid = '~oidc:' + profile.email;
8123 + } else if (profile && obj.common.validateString(profile.upn)) {
8124 + user.sid = '~oidc:' + profile.upn;
8125 + }
8126 +
8127 + user.name = profile && obj.common.validateString(profile.name) ? profile.name : null;
8128 + user.email = profile && obj.common.validateString(profile.email) ? profile.email : null;
8129 if (claims != null) {
8130 user.sid = obj.common.validateString(profile[claims.uuid]) ? '~oidc:' + profile[claims.uuid] : user.sid;
8131 user.name = obj.common.validateString(profile[claims.name]) ? profile[claims.name] : user.name;
8132 user.email = obj.common.validateString(profile[claims.email]) ? profile[claims.email] : user.email;
8133 }
8093 - user.emailVerified = profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email);
8094 - user.groups = obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null;
8134 +
8135 + // Ensure we have a valid sid before proceeding
8136 + if (!user.sid) {
8137 + parent.debug('error', `OIDC: No valid user identifier found in profile`);
8138 + return done(new Error('OIDC: No valid user identifier found in profile'));
8139 + }
8140 +
8141 + user.emailVerified = profile && profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email);
8142 + user.groups = profile && obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null;
8143 user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null;
8144 if (strategy.groups && obj.common.validateString(strategy.groups.claim)) {
8097 - user.groups = obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
8145 + user.groups = profile && obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
8146 }
8147
8148 // 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
8162 if (strategy.groups && typeof user.preset == 'string') {
8163 getGroups(user.preset, tokenset).then((groups) => {
8164 user = Object.assign(user, { 'groups': groups });
8117 - return verified(null, user);
8165 + done(null, user);
8166 }).catch((err) => {
8167 let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
8168 parent.debug('error', `${JSON.stringify(error)}`);
8169 parent.authLog('oidcCallback', error.message);
8170 user.groups = [];
8123 - return verified(null, user);
8171 + done(null, user);
8172 });
8173 } else {
8126 - return verified(null, user);
8174 + done(null, user);
8175 }
8176
8177 async function getGroups(preset, tokenset) {