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')) {