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) {