Added support for Azure Active Directory.
Ylian Saint-Hilaire committed
May 24, 2020 at 00:04 UTC
1922e6c958ecce2c147ddff0eeaa994d9117c983
10 files changed
+90
-3
meshcentral.js
+1
@@ -2556,6 +2556,7 @@ function mainStart() {
2556
if ((typeof config.domains[i].authstrategies.google == 'object') && (typeof config.domains[i].authstrategies.google.clientid == 'string') && (typeof config.domains[i].authstrategies.google.clientsecret == 'string') && (passport.indexOf('passport-google-oauth20') == -1)) { passport.push('passport-google-oauth20'); }
2557
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'); }
2558
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'); }
2559
+ 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'); }
2560
if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); }
2561
}
2562
if ((config.domains[i].sessionrecording != null) && (config.domains[i].sessionrecording.index == true)) { recordingIndex = true; }
package.json
+2
-1
@@ -26,7 +26,8 @@
26
"translate",
27
"readme.txt",
28
"license.txt",
29
- "sample-config.json"
29
+ "sample-config.json",
30
+ "sample-config-advanced.json"
31
],
32
"dependencies": {
33
"archiver": "^3.0.0",
public/images/login/azure32.png
Binary files /dev/null and b/public/images/login/azure32.png differ
public/images/login/azure64.png
Binary files /dev/null and b/public/images/login/azure64.png differ
sample-config-advanced.json
+13
@@ -163,32 +163,45 @@
163
"__comment__" : "This section is used to allow users to login using other accounts. You will need to get an API key from the services and register callback URL's",
164
"twitter": {
165
"__callbackurl": "https://server/auth-twitter-callback",
166
+ "newAccounts": true,
167
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
168
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
169
},
170
"google": {
171
"__callbackurl": "https://server/auth-google-callback",
172
+ "newAccounts": true,
173
"clientid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
174
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxx"
175
},
176
"github": {
177
"__callbackurl": "https://server/auth-github-callback",
178
+ "newAccounts": true,
179
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
180
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
181
},
182
"reddit": {
183
"__callbackurl": "https://server/auth-reddit-callback",
184
+ "newAccounts": true,
185
"clientid": "xxxxxxxxxxxxxxxxxxxxxxx",
186
"clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
187
},
188
+ "azure": {
189
+ "__callbackurl": "https://server/auth-azure-callback",
190
+ "newAccounts": true,
191
+ "clientid": "00000000-0000-0000-0000-000000000000",
192
+ "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
193
+ "tenantid": "00000000-0000-0000-0000-000000000000"
194
+ },
195
"jumpcloud": {
196
"__callbackurl": "https://server/auth-jumpcloud-callback",
197
+ "newAccounts": true,
198
"entityid": "meshcentral",
199
"idpurl": "https://sso.jumpcloud.com/saml2/saml2",
200
"cert": "jumpcloud-saml.pem"
201
},
202
"saml": {
203
"__callbackurl": "https://server/auth-saml-callback",
204
+ "newAccounts": true,
205
"entityid": "meshcentral",
206
"idpurl": "https://server/saml2",
207
"cert": "saml.pem"
translate/translate.json
+7
@@ -24001,6 +24001,13 @@
24001
"default.handlebars->container->column_l->p6->p6info->p2ServerActions->3->p2ServerActionsErrors->0"
24002
]
24003
},
24004
+ {
24005
+ "en": "Sign-in using Azure",
24006
+ "xloc": [
24007
+ "login-mobile.handlebars->container->page_content->column_l->1->1->0->1->loginpanel->1->authStrategies->auth-azure",
24008
+ "login.handlebars->container->column_l->centralTable->1->0->logincell->loginpanel->1->authStrategies->auth-azure"
24009
+ ]
24010
+ },
24011
{
24012
"en": "Sign-in using GitHub",
24013
"nl": "Log in met GitHub",
views/default.handlebars
+1
@@ -10883,6 +10883,7 @@
10883
else if (shortuserid.startsWith('~google:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/google64.png'; }
10884
else if (shortuserid.startsWith('~github:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/github64.png'; }
10885
else if (shortuserid.startsWith('~reddit:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/reddit64.png'; }
10886
+ else if (shortuserid.startsWith('~azure:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/azure64.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'; }
views/login-mobile.handlebars
+2
@@ -79,6 +79,7 @@
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-azure" href="auth-azure" style="display:none"><img src="images/login/azure32.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 Azure" /></a>
83
<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>
84
<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>
85
<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>
@@ -373,6 +374,7 @@
374
if (authStrategies.indexOf('google') >= 0) { QV('auth-google', true); }
375
if (authStrategies.indexOf('github') >= 0) { QV('auth-github', true); }
376
if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); }
377
+ if (authStrategies.indexOf('azure') >= 0) { QV('auth-azure', true); }
378
if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); }
379
if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); }
380
if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); }
views/login.handlebars
+2
@@ -77,6 +77,7 @@
77
<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>
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-azure" href="auth-azure" style="display:none"><img src="images/login/azure32.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 Azure" /></a>
81
<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>
82
<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>
83
<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>
@@ -387,6 +388,7 @@
388
if (authStrategies.indexOf('google') >= 0) { QV('auth-google', true); }
389
if (authStrategies.indexOf('github') >= 0) { QV('auth-github', true); }
390
if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); }
391
+ if (authStrategies.indexOf('azure') >= 0) { QV('auth-azure', true); }
392
if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); }
393
if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); }
394
if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); }
webserver.js
+62
-2
@@ -1743,7 +1743,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
1743
const userid = req.user.id;
1744
var user = obj.users[userid];
1745
if (user == null) {
1746
- if (domain.newaccounts == true) {
1746
+ if ((domain.newaccounts === true) || (req.user.newaccounts === true)) {
1747
// Create the user
1748
parent.debug('web', 'handleStrategyLogin: creating new user: ' + userid);
1749
user = { type: 'user', _id: userid, name: req.user.name, email: req.user.email, creation: Math.floor(Date.now() / 1000), domain: domain.id };
@@ -2111,6 +2111,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
2111
if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); }
2112
if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); }
2113
if (typeof domain.authstrategies.reddit == 'object') { authStrategies.push('reddit'); }
2114
+ if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); }
2115
if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); }
2116
if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); }
2117
if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); }
@@ -4194,11 +4195,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4195
function (token, tokenSecret, profile, cb) {
4196
var user = { id: 'user/' + domain.id + '/~twitter:' + profile.id, name: profile.displayName };
4197
if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
4198
+ if (domain.authstrategies.twitter.newaccounts == true) { user.newaccounts = true; }
4199
return cb(null, user);
4200
}
4201
));
4202
obj.app.get(url + 'auth-twitter', domain.passport.authenticate('twitter'));
4203
obj.app.get(url + 'auth-twitter-callback', function (req, res, next) {
4204
+ if (domain.passport == null) { next(); return; }
4205
if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
4206
// This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
4207
var url = req.url;
@@ -4218,6 +4221,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4221
function (token, tokenSecret, profile, cb) {
4222
var user = { id: 'user/' + domain.id + '/~google:' + profile.id, name: profile.displayName };
4223
if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string') && (profile.emails[0].verified == true)) { user.email = profile.emails[0].value; }
4224
+ if (domain.authstrategies.google.newaccounts == true) { user.newaccounts = true; }
4225
return cb(null, user);
4226
}
4227
));
@@ -4232,6 +4236,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4236
function (token, tokenSecret, profile, cb) {
4237
var user = { id: 'user/' + domain.id + '/~github:' + profile.id, name: profile.displayName };
4238
if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
4239
+ if (domain.authstrategies.github.newaccounts == true) { user.newaccounts = true; }
4240
return cb(null, user);
4241
}
4242
));
@@ -4246,14 +4251,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4251
function (token, tokenSecret, profile, cb) {
4252
var user = { id: 'user/' + domain.id + '/~reddit:' + profile.id, name: profile.name };
4253
if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; }
4254
+ if (domain.authstrategies.reddit.newaccounts == true) { user.newaccounts = true; }
4255
return cb(null, user);
4256
}
4257
));
4258
obj.app.get(url + 'auth-reddit', function (req, res, next) {
4259
+ if (domain.passport == null) { next(); return; }
4260
req.session.rstate = obj.crypto.randomBytes(32).toString('hex');
4261
domain.passport.authenticate('reddit', { state: req.session.rstate, duration: 'permanent' })(req, res, next);
4262
});
4263
obj.app.get(url + 'auth-reddit-callback', function (req, res, next) {
4264
+ if (domain.passport == null) { next(); return; }
4265
if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
4266
// This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
4267
var url = req.url;
@@ -4272,6 +4280,52 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4280
}, handleStrategyLogin);
4281
}
4282
4283
+ // Azure
4284
+ if ((typeof domain.authstrategies.azure == 'object') && (typeof domain.authstrategies.azure.clientid == 'string') && (typeof domain.authstrategies.azure.clientsecret == 'string')) {
4285
+ const AzureOAuth2Strategy = require('passport-azure-oauth2');
4286
+ passport.use('azure', new AzureOAuth2Strategy({
4287
+ clientID: domain.authstrategies.azure.clientid,
4288
+ clientSecret: domain.authstrategies.azure.clientsecret,
4289
+ tenant: domain.authstrategies.azure.tenantid,
4290
+ callbackURL: url + 'auth-azure-callback'
4291
+ },
4292
+ function (accessToken, refreshtoken, params, profile, done) {
4293
+ var userex = null;
4294
+ try { userex = require('jwt-simple').decode(params.id_token, "", true); } catch (ex) { }
4295
+ var user = null;
4296
+ if (userex != null) {
4297
+ var user = { id: 'user/' + domain.id + '/~azure:' + userex.unique_name, name: userex.name };
4298
+ if (typeof userex.email == 'string') { user.email = userex.email; }
4299
+ if (domain.authstrategies.azure.newaccounts == true) { user.newaccounts = true; }
4300
+ }
4301
+ return done(null, user);
4302
+ }
4303
+ ));
4304
+ obj.app.get(url + 'auth-azure', function (req, res, next) {
4305
+ if (domain.passport == null) { next(); return; }
4306
+ req.session.rstate = obj.crypto.randomBytes(32).toString('hex');
4307
+ domain.passport.authenticate('azure', { state: req.session.rstate })(req, res, next);
4308
+ });
4309
+ obj.app.get(url + 'auth-azure-callback', function (req, res, next) {
4310
+ if (domain.passport == null) { next(); return; }
4311
+ if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) {
4312
+ // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack).
4313
+ var url = req.url;
4314
+ if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop.
4315
+ res.set('Content-Type', 'text/html');
4316
+ res.end('<html><head><meta http-equiv="refresh" content=0;url="' + url + '"></head><body></body></html>');
4317
+ } else {
4318
+ if (req.query.state == req.session.rstate) {
4319
+ delete req.session.rstate;
4320
+ domain.passport.authenticate('azure', { failureRedirect: '/' })(req, res, next);
4321
+ } else {
4322
+ delete req.session.rstate;
4323
+ next(new Error(403));
4324
+ }
4325
+ }
4326
+ }, handleStrategyLogin);
4327
+ }
4328
+
4329
// Generic SAML
4330
if (typeof domain.authstrategies.saml == 'object') {
4331
if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {
@@ -4291,13 +4345,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4345
var user = { id: 'user/' + domain.id + '/~' + profile.issuer + ':' + profile.nameID, name: profile.nameID };
4346
if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; }
4347
if (typeof profile.email == 'string') { user.email = profile.email; }
4348
+ if (domain.authstrategies.saml.newaccounts == true) { user.newaccounts = true; }
4349
return done(null, user);
4350
}
4351
));
4352
obj.app.get(url + 'auth-saml', function (req, res, next) {
4353
+ if (domain.passport == null) { next(); return; }
4354
domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4355
});
4356
obj.app.post(url + 'auth-saml-callback', function (req, res, next) {
4357
+ if (domain.passport == null) { next(); return; }
4358
domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4359
}, handleStrategyLogin);
4360
}
@@ -4330,10 +4387,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4387
}
4388
));
4389
obj.app.get(url + 'auth-intel', function (req, res, next) {
4390
+ if (domain.passport == null) { next(); return; }
4391
domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4392
});
4393
obj.app.post(url + 'auth-intel-callback', function (req, res, next) {
4336
- console.log('auth-intel-callback');
4394
+ if (domain.passport == null) { next(); return; }
4395
domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4396
}, handleStrategyLogin);
4397
}
@@ -4363,9 +4421,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
4421
}
4422
));
4423
obj.app.get(url + 'auth-jumpcloud', function (req, res, next) {
4424
+ if (domain.passport == null) { next(); return; }
4425
domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4426
});
4427
obj.app.post(url + 'auth-jumpcloud-callback', function (req, res, next) {
4428
+ if (domain.passport == null) { next(); return; }
4429
domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next);
4430
}, handleStrategyLogin);
4431
}