diff --git a/meshcentral.js b/meshcentral.js index ba79a458..5b7505fe 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -2556,6 +2556,7 @@ function mainStart() { 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'); } 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'); } 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'); } + 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'); } if ((typeof config.domains[i].authstrategies.saml == 'object') || (typeof config.domains[i].authstrategies.jumpcloud == 'object')) { passport.push('passport-saml'); } } if ((config.domains[i].sessionrecording != null) && (config.domains[i].sessionrecording.index == true)) { recordingIndex = true; } diff --git a/package.json b/package.json index 3484f430..759f2add 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "translate", "readme.txt", "license.txt", - "sample-config.json" + "sample-config.json", + "sample-config-advanced.json" ], "dependencies": { "archiver": "^3.0.0", diff --git a/public/images/login/azure32.png b/public/images/login/azure32.png new file mode 100644 index 00000000..6e85dac3 Binary files /dev/null and b/public/images/login/azure32.png differ diff --git a/public/images/login/azure64.png b/public/images/login/azure64.png new file mode 100644 index 00000000..9a9b86f9 Binary files /dev/null and b/public/images/login/azure64.png differ diff --git a/sample-config-advanced.json b/sample-config-advanced.json index 5e5e7209..17616c2c 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -163,32 +163,45 @@ "__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", "twitter": { "__callbackurl": "https://server/auth-twitter-callback", + "newAccounts": true, "clientid": "xxxxxxxxxxxxxxxxxxxxxxx", "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, "google": { "__callbackurl": "https://server/auth-google-callback", + "newAccounts": true, "clientid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com", "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxx" }, "github": { "__callbackurl": "https://server/auth-github-callback", + "newAccounts": true, "clientid": "xxxxxxxxxxxxxxxxxxxxxxx", "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, "reddit": { "__callbackurl": "https://server/auth-reddit-callback", + "newAccounts": true, "clientid": "xxxxxxxxxxxxxxxxxxxxxxx", "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + "azure": { + "__callbackurl": "https://server/auth-azure-callback", + "newAccounts": true, + "clientid": "00000000-0000-0000-0000-000000000000", + "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "tenantid": "00000000-0000-0000-0000-000000000000" + }, "jumpcloud": { "__callbackurl": "https://server/auth-jumpcloud-callback", + "newAccounts": true, "entityid": "meshcentral", "idpurl": "https://sso.jumpcloud.com/saml2/saml2", "cert": "jumpcloud-saml.pem" }, "saml": { "__callbackurl": "https://server/auth-saml-callback", + "newAccounts": true, "entityid": "meshcentral", "idpurl": "https://server/saml2", "cert": "saml.pem" diff --git a/translate/translate.json b/translate/translate.json index 415d3dc8..e6737a65 100644 --- a/translate/translate.json +++ b/translate/translate.json @@ -24001,6 +24001,13 @@ "default.handlebars->container->column_l->p6->p6info->p2ServerActions->3->p2ServerActionsErrors->0" ] }, + { + "en": "Sign-in using Azure", + "xloc": [ + "login-mobile.handlebars->container->page_content->column_l->1->1->0->1->loginpanel->1->authStrategies->auth-azure", + "login.handlebars->container->column_l->centralTable->1->0->logincell->loginpanel->1->authStrategies->auth-azure" + ] + }, { "en": "Sign-in using GitHub", "nl": "Log in met GitHub", diff --git a/views/default.handlebars b/views/default.handlebars index 90ddfc61..a8473632 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -10883,6 +10883,7 @@ else if (shortuserid.startsWith('~google:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/google64.png'; } else if (shortuserid.startsWith('~github:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/github64.png'; } else if (shortuserid.startsWith('~reddit:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/reddit64.png'; } + else if (shortuserid.startsWith('~azure:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/azure64.png'; } else if (shortuserid.startsWith('~jumpcloud:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/jumpcloud64.png'; } else if (shortuserid.startsWith('~intel:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/intel64.png'; } else if (shortuserid.startsWith('~:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/generic64.png'; } diff --git a/views/login-mobile.handlebars b/views/login-mobile.handlebars index 3e46ac77..961f389d 100644 --- a/views/login-mobile.handlebars +++ b/views/login-mobile.handlebars @@ -79,6 +79,7 @@
'); + } else { + if (req.query.state == req.session.rstate) { + delete req.session.rstate; + domain.passport.authenticate('azure', { failureRedirect: '/' })(req, res, next); + } else { + delete req.session.rstate; + next(new Error(403)); + } + } + }, handleStrategyLogin); + } + // Generic SAML if (typeof domain.authstrategies.saml == 'object') { 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) { var user = { id: 'user/' + domain.id + '/~' + profile.issuer + ':' + profile.nameID, name: profile.nameID }; if ((typeof profile.firstname == 'string') && (typeof profile.lastname == 'string')) { user.name = profile.firstname + ' ' + profile.lastname; } if (typeof profile.email == 'string') { user.email = profile.email; } + if (domain.authstrategies.saml.newaccounts == true) { user.newaccounts = true; } return done(null, user); } )); obj.app.get(url + 'auth-saml', function (req, res, next) { + if (domain.passport == null) { next(); return; } domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next); }); obj.app.post(url + 'auth-saml-callback', function (req, res, next) { + if (domain.passport == null) { next(); return; } domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next); }, handleStrategyLogin); } @@ -4330,10 +4387,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } )); obj.app.get(url + 'auth-intel', function (req, res, next) { + if (domain.passport == null) { next(); return; } domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next); }); obj.app.post(url + 'auth-intel-callback', function (req, res, next) { - console.log('auth-intel-callback'); + if (domain.passport == null) { next(); return; } domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next); }, handleStrategyLogin); } @@ -4363,9 +4421,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } )); obj.app.get(url + 'auth-jumpcloud', function (req, res, next) { + if (domain.passport == null) { next(); return; } domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next); }); obj.app.post(url + 'auth-jumpcloud-callback', function (req, res, next) { + if (domain.passport == null) { next(); return; } domain.passport.authenticate('saml', { failureRedirect: '/', failureFlash: true })(req, res, next); }, handleStrategyLogin); } + @@ -373,6 +374,7 @@ if (authStrategies.indexOf('google') >= 0) { QV('auth-google', true); } if (authStrategies.indexOf('github') >= 0) { QV('auth-github', true); } if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); } + if (authStrategies.indexOf('azure') >= 0) { QV('auth-azure', true); } if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); } if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); } if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); } diff --git a/views/login.handlebars b/views/login.handlebars index 9429b188..efaa6342 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -77,6 +77,7 @@ + @@ -387,6 +388,7 @@ if (authStrategies.indexOf('google') >= 0) { QV('auth-google', true); } if (authStrategies.indexOf('github') >= 0) { QV('auth-github', true); } if (authStrategies.indexOf('reddit') >= 0) { QV('auth-reddit', true); } + if (authStrategies.indexOf('azure') >= 0) { QV('auth-azure', true); } if (authStrategies.indexOf('jumpcloud') >= 0) { QV('auth-jumpcloud', true); } if (authStrategies.indexOf('intel') >= 0) { QV('auth-intel', true); } if (authStrategies.indexOf('saml') >= 0) { QV('auth-saml', true); } diff --git a/webserver.js b/webserver.js index bdbf48bd..c572dace 100644 --- a/webserver.js +++ b/webserver.js @@ -1743,7 +1743,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { const userid = req.user.id; var user = obj.users[userid]; if (user == null) { - if (domain.newaccounts == true) { + if ((domain.newaccounts === true) || (req.user.newaccounts === true)) { // Create the user parent.debug('web', 'handleStrategyLogin: creating new user: ' + userid); 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) { if (typeof domain.authstrategies.google == 'object') { authStrategies.push('google'); } if (typeof domain.authstrategies.github == 'object') { authStrategies.push('github'); } if (typeof domain.authstrategies.reddit == 'object') { authStrategies.push('reddit'); } + if (typeof domain.authstrategies.azure == 'object') { authStrategies.push('azure'); } if (typeof domain.authstrategies.intel == 'object') { authStrategies.push('intel'); } if (typeof domain.authstrategies.jumpcloud == 'object') { authStrategies.push('jumpcloud'); } if (typeof domain.authstrategies.saml == 'object') { authStrategies.push('saml'); } @@ -4194,11 +4195,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function (token, tokenSecret, profile, cb) { var user = { id: 'user/' + domain.id + '/~twitter:' + profile.id, name: profile.displayName }; if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; } + if (domain.authstrategies.twitter.newaccounts == true) { user.newaccounts = true; } return cb(null, user); } )); obj.app.get(url + 'auth-twitter', domain.passport.authenticate('twitter')); obj.app.get(url + 'auth-twitter-callback', function (req, res, next) { + if (domain.passport == null) { next(); return; } if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) { // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack). var url = req.url; @@ -4218,6 +4221,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function (token, tokenSecret, profile, cb) { var user = { id: 'user/' + domain.id + '/~google:' + profile.id, name: profile.displayName }; 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; } + if (domain.authstrategies.google.newaccounts == true) { user.newaccounts = true; } return cb(null, user); } )); @@ -4232,6 +4236,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function (token, tokenSecret, profile, cb) { var user = { id: 'user/' + domain.id + '/~github:' + profile.id, name: profile.displayName }; if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; } + if (domain.authstrategies.github.newaccounts == true) { user.newaccounts = true; } return cb(null, user); } )); @@ -4246,14 +4251,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function (token, tokenSecret, profile, cb) { var user = { id: 'user/' + domain.id + '/~reddit:' + profile.id, name: profile.name }; if ((typeof profile.emails == 'object') && (profile.emails[0] != null) && (typeof profile.emails[0].value == 'string')) { user.email = profile.emails[0].value; } + if (domain.authstrategies.reddit.newaccounts == true) { user.newaccounts = true; } return cb(null, user); } )); obj.app.get(url + 'auth-reddit', function (req, res, next) { + if (domain.passport == null) { next(); return; } req.session.rstate = obj.crypto.randomBytes(32).toString('hex'); domain.passport.authenticate('reddit', { state: req.session.rstate, duration: 'permanent' })(req, res, next); }); obj.app.get(url + 'auth-reddit-callback', function (req, res, next) { + if (domain.passport == null) { next(); return; } if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) { // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack). var url = req.url; @@ -4272,6 +4280,52 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }, handleStrategyLogin); } + // Azure + if ((typeof domain.authstrategies.azure == 'object') && (typeof domain.authstrategies.azure.clientid == 'string') && (typeof domain.authstrategies.azure.clientsecret == 'string')) { + const AzureOAuth2Strategy = require('passport-azure-oauth2'); + passport.use('azure', new AzureOAuth2Strategy({ + clientID: domain.authstrategies.azure.clientid, + clientSecret: domain.authstrategies.azure.clientsecret, + tenant: domain.authstrategies.azure.tenantid, + callbackURL: url + 'auth-azure-callback' + }, + function (accessToken, refreshtoken, params, profile, done) { + var userex = null; + try { userex = require('jwt-simple').decode(params.id_token, "", true); } catch (ex) { } + var user = null; + if (userex != null) { + var user = { id: 'user/' + domain.id + '/~azure:' + userex.unique_name, name: userex.name }; + if (typeof userex.email == 'string') { user.email = userex.email; } + if (domain.authstrategies.azure.newaccounts == true) { user.newaccounts = true; } + } + return done(null, user); + } + )); + obj.app.get(url + 'auth-azure', function (req, res, next) { + if (domain.passport == null) { next(); return; } + req.session.rstate = obj.crypto.randomBytes(32).toString('hex'); + domain.passport.authenticate('azure', { state: req.session.rstate })(req, res, next); + }); + obj.app.get(url + 'auth-azure-callback', function (req, res, next) { + if (domain.passport == null) { next(); return; } + if ((Object.keys(req.session).length == 0) && (req.query.nmr == null)) { + // This is an empty session likely due to the 302 redirection, redirect again (this is a bit of a hack). + var url = req.url; + if (url.indexOf('?') >= 0) { url += '&nmr=1'; } else { url += '?nmr=1'; } // Add this to the URL to prevent redirect loop. + res.set('Content-Type', 'text/html'); + res.end('