diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 13901fe2..b226c32e 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -1069,6 +1069,21 @@ "logouturl": {"type": "string", "format": "uri", "description": "Then set, the user will be redirected to this URL when hitting the logout link."} }, "required": [ "entityid", "idpurl", "cert" ] + }, + "oidc": { + "type": "object", + "properties": { + "authorizationURL": { "type": "string", "format": "uri", "description": "If set, this will be used as the authorization URL. (If set tokenURL and userInfoURL need set also)" }, + "callbackURL": { "type": "string", "format": "uri", "description": "Required, this is the URL that your SSO provider sends auth approval to." }, + "clientid": { "type": "string" }, + "clientsecret": { "type": "string" }, + "issuer": { "type": "string", "format": "uri", "description": "Full URL of SSO portal" }, + "tokenURL": { "type": "string", "format": "uri", "description": "If set, this will be used as the token URL. (If set authorizationURL and userInfoURL need set also)" }, + "userInfoURL": { "type": "string", "format": "uri", "description": "If set, this will be used as the user info URL. (If set authorizationURL and tokenURL need set also)" }, + "logouturl": { "type": "string", "format": "uri", "description": "Then set, the user will be redirected to this URL when hitting the logout link." }, + "newAccounts": { "type": "boolean", "default": true } + }, + "required": [ "issuer", "clientid", "clientsecret", "callbackURL" ] } } } diff --git a/meshcentral.js b/meshcentral.js index f69e0faf..2c5179b6 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -3495,6 +3495,7 @@ function mainStart() { 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.oidc == 'object') && (typeof config.domains[i].authstrategies.oidc.clientid == 'string') && (typeof config.domains[i].authstrategies.oidc.clientsecret == 'string') && (passport.indexOf('@mstrhakr/passport-generic-oidc') == -1)) { passport.push('@mstrhakr/passport-generic-oidc'); } 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) { sessionRecording = true; } diff --git a/public/images/Login/oidc32.png b/public/images/Login/oidc32.png new file mode 100644 index 00000000..63891fb8 Binary files /dev/null and b/public/images/Login/oidc32.png differ diff --git a/public/images/Login/oidc64.png b/public/images/Login/oidc64.png new file mode 100644 index 00000000..ef8607ab Binary files /dev/null and b/public/images/Login/oidc64.png differ diff --git a/sample-config-advanced.json b/sample-config-advanced.json index 34693586..0d9df467 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -138,7 +138,7 @@ "footer": "Default page footer", "newAccounts": false }, - "_domains": { + "domains": { "": { "_siteStyle": 2, "title": "MyServer", @@ -250,21 +250,21 @@ "files": "{0} started a remote files session." }, "_agentCustomization": { - "displayName": "Compagny® Product™", - "description": "Compagny® Product™ agent for remote monitoring, management and assistance.", - "companyName": "Compagny", - "serviceName": "compagnyagent", + "displayName": "Company® Productâ„¢", + "description": "Company® Productâ„¢ agent for remote monitoring, management and assistance.", + "companyName": "Company®", + "serviceName": "companyagent", "image": "agent-logo.png", "fileName": "compagnyagent" }, "_assistantCustomization": { - "title": "Compagny® Product™", + "title": "Company® Productâ„¢", "image": "assistant-logo.png", "fileName": "compagny" }, "_androidCustomization": { - "title": "Compagny® Product™", - "subtitle": "Product Subtitle™", + "title": "Company® Productâ„¢", + "subtitle": "Product Subtitleâ„¢", "image": "assistant-logo.png" }, "_userAllowedIP": "127.0.0.1,192.168.1.0/24", @@ -407,6 +407,17 @@ "entityid": "meshcentral", "idpurl": "https://server/saml2", "cert": "saml.pem" + }, + "oidc": { + "authorizationURL": "https://sso.server.com/api/oidc/authorization", + "callbackURL": "https://mesh.server.com/oidc-callback", + "clientid": "00000000-0000-0000-0000-000000000000", + "clientsecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "issuer": "https://sso.server.com", + "tokenURL": "https://sso.server.com/api/oidc/token", + "userInfoURL": "https://sso.server.com/api/oidc/userinfo", + "logoutURL": "https://sso.server.com/logout", + "newAccounts": true } } }, diff --git a/views/default.handlebars b/views/default.handlebars index b521fc03..6b2ce17b 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -14852,6 +14852,7 @@ 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('~oidc:')) { QV('p30userAuthServiceLogo', true); Q('p30userAuthServiceLogo').src = 'images/login/oidc64.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 e11f9f7b..d2d1dcf7 100644 --- a/views/login-mobile.handlebars +++ b/views/login-mobile.handlebars @@ -90,6 +90,7 @@ + @@ -387,6 +388,7 @@ 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('oidc') >= 0) { QV('auth-oidc', 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 8fedea2f..ab41e945 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -82,6 +82,7 @@ + @@ -408,6 +409,7 @@ 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('oidc') >= 0) { QV('auth-oidc', 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/login2.handlebars b/views/login2.handlebars index 63b44c78..969db815 100644 --- a/views/login2.handlebars +++ b/views/login2.handlebars @@ -104,6 +104,7 @@ + @@ -473,6 +474,7 @@ 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('oidc') >= 0) { QV('auth-oidc', 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 12974a6b..8c4393c8 100644 --- a/webserver.js +++ b/webserver.js @@ -783,6 +783,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (u.startsWith('~github:') && (domain.authstrategies.github != null) && (typeof domain.authstrategies.github.logouturl == 'string')) { res.redirect(domain.authstrategies.github.logouturl); return; } if (u.startsWith('~reddit:') && (domain.authstrategies.reddit != null) && (typeof domain.authstrategies.reddit.logouturl == 'string')) { res.redirect(domain.authstrategies.reddit.logouturl); return; } if (u.startsWith('~azure:') && (domain.authstrategies.azure != null) && (typeof domain.authstrategies.azure.logouturl == 'string')) { res.redirect(domain.authstrategies.azure.logouturl); return; } + if (u.startsWith('~oidc:') && (domain.authstrategies.oidc != null) && (typeof domain.authstrategies.oidc.logouturl == 'string')) { res.redirect(domain.authstrategies.oidc.logouturl); return; } if (u.startsWith('~jumpcloud:') && (domain.authstrategies.jumpcloud != null) && (typeof domain.authstrategies.jumpcloud.logouturl == 'string')) { res.redirect(domain.authstrategies.jumpcloud.logouturl); return; } if (u.startsWith('~saml:') && (domain.authstrategies.saml != null) && (typeof domain.authstrategies.saml.logouturl == 'string')) { res.redirect(domain.authstrategies.saml.logouturl); return; } if (u.startsWith('~intel:') && (domain.authstrategies.intel != null) && (typeof domain.authstrategies.intel.logouturl == 'string')) { res.redirect(domain.authstrategies.intel.logouturl); return; } @@ -3008,6 +3009,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF 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.oidc == 'object') { authStrategies.push('oidc'); } 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'); } @@ -6239,6 +6241,35 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF }, handleStrategyLogin); } + // Generic OpenID Connect + if ((typeof domain.authstrategies.oidc == 'object') && (typeof domain.authstrategies.oidc.clientid == 'string') && (typeof domain.authstrategies.oidc.clientsecret == 'string') && (typeof domain.authstrategies.oidc.issuer == 'string')) { + var options = { + authorizationURL: domain.authstrategies.oidc.authorizationurl, + callbackURL: domain.authstrategies.oidc.callbackurl, + clientID: domain.authstrategies.oidc.clientid, + clientSecret: domain.authstrategies.oidc.clientsecret, + issuer: domain.authstrategies.oidc.issuer, + tokenURL: domain.authstrategies.oidc.tokenurl, + userInfoURL: domain.authstrategies.oidc.userinfourl, + scope: [ 'openid profile email' ], + responseMode: 'form_post' , + state: true + }; + const OIDCStrategy = require('@mstrhakr/passport-generic-oidc'); + if (typeof domain.authstrategies.oidc.callbackurl == 'string') { options.callbackURL = domain.authstrategies.oidc.callbackurl; } else { options.callbackURL = url + 'oidc-callback'; } + parent.debug('web', 'Adding Generic OIDC SSO with options: ' + JSON.stringify(options)); + passport.use('openidconnect', new OIDCStrategy.Strategy(options, + function verify( iss, sub, profile, cb ) { + var user = { sid: '~oidc:' + profile.id, name: profile.displayName, email: profile.email, strategy: 'oidc' }; + parent.debug('AUTH', 'OIDC: Configured user: ' + JSON.stringify(user)); + return cb(null, user); + } + )); + obj.app.get(url + 'auth-oidc', domain.passport.authenticate('openidconnect')); + obj.app.get(url + 'oidc-callback', domain.passport.authenticate('openidconnect', { failureRedirect: '/login?failed-auth-attempt', failureFlash: true }), handleStrategyLogin); + } + + // Generic SAML if (typeof domain.authstrategies.saml == 'object') { if ((typeof domain.authstrategies.saml.cert != 'string') || (typeof domain.authstrategies.saml.idpurl != 'string')) {