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