Added Group Support to OIDC
Supports choosing groups to.. -Allow or restrict login to server -Sync with user groups (with / without filter) -Grant or revoke site admin privileges
mstrhakr committed
Sep 6, 2022 at 00:29 UTC
5619948d31206a5a3d241b08ec0d2087a6705a83
2 files changed
+130
-35
meshcentral.js
+1
-1
@@ -3941,7 +3941,7 @@ function mainStart() {
3941
if ((typeof config.domains[i].authstrategies.github == 'object') && (typeof config.domains[i].authstrategies.github.clientid == 'string') && (typeof config.domains[i].authstrategies.github.clientsecret == 'string') && (passport.indexOf('passport-github2') == -1)) { passport.push('passport-github2'); }
3942
if ((typeof config.domains[i].authstrategies.reddit == 'object') && (typeof config.domains[i].authstrategies.reddit.clientid == 'string') && (typeof config.domains[i].authstrategies.reddit.clientsecret == 'string') && (passport.indexOf('passport-reddit') == -1)) { passport.push('passport-reddit'); }
3943
if ((typeof config.domains[i].authstrategies.azure == 'object') && (typeof config.domains[i].authstrategies.azure.clientid == 'string') && (typeof config.domains[i].authstrategies.azure.clientsecret == 'string') && (typeof config.domains[i].authstrategies.azure.tenantid == 'string') && (passport.indexOf('passport-azure-oauth2') == -1)) { passport.push('passport-azure-oauth2'); passport.push('jwt-simple'); }
3944
- if ((typeof config.domains[i].authstrategies.oidc == 'object') && (typeof config.domains[i].authstrategies.oidc.clientid == 'string') && (typeof config.domains[i].authstrategies.oidc.clientsecret == 'string') && (typeof config.domains[i].authstrategies.oidc.issuer == 'string') && (passport.indexOf('passport-openidconnect') == -1)) { passport.push('passport-openidconnect'); passport.push('openid-client'); }
3944
+ if ((typeof config.domains[i].authstrategies.oidc == 'object') && (typeof config.domains[i].authstrategies.oidc.clientid == 'string') && (typeof config.domains[i].authstrategies.oidc.clientsecret == 'string') && (typeof config.domains[i].authstrategies.oidc.issuer == 'string') && (passport.indexOf('@mstrhakr/passport-openidconnect') == -1)) { passport.push('@mstrhakr/passport-openidconnect'); passport.push('openid-client'); }
3945
if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
3946
}
3947
if (config.domains[i].sessionrecording != null) { sessionRecording = true; }
webserver.js
+129
-34
@@ -2492,9 +2492,70 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2492
// This is called after a succesful Oauth to Twitter, Google, GitHub...
2493
function handleStrategyLogin(req, res) {
2494
const domain = checkUserIpAddress(req, res);
2495
+ const authStrategy = req.user.strategy
2496
if (domain == null) { return; }
2496
- parent.debug('web', 'handleStrategyLogin: ' + JSON.stringify(req.user));
2497
+ parent.debug(authStrategy, 'handleStrategyLogin: ' + JSON.stringify(req.user));
2498
if ((req.user != null) && (req.user.sid != null)) {
2499
+
2500
+ // Check if any group related options exist
2501
+ if (typeof domain.authstrategies[authStrategy].groups !== 'undefined' && domain.authstrategies[authStrategy].groups !== null) {
2502
+ if (typeof req.user.groups !== 'undefined' && req.user.groups !== null) { var userMemberships = req.user.groups; }
2503
+ if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; }
2504
+ if (Array.isArray(userMemberships) == false) { userMemberships = []; }
2505
+ parent.debug(authStrategy, `Groups: Reported User Memberships from IdP: ${userMemberships.join(', ')}`);
2506
+
2507
+ // See if the user is required to be part of a specific group in order to log into this server.
2508
+ if (typeof domain.authstrategies[authStrategy].groups.required !== 'undefined' && domain.authstrategies[authStrategy].groups.required !== null){
2509
+ if (typeof domain.authstrategies[authStrategy].groups.required == 'string') { domain.authstrategies[authStrategy].groups.required = [domain.authstrategies[authStrategy].groups.required]; }
2510
+ if (Array.isArray(domain.authstrategies[authStrategy].groups.required) && (domain.authstrategies[authStrategy].groups.required.length > 0)) {
2511
+ var userMembershipMatch = false;
2512
+ for (var i in domain.authstrategies[authStrategy].groups.required) {
2513
+ if (userMemberships.indexOf(domain.authstrategies[authStrategy].groups.required[i]) >= 0) {
2514
+ userMembershipMatch = true;
2515
+ parent.debug(authStrategy, `Groups: User found in required group: ${domain.authstrategies[authStrategy].groups.required[i]}`);
2516
+ }
2517
+ }
2518
+ if (userMembershipMatch === false) { parent.debug(authStrategy, 'Groups: DENIED - User not found in required group.'); fn('denied'); return;}
2519
+ }
2520
+ }
2521
+
2522
+ // Check if user is in admin group
2523
+ var siteadminGroup = false
2524
+ if (typeof domain.authstrategies[authStrategy].groups.siteadmin !== 'undefined' && domain.authstrategies[authStrategy].groups.siteadmin !== null) {
2525
+ if (typeof domain.authstrategies[authStrategy].groups.siteadmin == 'string') { domain.authstrategies[authStrategy].groups.siteadmin = [domain.authstrategies[authStrategy].groups.siteadmin]; }
2526
+ if (Array.isArray(domain.authstrategies[authStrategy].groups.siteadmin) && (domain.authstrategies[authStrategy].groups.siteadmin.length > 0)) {
2527
+ for (var i in domain.authstrategies[authStrategy].groups.siteadmin) {
2528
+ if (userMemberships.indexOf(domain.authstrategies[authStrategy].groups.siteadmin[i]) >= 0) {
2529
+ siteadminGroup = true;
2530
+ }
2531
+ }
2532
+ }
2533
+ }
2534
+
2535
+ // See if we need to sync user-memberships (IdP) with user-groups (meshcentral)
2536
+ if (domain.authstrategies[authStrategy].groups.sync.enabled === true || domain.authstrategies[authStrategy].groups.sync === true) {
2537
+ if (domain.authstrategies[authStrategy].groups.sync === true) { domain.authstrategies[authStrategy].groups.sync = {"enabled":true}; }
2538
+ if (typeof domain.authstrategies[authStrategy].groups.sync.filter !== 'undefined' && domain.authstrategies[authStrategy].groups.sync.filter !== null) {
2539
+ const filteredMemberships = [];
2540
+ if (typeof domain.authstrategies[authStrategy].groups.sync.filter == 'string') { domain.authstrategies[authStrategy].groups.sync.filter = [domain.authstrategies[authStrategy].groups.sync.filter]; }
2541
+ if (Array.isArray(domain.authstrategies[authStrategy].groups.sync.filter)) {
2542
+ for (var i in userMemberships) {
2543
+ for (var j in domain.authstrategies[authStrategy].groups.sync.filter) {
2544
+ if (userMemberships[i].indexOf(domain.authstrategies[authStrategy].groups.sync.filter[j]) >= 0) { filteredMemberships.push(userMemberships[i]); }
2545
+ }
2546
+ }
2547
+ }
2548
+ if (Array.isArray(filteredMemberships)) {
2549
+ if (filteredMemberships.length > 0) {
2550
+ parent.debug(authStrategy, `Groups: Filtered user memberships from config: ${filteredMemberships.join(', ')}`);
2551
+ }
2552
+ } else {
2553
+ parent.debug(authStrategy, `Groups: No groups found with filter: ${domain.authstrategies[authStrategy].groups.sync.filter.join(', ')}`);
2554
+ }
2555
+ userMemberships = filteredMemberships;
2556
+ }
2557
+ }
2558
+ }
2559
const userid = 'user/' + domain.id + '/' + req.user.sid;
2560
var user = obj.users[userid];
2561
if (user == null) {
@@ -2504,25 +2565,25 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2565
if (domain.newaccounts === true) { newAccountAllowed = true; }
2566
if (obj.common.validateStrArray(domain.newaccountrealms)) { newAccountRealms = domain.newaccountrealms; }
2567
2507
- if ((domain.authstrategies != null) && (domain.authstrategies[req.user.strategy] != null)) {
2508
- if (domain.authstrategies[req.user.strategy].newaccounts === true) { newAccountAllowed = true; }
2509
- if (obj.common.validateStrArray(domain.authstrategies[req.user.strategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2568
+ if ((domain.authstrategies != null) && (domain.authstrategies[authStrategy] != null)) {
2569
+ if (domain.authstrategies[authStrategy].newaccounts === true) { newAccountAllowed = true; }
2570
+ if (obj.common.validateStrArray(domain.authstrategies[authStrategy].newaccountrealms)) { newAccountRealms = domain.authstrategies[req.user.strategy].newaccountrealms; }
2571
}
2572
2573
if (newAccountAllowed === true) {
2574
// Create the user
2514
- parent.debug('web', 'handleStrategyLogin: creating new user: ' + userid);
2575
+ parent.debug(authStrategy, 'handleStrategyLogin: creating new user: ' + userid);
2576
user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), login: Math.floor(Date.now() / 1000), access: Math.floor(Date.now() / 1000), domain: domain.id };
2577
if (req.user.email != null) { user.email = req.user.email; user.emailVerified = true; }
2578
if (domain.newaccountsrights) { user.siteadmin = domain.newaccountsrights; } // New accounts automatically assigned server rights.
2518
- if (domain.authstrategies[req.user.strategy].newaccountsrights) { user.siteadmin = obj.common.meshServerRightsArrayToNumber(domain.authstrategies[req.user.strategy].newaccountsrights); } // If there are specific SSO server rights, use these instead.
2579
+ if (domain.authstrategies[authStrategy].newaccountsrights) { user.siteadmin = obj.common.meshServerRightsArrayToNumber(domain.authstrategies[req.user.strategy].newaccountsrights); } // If there are specific SSO server rights, use these instead.
2580
if (newAccountRealms) { user.groups = newAccountRealms; } // New accounts automatically part of some groups (Realms).
2581
obj.users[userid] = user;
2582
2583
// Auto-join any user groups
2584
var newaccountsusergroups = null;
2585
if (typeof domain.newaccountsusergroups == 'object') { newaccountsusergroups = domain.newaccountsusergroups; }
2525
- if (typeof domain.authstrategies[req.user.strategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2586
+ if (typeof domain.authstrategies[authStrategy].newaccountsusergroups == 'object') { newaccountsusergroups = domain.authstrategies[req.user.strategy].newaccountsusergroups; }
2587
if (newaccountsusergroups) {
2588
for (var i in newaccountsusergroups) {
2589
var ugrpid = newaccountsusergroups[i];
@@ -2545,6 +2606,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2606
}
2607
}
2608
2609
+
2610
+ // Sync the user groups if enabled
2611
+ if (domain.authstrategies[authStrategy].groups.sync.enabled === true) { syncExternalUserGroups(domain, user, userMemberships, authStrategy) }
2612
+
2613
+ // See if the user is a member of the site admin group.
2614
+ if (typeof domain.authstrategies[authStrategy].groups.siteadmin !== 'undefined' && domain.authstrategies[authStrategy].groups.siteadmin !== null)
2615
+ if (siteadminGroup === true && user.siteadmin != 4294967295) {
2616
+ parent.debug(authStrategy, `Groups: GRANTING ADMIN PRIVLEGE - User found in admin group: ${domain.authstrategies[authStrategy].groups.siteadmin[i]}`);
2617
+ user.siteadmin = 4294967295;
2618
+ }
2619
+
2620
// Save the user
2621
obj.db.SetUser(user);
2622
@@ -2565,18 +2637,36 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2637
obj.parent.DispatchEvent(targets, obj, loginEvent);
2638
} else {
2639
// New users not allowed
2568
- parent.debug('web', 'handleStrategyLogin: Can\'t create new accounts');
2640
+ parent.debug(authStrategy, 'handleStrategyLogin: Can\'t create new accounts');
2641
req.session.loginmode = 1;
2642
req.session.messageid = 100; // Unable to create account.
2643
res.redirect(domain.url + getQueryPortion(req));
2644
return;
2645
}
2574
- } else {
2575
- // Login success
2576
- var userChange = false;
2577
- if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChange = true; }
2578
- if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChange = true; }
2579
- if (userChange) {
2646
+ } else { // Login success
2647
+ // Check for basic changes
2648
+ var userChanged = false;
2649
+ if ((req.user.name != null) && (req.user.name != user.name)) { user.name = req.user.name; userChanged = true; }
2650
+ if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChanged = true; }
2651
+
2652
+ // Sync the user groups if enabled
2653
+ if (domain.authstrategies[authStrategy].groups.sync.enabled === true) { syncExternalUserGroups(domain, user, userMemberships, authStrategy) }
2654
+
2655
+ // See if the user is a member of the site admin group.
2656
+ if (typeof domain.authstrategies[authStrategy].groups.siteadmin !== 'undefined' && domain.authstrategies[authStrategy].groups.siteadmin !== null) {
2657
+ if (siteadminGroup === true && user.siteadmin != 4294967295) {
2658
+ parent.debug(authStrategy, 'Groups: GRANTING ADMIN PRIVLEGE - User found in admin group.');
2659
+ user.siteadmin = 4294967295;
2660
+ userChanged = true;
2661
+ } else if (siteadminGroup === false && user.siteadmin == 4294967295) {
2662
+ parent.debug(authStrategy, 'Groups: REVOKING ADMIN PRIVLEGE - User NOT found in admin group!');
2663
+ user.siteadmin = null;
2664
+ userChanged = true;
2665
+ }
2666
+ }
2667
+
2668
+ // Update db record for user if there are changes detected
2669
+ if (userChanged) {
2670
obj.db.SetUser(user);
2671
2672
// Event user change
@@ -2585,7 +2675,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2675
if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
2676
parent.DispatchEvent(targets, obj, event);
2677
}
2588
- parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid);
2678
+ parent.debug(authStrategy, 'handleStrategyLogin: succesful login: ' + userid);
2679
req.session.userid = userid;
2680
setSessionRandom(req);
2681
@@ -2596,7 +2686,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2686
const loginEvent = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'login', msgid: 107, msgArgs: [req.clientIp, ua.browserStr, ua.osStr], msg: 'Account login', domain: domain.id, ip: req.clientIp, userAgent: req.headers['user-agent'], twoFactorType: 'sso' };
2687
obj.parent.DispatchEvent(targets, obj, loginEvent);
2688
}
2599
- }
2689
+ } else { parent.debug('warn', 'handleStrategyLogin: FAILED - No user'); }
2690
//res.redirect(domain.url); // This does not handle cookie correctly.
2691
res.set('Content-Type', 'text/html');
2692
res.end('<html><head><meta http-equiv="refresh" content=0;url="' + domain.url + '"></head><body></body></html>');
@@ -6895,33 +6985,38 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
6985
6986
// Generic OpenID Connect
6987
if ((typeof domain.authstrategies.oidc == 'object') && (typeof domain.authstrategies.oidc.clientid == 'string') && (typeof domain.authstrategies.oidc.clientsecret == 'string') && (typeof domain.authstrategies.oidc.issuer == 'string')) {
6898
- const OIDCStrategy = require('passport-openidconnect');
6988
+ const OIDCStrategy = require('@mstrhakr/passport-openidconnect');
6989
var options = {
6990
issuer: domain.authstrategies.oidc.issuer,
6991
clientID: domain.authstrategies.oidc.clientid,
6992
clientSecret: domain.authstrategies.oidc.clientsecret,
6903
- scope: ['profile email'],
6993
+ scope: ['profile', 'email', 'groups'],
6994
};
6995
async function discoverOptions(options){
6996
if ((typeof domain.authstrategies.oidc.authorizationurl != 'string') || (typeof domain.authstrategies.oidc.tokenurl != 'string') || (typeof domain.authstrategies.oidc.userinfourl != 'string')) {
6997
const Issuer = require('openid-client').Issuer;
6908
- parent.debug('web', 'Attempting to discover well known endpoints for ' + options.issuer);
6998
+ parent.debug('oidc', 'Attempting to discover well known endpoints for ' + options.issuer);
6999
var issuer = await Issuer.discover(options.issuer)
7000
if (typeof domain.authstrategies.oidc.authorizationurl == 'string') { options.authorizationURL = domain.authstrategies.oidc.authorizationurl; } else { options.authorizationURL = issuer.metadata.authorization_endpoint; }
7001
if (typeof domain.authstrategies.oidc.tokenurl == 'string') { options.tokenURL = domain.authstrategies.oidc.tokenurl; } else { options.tokenURL = issuer.metadata.token_endpoint; }
7002
if (typeof domain.authstrategies.oidc.userinfourl == 'string') { options.userInfoURL = domain.authstrategies.oidc.userinfourl; } else { options.userInfoURL = issuer.metadata.userinfo_endpoint; }
7003
if (typeof domain.authstrategies.oidc.callbackurl == 'string') { options.callbackURL = domain.authstrategies.oidc.callbackurl; } else { options.callbackURL = url + 'oidc-callback'; }
6914
- }
6915
- parent.debug('web', 'Discovered ' + JSON.stringify(options));
6916
- return options
6917
- }
6918
- discoverOptions(options).then((options)=>{passport.use('oidc-' + domain.id, new OIDCStrategy.Strategy(options,
6919
- function verify(issuer, profile, verified) {
6920
- var user = { sid: '~oidc:' + profile.id, name: profile.displayName, email: profile.email, strategy: 'oidc' };
6921
- parent.debug('AUTH', `OIDC: Configured user: ${JSON.stringify(user)} using ${issuer}`);
6922
- return verified(null, user);
6923
- }
6924
- ))});
7004
+ parent.debug('oidc', 'Discovered ' + JSON.stringify(options));
7005
+ }
7006
+ return options;
7007
+ }
7008
+ discoverOptions(options).then((options)=>{
7009
+ passport.use('oidc-' + domain.id, new OIDCStrategy.Strategy(options,
7010
+ function verify(issuer, profile, verified) {
7011
+ parent.debug('oidc', 'Connecting to ' + issuer + ' with the following options ' + JSON.stringify(options));
7012
+ var user = { sid: '~oidc:' + profile.id, name: profile.displayName, strategy: 'oidc' };
7013
+ if ( Array.isArray(profile.emails[0].value) ) { user.email = profile.emails[0].value[0]; } else { user.email = profile.emails[0].value; }
7014
+ if ( Array.isArray(profile.groups[0].value) ) { user.groups = profile.groups[0].value; } else { user.groups = [profile.groups[0].value]; }
7015
+ parent.debug('oidc', `Configured: User: ${JSON.stringify(user)} FROM Profile: ${JSON.stringify(profile)}`);
7016
+ return verified(null, user);
7017
+ }
7018
+ ))
7019
+ });
7020
authStrategyFlags |= domainAuthStrategyConsts.oidc;
7021
}
7022
@@ -8821,7 +8916,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8916
function syncExternalUserGroups(domain, user, userMemberships, userMembershipType) {
8917
var userChanged = false;
8918
if (user.links == null) { user.links = {}; }
8824
-
8919
+ var authType = userMembershipType
8920
// Create a user of memberships for this user that type
8921
var existingUserMemberships = {};
8922
for (var i in user.links) {
@@ -8835,7 +8930,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8930
var ugrp = obj.userGroups[ugrpid];
8931
if (ugrp == null) {
8932
// This user group does not exist, create it
8838
- parent.debug('ldap', 'Creating new LDAP user group ' + userMemberships[i] + '.');
8933
+ parent.debug(authType, 'Creating new ' + authType + ' user group ' + userMemberships[i] + '.');
8934
ugrp = { type: 'ugrp', _id: ugrpid, name: membership, domain: domain.id, membershipType: userMembershipType, links: {} };
8935
8936
// Save the new group
@@ -8852,7 +8947,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8947
8948
if (existingUserMemberships[ugrpid] == null) {
8949
// This user is not part of the user group, add it.
8855
- parent.debug('ldap', 'Adding ' + user.name + ' to LDAP user group ' + userMemberships[i] + '.');
8950
+ parent.debug(authType, 'Adding ' + user.name + ' to ' + authType + ' user group ' + userMemberships[i] + '.');
8951
if (user.links == null) { user.links = {}; }
8952
user.links[ugrp._id] = { rights: 1 };
8953
userChanged = true;
@@ -8882,7 +8977,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
8977
// Remove the user from any memberships they don't belong to anymore
8978
for (var ugrpid in existingUserMemberships) {
8979
var ugrp = obj.userGroups[ugrpid];
8885
- parent.debug('ldap', 'Removing ' + user.name + ' from LDAP user group ' + ugrp.name + '.');
8980
+ parent.debug(authType, 'Removing ' + user.name + ' from ' + authType + ' user group ' + ugrp.name + '.');
8981
if ((user.links != null) && (user.links[ugrpid] != null)) {
8982
delete user.links[ugrpid];
8983