OIDC - Azure roles (#7551)

stephannn committed Mar 9, 2026 at 11:33 UTC 9d8849ebdf4356511bc69c246a5b5bfb5edab41c
3 files changed +62 -20
meshcentral-config-schema.json
+15 -1
@@ -3994,7 +3994,17 @@
3994 "customer_id": {
3995 "type": "string",
3996 "description": "REQUIRED IF USING GROUPS: Customer ID from Google Workspace Admin Console (https://admin.google.com/ac/accountsettings/profile)"
3997 - }
3997 + },
3998 + "authorities": {
3999 + "type": [
4000 + "array"
4001 + ],
4002 + "description": "Default value is groups. Roles can be used in azure to assign app permissons",
4003 + "enum": [
4004 + "groups",
4005 + "roles"
4006 + ]
4007 + }
4008 }
4009 },
4010 "groups": {
@@ -4049,6 +4059,10 @@
4059 "description": "Custom claim to use."
4060 }
4061 }
4062 + },
4063 + "options": {
4064 + "type": "object",
4065 + "description": "Custom options to pass to the OpenID Client"
4066 }
4067 }
4068 }
sample-config-advanced.json
+5
@@ -582,6 +582,11 @@
582 "filter": [ "groupB", "groupC" ]
583 }
584 },
585 + "options" : {
586 + "params": {
587 + "prompt": "select_account"
588 + }
589 + },
590 "newAccounts": true,
591 "_newAccountsRights": [ "nonewgroups", "notools" ],
592 "_newAccountsUserGroups": [ "ugrp//xxxxxxxxxxxxxxxxx" ]
webserver.js
+42 -19
@@ -8074,13 +8074,18 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8074 strategy.options.params = Object.assign(strategy.options.params || {}, { 'scope': ['openid', 'profile', 'email'] });
8075 }
8076 if (typeof strategy.groups == 'object') {
8077 - let groupScope = strategy.groups.scope || null
8078 - if (groupScope == null) {
8079 - if (preset == 'azure') { groupScope = 'Group.Read.All' }
8080 - if (preset == 'google') { groupScope = 'https://www.googleapis.com/auth/cloud-identity.groups.readonly' }
8081 - if (typeof preset != 'string') { groupScope = 'groups' }
8077 + strategy.custom.authorities = obj.common.convertStrArray(strategy.custom.authorities, ' ')
8078 + // Check if authorities does not exist or includes groups
8079 + if((Array.isArray(strategy.custom.authorities) && strategy.custom.authorities.filter(x => x.trim().length > 0).length > 0) == false || strategy.custom.authorities.includes('groups')) {
8080 + let groupScope = strategy.groups.scope || null
8081 + if (groupScope == null) {
8082 + if (preset == 'azure') { groupScope = 'Group.Read.All' }
8083 + if (preset == 'google') { groupScope = 'https://www.googleapis.com/auth/cloud-identity.groups.readonly' }
8084 + if (typeof preset != 'string') { groupScope = 'groups' }
8085 + }
8086 + strategy.options.params.scope.push(groupScope)
8087 + parent.authLog('setupDomainAuthStrategy', `OIDC: Groups sync enabled, added group scope to request: ${strategy.options.params.scope}`);
8088 }
8083 - strategy.options.params.scope.push(groupScope)
8089 }
8090 strategy.options.params.scope = strategy.options.params.scope.join(' ')
8091
@@ -8228,8 +8233,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8233 }
8234 }
8235
8231 - // If profile is null/undefined, extract user info from the tokenset
8232 - if (!profile && tokenset && tokenset.id_token) {
8236 + // If profile is null/undefined or roles are requested, extract user info from the tokenset
8237 + if ((!profile || (strategy.custom.authorities.includes('roles')) && !profile.roles) && tokenset && tokenset.id_token) {
8238 try {
8239 // Simple JWT decoder to extract user claims from id_token
8240 const parts = tokenset.id_token.split('.');
@@ -8238,7 +8243,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8243 const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4);
8244 const decoded = JSON.parse(Buffer.from(paddedPayload, 'base64').toString());
8245 if (decoded) {
8241 - profile = decoded;
8246 + if(!profile){
8247 + profile = decoded;
8248 + } else {
8249 + profile.roles = decoded.roles;
8250 + }
8251 }
8252 }
8253 } catch (err) {
@@ -8277,6 +8286,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8286
8287 user.emailVerified = profile && profile.email_verified ? profile.email_verified : obj.common.validateEmail(user.email);
8288 user.groups = profile && obj.common.validateStrArray(profile.groups, 1) ? profile.groups : null;
8289 + user.roles = profile && obj.common.validateStrArray(profile.roles, 1) ? profile.roles : null;
8290 user.preset = obj.common.validateString(strategy.custom.preset) ? strategy.custom.preset : null;
8291 if (strategy.groups && obj.common.validateString(strategy.groups.claim)) {
8292 user.groups = profile && obj.common.validateStrArray(profile[strategy.groups.claim], 1) ? profile[strategy.groups.claim] : null
@@ -8295,16 +8305,29 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8305
8306 // Setup presets and groups, get groups from API if needed then return
8307 if (strategy.groups && typeof user.preset == 'string') {
8298 - getGroups(user.preset, tokenset).then((groups) => {
8299 - user = Object.assign(user, { 'groups': groups });
8300 - done(null, user);
8301 - }).catch((err) => {
8302 - let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
8303 - parent.debug('error', `${JSON.stringify(error)}`);
8304 - parent.authLog('oidcCallback', error.message);
8305 - user.groups = [];
8306 - done(null, user);
8307 - });
8308 + if((Array.isArray(strategy.custom.authorities) && strategy.custom.authorities.filter(x => x.trim().length > 0).length > 0) == false || strategy.custom.authorities.includes('groups')) {
8309 + getGroups(user.preset, tokenset).then((groups) => {
8310 + user = Object.assign(user, { 'groups': groups });
8311 + //done(null, user);
8312 + }).catch((err) => {
8313 + let error = new Error('OIDC: GROUPS: No groups found due to error:', { cause: err });
8314 + parent.debug('error', `${JSON.stringify(error)}`);
8315 + parent.authLog('oidcCallback', error.message);
8316 + user.groups = [];
8317 + //done(null, user);
8318 + });
8319 + }
8320 + if(strategy.custom.authorities.includes('roles')){
8321 + if(user.roles){
8322 + if(!strategy.custom.authorities.includes('groups')){
8323 + user.groups = user.roles;
8324 + } else {
8325 + user.groups = (user.groups || []).concat(user.roles);
8326 + }
8327 + }
8328 + }
8329 + parent.authLog('OIDC: USER GROUPS/ROLES:', user);
8330 + done(null, user);
8331 } else {
8332 done(null, user);
8333 }