Added SAML, JumpCloud support.

Ylian Saint-Hilaire committed May 20, 2020 at 00:39 UTC 5a06c46d3e6991a1884925595b3bacb77c8f42a1
8 files changed +112 -42
public/images/login/generic32.png
Binary files /dev/null and b/public/images/login/generic32.png differ
public/images/login/generic64.png
Binary files /dev/null and b/public/images/login/generic64.png differ
sample-config-advanced.json
+12
@@ -179,6 +179,18 @@
179 "__callbackurl": "https://server/auth-reddit-callback",
180 "clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
181 "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
182 + },
183 + "jumpcloud": {
184 + "__callbackurl": "https://server/auth-jumpcloud-callback",
185 + "entityid": "meshcentral",
186 + "idpurl": "https://sso.jumpcloud.com/saml2/saml2",
187 + "cert": "jumpcloud-saml.pem"
188 + },
189 + "saml": {
190 + "__callbackurl": "https://server/auth-saml-callback",
191 + "entityid": "meshcentral",
192 + "idpurl": "https://server/saml2",
193 + "cert": "saml.pem"
194 }
195 }
196 },
translate/translate.json
+14
@@ -23957,6 +23957,13 @@
23957 "login-mobile.handlebars->container->page_content->column_l->1->1->0->1->loginpanel->1->authStrategies->auth-intel"
23958 ]
23959 },
23960 + {
23961 + "en": "Sign-in using JumpCloud",
23962 + "xloc": [
23963 + "login-mobile.handlebars->container->page_content->column_l->1->1->0->1->loginpanel->1->authStrategies->auth-jumpcloud",
23964 + "login.handlebars->container->column_l->centralTable->1->0->logincell->loginpanel->1->authStrategies->auth-jumpcloud"
23965 + ]
23966 + },
23967 {
23968 "en": "Sign-in using Reddit",
23969 "nl": "Log in met Reddit",
@@ -24044,6 +24051,13 @@
24051 "default.handlebars->25->1035"
24052 ]
24053 },
24054 + {
24055 + "en": "Single Sign-in",
24056 + "xloc": [
24057 + "login-mobile.handlebars->container->page_content->column_l->1->1->0->1->loginpanel->1->authStrategies->auth-saml",
24058 + "login.handlebars->container->column_l->centralTable->1->0->logincell->loginpanel->1->authStrategies->auth-saml"
24059 + ]
24060 + },
24061 {
24062 "cs": "Velikost",
24063 "de": "Größe",
views/default.handlebars
+7 -6
@@ -10880,12 +10880,13 @@
10880
10881 // Add user auth strategy
10882 var shortuserid = user._id.split('/')[2];
10883 - if (shortuserid.startsWith('~twitter:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/twitter64.png';; }
10884 - else if (shortuserid.startsWith('~google:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/google64.png';; }
10885 - else if (shortuserid.startsWith('~github:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/github64.png';; }
10886 - else if (shortuserid.startsWith('~reddit:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/reddit64.png';; }
10887 - else if (shortuserid.startsWith('~jumpcloud:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/jumpcloud64.png';; }
10888 - else if (shortuserid.startsWith('~intel:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/intel64.png';; }
10883 + if (shortuserid.startsWith('~twitter:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/twitter64.png'; }
10884 + else if (shortuserid.startsWith('~google:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/google64.png'; }
10885 + else if (shortuserid.startsWith('~github:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/github64.png'; }
10886 + else if (shortuserid.startsWith('~reddit:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/reddit64.png'; }
10887 + else if (shortuserid.startsWith('~jumpcloud:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/jumpcloud64.png'; }
10888 + else if (shortuserid.startsWith('~intel:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/intel64.png'; }
10889 + else if (shortuserid.startsWith('~:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/generic64.png'; }
10890 else { QV('p30userAuthServiceLogo', false); }
10891
10892 // Server permissions
views/login-mobile.handlebars
+4
@@ -79,7 +79,9 @@
79 <a id="auth-google" href="auth-google" style="display:none"><img src="images/login/google32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Google" /></a>
80 <a id="auth-github" href="auth-github" style="display:none"><img src="images/login/github32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using GitHub" /></a>
81 <a id="auth-reddit" href="auth-reddit" style="display:none"><img src="images/login/reddit32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Reddit" /></a>
82 + <a id="auth-jumpcloud" href="auth-jumpcloud" style="display:none"><img src="images/login/jumpcloud32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using JumpCloud" /></a>
83 <a id="auth-intel" href="auth-intel" style="display:none"><img src="images/login/intel32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Intel" /></a>
84 + <a id="auth-saml" href="auth-saml" style="display:none"><img src="images/login/generic32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Single Sign-in" /></a>
85 </div>
86 </form>
87 </div>
@@ -371,7 +373,9 @@
373 if (authStrategies.indexOf('google') >= 0) { QV('auth-google', true); }
374 if (authStrategies.indexOf('github') >= 0) { QV('auth-github', true); }
375 if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); }
376 + if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); }
377 if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); }
378 + if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); }
379 }
380
381 window.onresize = center;
views/login.handlebars
+3
@@ -78,6 +78,7 @@
78 <a id="auth-github" href="auth-github" style="display:none"><img src="images/login/github32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using GitHub" /></a>
79 <a id="auth-reddit" href="auth-reddit" style="display:none"><img src="images/login/reddit32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using Reddit" /></a>
80 <a id="auth-jumpcloud" href="auth-jumpcloud" style="display:none"><img src="images/login/jumpcloud32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Sign-in using JumpCloud" /></a>
81 + <a id="auth-saml" href="auth-saml" style="display:none"><img src="images/login/generic32.png" loading="lazy" width="32" height="32" style="margin-left:3px;margin-right:3px;border-radius:3px;box-shadow:2px 2px 5px black;cursor:pointer" title="Single Sign-in" /></a>
82 </div>
83 </form>
84 </div>
@@ -386,6 +387,8 @@
387 if (authStrategies.indexOf('github') >= 0) { QV('auth-github', true); }
388 if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); }
389 if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); }
390 + if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); }
391 + if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); }
392 }
393
394 // Display the welcome text
webserver.js
+72 -36
@@ -1783,7 +1783,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1783 if ((req.user.email != null) && (req.user.email != user.email)) { user.email = req.user.email; user.emailVerified = true; userChange = true; }
1784 if (userChange) {
1785 obj.db.SetUser(user);
1786 - // TODO: Event user change
1786 +
1787 + // Event user creation
1788 + var targets = ['*', 'server-users'];
1789 + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msg: 'Account changed', domain: domain.id };
1790 + 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.
1791 + parent.DispatchEvent(targets, obj, event);
1792 }
1793 parent.debug('web', 'handleStrategyLogin: succesful login: ' + userid);
1794 req.session.userid = req.user.id;
@@ -2111,12 +2116,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2116 // See what authentication strategies we have
2117 var authStrategies = [];
2118 if (typeof domain.authstrategies == 'object') {
2114 - if ((typeof domain.authstrategies.twitter == 'object') && (typeof domain.authstrategies.twitter.clientid == 'string') && (typeof domain.authstrategies.twitter.clientsecret == 'string')) { authStrategies.push('twitter'); }
2115 - if ((typeof domain.authstrategies.google == 'object') && (typeof domain.authstrategies.google.clientid == 'string') && (typeof domain.authstrategies.google.clientsecret == 'string')) { authStrategies.push('google'); }
2116 - if ((typeof domain.authstrategies.github == 'object') && (typeof domain.authstrategies.github.clientid == 'string') && (typeof domain.authstrategies.github.clientsecret == 'string')) { authStrategies.push('github'); }
2117 - if ((typeof domain.authstrategies.reddit == 'object') && (typeof domain.authstrategies.reddit.clientid == 'string') && (typeof domain.authstrategies.reddit.clientsecret == 'string')) { authStrategies.push('reddit'); }
2118 - if ((typeof domain.authstrategies.jumpcloud == 'object')) { authStrategies.push('jumpcloud'); }
2119 - if ((typeof domain.authstrategies.intel == 'object') && (typeof domain.authstrategies.intel.clientid == 'string') && (typeof domain.authstrategies.intel.clientsecret == 'string')) { authStrategies.push('intel'); }
2119 + if (typeof domain.authstrategies.twitter == 'object') { authStrategies.push('twitter'); }
2120 + if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
2121 + if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
2122 + if (typeof domain.authstrategies.reddit == 'object') { authStrategies.push('reddit'); }
2123 + if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
2124 + if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
2125 + if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
2126 }
2127
2128 // Render the login page
@@ -4210,40 +4216,70 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4216 }, handleStrategyLogin);
4217 }
4218
4219 + // Generic SAML
4220 + if (typeof domain.authstrategies.saml == 'object') {
4221 + if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {
4222 + console.log('ERROR: Missing SAML configuration.');
4223 + } else {
4224 + var cert = obj.fs.readFileSync(obj.path.join(obj.parent.datapath, domain.authstrategies.saml.cert));
4225 + if (cert == null) {
4226 + console.log('ERROR: Unable to read SAML IdP certificate: ' + domain.authstrategies.saml.cert);
4227 + } else {
4228 + var options = { path: url + 'auth-saml-callback', entryPoint: domain.authstrategies.saml.idpurl, issuer: 'meshcentral' };
4229 + if (typeof domain.authstrategies.saml.entityid == 'string') { options.issuer = domain.authstrategies.saml.entityid; }
4230 + options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
4231 + const SamlStrategy = require('passport-saml').Strategy;
4232 + passport.use(new SamlStrategy(options,
4233 + function (profile, done) {
4234 + if (typeof profile.nameID != 'string') { return done(); }
4235 + var user = { id: 'user/' + domain.id + '/~' + profile.issuer + ':' + profile.nameID, name: profile.nameID };
4236 + if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
4237 + if (typeof profile.email == 'string') { user.email = profile.email; }
4238 + return done(null, user);
4239 + }
4240 + ));
4241 + obj.app.get(url + 'auth-saml', function (req, res, next) {
4242 + domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4243 + });
4244 + obj.app.post(url + 'auth-saml-callback', function (req, res, next) {
4245 + domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4246 + }, handleStrategyLogin);
4247 + }
4248 + }
4249 + }
4250 +
4251 // JumpCloud
4252 if (typeof domain.authstrategies.jumpcloud == 'object') {
4215 - const SamlStrategy = require('passport-saml').Strategy;
4216 -
4217 - var options = {
4218 - path: url + 'auth-jumpcloud-callback',
4219 - entryPoint: domain.authstrategies.jumpcloud.idpurl,
4220 - issuer: 'passport-saml'
4221 - };
4222 -
4223 - if (domain.authstrategies.jumpcloud.cert) {
4253 + if ((typeof domain.authstrategies.jumpcloud.cert != 'string') || (typeof domain.authstrategies.jumpcloud.idpurl != 'string')) {
4254 + console.log('ERROR: Missing JumpCloud configuration.');
4255 + } else {
4256 var cert = obj.fs.readFileSync(obj.path.join(obj.parent.datapath, domain.authstrategies.jumpcloud.cert));
4225 - if (cert != null) { options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join(''); }
4226 - //console.log(options);
4227 - }
4228 -
4229 - passport.use(new SamlStrategy(options,
4230 - function (profile, done) {
4231 - //var user = { id: 'user/' + domain.id + '/~reddit:' + profile.id, name: profile.name };
4232 - //if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
4233 - console.log('JumpCloud Profile', profile);
4234 - var user = { id: 'user/' + domain.id + '/~jumpcloud:' + profile.id, name: profile.name };
4235 - return done(null, user);
4257 + if (cert == null) {
4258 + console.log('ERROR: Unable to read JumpCloud IdP certificate: ' + domain.authstrategies.jumpcloud.cert);
4259 + } else {
4260 + var options = { path: url + 'auth-jumpcloud-callback', entryPoint: domain.authstrategies.jumpcloud.idpurl, issuer: 'meshcentral' };
4261 + if (typeof domain.authstrategies.jumpcloud.entityid == 'string') { options.issuer = domain.authstrategies.jumpcloud.entityid; }
4262 + options.cert = cert.toString().split('-----BEGIN CERTIFICATE-----').join('').split('-----END CERTIFICATE-----').join('');
4263 + const SamlStrategy = require('passport-saml').Strategy;
4264 + passport.use(new SamlStrategy(options,
4265 + function (profile, done) {
4266 + if (typeof profile.nameID != 'string') { return done(); }
4267 + var user = { id: 'user/' + domain.id + '/~' + profile.issuer + ':' + profile.nameID, name: profile.nameID };
4268 + if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
4269 + if (typeof profile.email == 'string') { user.email = profile.email; }
4270 + return done(null, user);
4271 + }
4272 + ));
4273 + obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
4274 + domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4275 + });
4276 + obj.app.post(url + 'auth-jumpcloud-callback', function (req, res, next) {
4277 + domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4278 + }, handleStrategyLogin);
4279 }
4237 - ));
4238 - obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
4239 - console.log('auth-jumpcloud');
4240 - domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4241 - });
4242 - obj.app.get(url + 'auth-jumpcloud-callback', function (req, res, next) {
4243 - console.log('auth-jumpcloud-callback');
4244 - domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4245 - });
4280 + }
4281 }
4282 +
4283 }
4284
4285 // Server redirects