The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Rocket.Chat/packages/meteor-accounts-saml/saml_server.js

370 lines
10 KiB

10 years ago
/* globals RoutePolicy, SAML */
/* jshint newcap: false */
10 years ago
if (!Accounts.saml) {
Accounts.saml = {
settings: {
debug: true,
generateUsername: false,
providers: []
}
};
}
const fiber = Npm.require('fibers');
const connect = Npm.require('connect');
RoutePolicy.declare('/_saml/', 'network');
/**
* Fetch SAML provider configs for given 'provider'.
*/
function getSamlProviderConfig(provider) {
if (! provider) {
throw new Meteor.Error('no-saml-provider',
'SAML internal error',
{ method: 'getSamlProviderConfig' });
}
const samlProvider = function(element) {
return (element.provider === provider);
};
return Accounts.saml.settings.providers.filter(samlProvider)[0];
}
Meteor.methods({
samlLogout(provider) {
// Make sure the user is logged in before initiate SAML SLO
if (!Meteor.userId()) {
Change meteor error (#2969) * Add function to handle errors * Delete message errors * handle error for hideRoom * Allow returning error instead of calling toastr.error * Handle error for leaveRoom * handle error for openRoom * handleError for toggleFavorite * handleError in updateMessage * error for samlLogout * handleError for assets * Add global handleError to eslint * handleError for addOAuthService * handleError: getUserRoles * handleError: insertOrUpdateUsere * handleError: messageDeleting * handleError: removeUserFromRoles * handleError: addPermissionToRole * handleError: addUserToRole * handleError: deleteRole * handleError: removeRoleFromPermission * handleError: removeUserFromRole * handleError: saveRole * Return ready on publish without permission * handleError: channel-settings * handleError: mailMessages * handleError: fileUpload * handleError: rocketchat-importer * handleError: addIncomingIntegration * handleError: deleteIncomingIntegration * handleError: updateIncomingIntegration * handleError: addOutgoingIntegration * handleError: deleteOutgoingIntegration * handleError: updateOutgoingIntegration * Return ready on publish without permission * handleError ldap * remove throw from client code * handleError: setEmail, slashCommand * Sort en.i18n.json * Google translated languages * Use correct error return from publishes * RateLimiter.limitFunction * Fix order of error "500" * handleError validateEmailDomain * handleError channelSettings; settings * handleError livechat * handleError: Mailer.sendMail * handleError pinMessage and unpinMessage * handleError messageStarring * handleError oauth apps * handleError: saveNotificationSettings * handleError getRoomRoles * handleError: createDirectMessage * handleError saveUserPreferences * handleError: saveUserProfile * handleError sendConfirmationEmail * Add ecmascript to root
10 years ago
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'samlLogout' });
}
const providerConfig = getSamlProviderConfig(provider);
10 years ago
if (Accounts.saml.settings.debug) {
console.log(`Logout request from ${ JSON.stringify(providerConfig) }`);
}
// This query should respect upcoming array of SAML logins
const user = Meteor.users.findOne({
_id: Meteor.userId(),
10 years ago
'services.saml.provider': provider
}, {
10 years ago
'services.saml': 1
});
let nameID = user.services.saml.nameID;
const sessionIndex = user.services.saml.idpSession;
10 years ago
nameID = sessionIndex;
10 years ago
if (Accounts.saml.settings.debug) {
console.log(`NameID for user ${ Meteor.userId() } found: ${ JSON.stringify(nameID) }`);
}
const _saml = new SAML(providerConfig);
const request = _saml.generateLogoutRequest({
nameID,
sessionIndex
});
// request.request: actual XML SAML Request
// request.id: comminucation id which will be mentioned in the ResponseTo field of SAMLResponse
Meteor.users.update({
_id: Meteor.userId()
}, {
$set: {
'services.saml.inResponseTo': request.id
}
});
const _syncRequestToUrl = Meteor.wrapAsync(_saml.requestToUrl, _saml);
const result = _syncRequestToUrl(request.request, 'logout');
10 years ago
if (Accounts.saml.settings.debug) {
console.log(`SAML Logout Request ${ result }`);
}
return result;
}
10 years ago
});
Accounts.registerLoginHandler(function(loginRequest) {
if (!loginRequest.saml || !loginRequest.credentialToken) {
return undefined;
}
const loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
10 years ago
if (Accounts.saml.settings.debug) {
console.log(`RESULT :${ JSON.stringify(loginResult) }`);
}
10 years ago
if (loginResult === undefined) {
return {
10 years ago
type: 'saml',
error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'No matching login attempt found')
};
}
if (loginResult && loginResult.profile && loginResult.profile.email) {
let user = Meteor.users.findOne({
'emails.address': loginResult.profile.email
});
if (!user) {
const newUser = {
name: loginResult.profile.cn || loginResult.profile.username,
active: true,
globalRoles: ['user'],
emails: [{
address: loginResult.profile.email,
verified: true
}]
};
if (Accounts.saml.settings.generateUsername === true) {
const username = RocketChat.generateUsernameSuggestion(newUser);
if (username) {
newUser.username = username;
}
9 years ago
} else if (loginResult.profile.username) {
newUser.username = loginResult.profile.username;
}
const userId = Accounts.insertUserDoc({}, newUser);
user = Meteor.users.findOne(userId);
}
//creating the token and adding to the user
const stampedToken = Accounts._generateStampedLoginToken();
Meteor.users.update(user, {
$push: {
'services.resume.loginTokens': stampedToken
}
});
const samlLogin = {
provider: Accounts.saml.RelayState,
idp: loginResult.profile.issuer,
idpSession: loginResult.profile.sessionIndex,
nameID: loginResult.profile.nameID
};
Meteor.users.update({
_id: user._id
}, {
$set: {
// TBD this should be pushed, otherwise we're only able to SSO into a single IDP at a time
'services.saml': samlLogin
}
});
//sending token along with the userId
const result = {
userId: user._id,
token: stampedToken.token
};
10 years ago
return result;
} else {
10 years ago
throw new Error('SAML Profile did not contain an email address');
}
});
Accounts.saml._loginResultForCredentialToken = {};
Accounts.saml.hasCredential = function(credentialToken) {
return _.has(Accounts.saml._loginResultForCredentialToken, credentialToken);
10 years ago
};
Accounts.saml.retrieveCredential = function(credentialToken) {
// The credentialToken in all these functions corresponds to SAMLs inResponseTo field and is mandatory to check.
const result = Accounts.saml._loginResultForCredentialToken[credentialToken];
delete Accounts.saml._loginResultForCredentialToken[credentialToken];
return result;
10 years ago
};
const closePopup = function(res, err) {
10 years ago
res.writeHead(200, {
'Content-Type': 'text/html'
});
let content = '<html><head><script>window.close()</script></head><body><H1>Verified</H1></body></html>';
10 years ago
if (err) {
content = `<html><body><h2>Sorry, an annoying error occured</h2><div>${ err }</div><a onclick="window.close();">Close Window</a></body></html>`;
10 years ago
}
res.end(content, 'utf-8');
};
const samlUrlToObject = function(url) {
10 years ago
// req.url will be '/_saml/<action>/<service name>/<credentialToken>'
if (!url) {
return null;
}
const splitUrl = url.split('?');
const splitPath = splitUrl[0].split('/');
10 years ago
// Any non-saml request will continue down the default
// middlewares.
if (splitPath[1] !== '_saml') {
return null;
}
const result = {
10 years ago
actionName: splitPath[2],
serviceName: splitPath[3],
credentialToken: splitPath[4]
};
if (Accounts.saml.settings.debug) {
console.log(result);
}
return result;
};
const middleware = function(req, res, next) {
// Make sure to catch any exceptions because otherwise we'd crash
// the runner
try {
const samlObject = samlUrlToObject(req.url);
if (!samlObject || !samlObject.serviceName) {
next();
return;
}
10 years ago
if (!samlObject.actionName) {
throw new Error('Missing SAML action');
}
10 years ago
console.log(Accounts.saml.settings.providers);
console.log(samlObject.serviceName);
const service = _.find(Accounts.saml.settings.providers, function(samlSetting) {
return samlSetting.provider === samlObject.serviceName;
});
// Skip everything if there's no service set by the saml middleware
10 years ago
if (!service) {
throw new Error(`Unexpected SAML service ${ samlObject.serviceName }`);
10 years ago
}
let _saml;
switch (samlObject.actionName) {
case 'metadata':
_saml = new SAML(service);
service.callbackUrl = Meteor.absoluteUrl(`_saml/validate/${ service.provider }`);
res.writeHead(200);
res.write(_saml.generateServiceProviderMetadata(service.callbackUrl));
res.end();
//closePopup(res);
break;
case 'logout':
// This is where we receive SAML LogoutResponse
_saml = new SAML(service);
_saml.validateLogoutResponse(req.query.SAMLResponse, function(err, result) {
if (!err) {
const logOutUser = function(inResponseTo) {
10 years ago
if (Accounts.saml.settings.debug) {
console.log(`Logging Out user via inResponseTo ${ inResponseTo }`);
}
const loggedOutUser = Meteor.users.find({
'services.saml.inResponseTo': inResponseTo
}).fetch();
if (loggedOutUser.length === 1) {
if (Accounts.saml.settings.debug) {
console.log(`Found user ${ loggedOutUser[0]._id }`);
}
Meteor.users.update({
_id: loggedOutUser[0]._id
}, {
$set: {
'services.resume.loginTokens': []
}
});
Meteor.users.update({
_id: loggedOutUser[0]._id
}, {
$unset: {
'services.saml': ''
}
});
} else {
throw new Meteor.Error('Found multiple users matching SAML inResponseTo fields');
}
};
fiber(function() {
logOutUser(result);
}).run();
res.writeHead(302, {
'Location': req.query.RelayState
});
res.end();
}
// else {
// // TBD thinking of sth meaning full.
// }
});
break;
case 'sloRedirect':
res.writeHead(302, {
// credentialToken here is the SAML LogOut Request that we'll send back to IDP
'Location': req.query.redirect
});
res.end();
break;
case 'authorize':
service.callbackUrl = Meteor.absoluteUrl(`_saml/validate/${ service.provider }`);
service.id = samlObject.credentialToken;
_saml = new SAML(service);
_saml.getAuthorizeUrl(req, function(err, url) {
if (err) {
throw new Error('Unable to generate authorize url');
}
res.writeHead(302, {
'Location': url
});
res.end();
});
break;
case 'validate':
_saml = new SAML(service);
Accounts.saml.RelayState = req.body.RelayState;
_saml.validateResponse(req.body.SAMLResponse, req.body.RelayState, function(err, profile/*, loggedOut*/) {
if (err) {
throw new Error(`Unable to validate response url: ${ err }`);
}
const credentialToken = profile.inResponseToId || profile.InResponseTo || samlObject.credentialToken;
if (!credentialToken) {
// No credentialToken in IdP-initiated SSO
const saml_idp_credentialToken = Random.id();
Accounts.saml._loginResultForCredentialToken[saml_idp_credentialToken] = {
profile
};
const url = `${ Meteor.absoluteUrl('home') }?saml_idp_credentialToken=${ saml_idp_credentialToken }`;
res.writeHead(302, {
'Location': url
});
res.end();
} else {
Accounts.saml._loginResultForCredentialToken[credentialToken] = {
profile
};
closePopup(res);
}
});
break;
default:
throw new Error(`Unexpected SAML action ${ samlObject.actionName }`);
}
} catch (err) {
closePopup(res, err);
}
};
10 years ago
// Listen to incoming SAML http requests
WebApp.connectHandlers.use(connect.bodyParser()).use(function(req, res, next) {
10 years ago
// Need to create a fiber since we're using synchronous http calls and nothing
// else is wrapping this in a fiber automatically
fiber(function() {
10 years ago
middleware(req, res, next);
}).run();
});