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

346 lines
9.1 KiB

10 years ago
/* globals RoutePolicy, SAML */
if (!Accounts.saml) {
Accounts.saml = {
settings: {
debug: true,
generateUsername: false,
providers: []
}
};
}
var Fiber = Npm.require('fibers');
var connect = Npm.require('connect');
RoutePolicy.declare('/_saml/', 'network');
Meteor.methods({
samlLogout: function (provider) {
// Make sure the user is logged in before initiate SAML SLO
if (!Meteor.userId()) {
10 years ago
throw new Meteor.Error('not-authorized');
}
var samlProvider = function (element) {
10 years ago
return (element.provider === provider);
};
var providerConfig = Accounts.saml.settings.providers.filter(samlProvider)[0];
10 years ago
if (Accounts.saml.settings.debug) {
10 years ago
console.log('Logout request from ' + JSON.stringify(providerConfig));
}
// This query should respect upcoming array of SAML logins
var user = Meteor.users.findOne({
_id: Meteor.userId(),
10 years ago
'services.saml.provider': provider
}, {
10 years ago
'services.saml': 1
});
var nameID = user.services.saml.nameID;
10 years ago
var sessionIndex = user.services.saml.idpSession;
nameID = sessionIndex;
10 years ago
if (Accounts.saml.settings.debug) {
10 years ago
console.log('NameID for user ' + Meteor.userId() + ' found: ' + JSON.stringify(nameID));
}
10 years ago
var _saml = new SAML(providerConfig);
var request = _saml.generateLogoutRequest({
nameID: nameID,
sessionIndex: 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
}
});
var _syncRequestToUrl = Meteor.wrapAsync(_saml.requestToUrl, _saml);
10 years ago
var result = _syncRequestToUrl(request.request, 'logout');
10 years ago
if (Accounts.saml.settings.debug) {
10 years ago
console.log('SAML Logout Request ' + result);
}
return result;
}
10 years ago
});
Accounts.registerLoginHandler(function (loginRequest) {
if (!loginRequest.saml || !loginRequest.credentialToken) {
return undefined;
}
var loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
10 years ago
if (Accounts.saml.settings.debug) {
10 years ago
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) {
var user = Meteor.users.findOne({
'emails.address': loginResult.profile.email
});
if (!user) {
var 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) {
var username = RocketChat.generateUsernameSuggestion(newUser);
if (username) {
newUser.username = username;
}
}
var userId = Accounts.insertUserDoc({}, newUser);
user = Meteor.users.findOne(userId);
}
//creating the token and adding to the user
var stampedToken = Accounts._generateStampedLoginToken();
Meteor.users.update(user, {
$push: {
'services.resume.loginTokens': stampedToken
}
});
var 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
var 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.
var result = Accounts.saml._loginResultForCredentialToken[credentialToken];
delete Accounts.saml._loginResultForCredentialToken[credentialToken];
return result;
10 years ago
};
// Listen to incoming SAML http requests
WebApp.connectHandlers.use(connect.bodyParser()).use(function (req, res, next) {
// Need to create a Fiber since we're using synchronous http calls and nothing
// else is wrapping this in a fiber automatically
Fiber(function () {
middleware(req, res, next);
}).run();
});
middleware = function (req, res, next) {
// Make sure to catch any exceptions because otherwise we'd crash
// the runner
try {
var 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);
10 years ago
var 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);
}
var _saml;
switch (samlObject.actionName) {
10 years ago
case 'metadata':
_saml = new SAML(service);
10 years ago
service.callbackUrl = Meteor.absoluteUrl('_saml/validate/' + service.provider);
res.writeHead(200);
res.write(_saml.generateServiceProviderMetadata(service.callbackUrl));
res.end();
//closePopup(res);
break;
10 years ago
case 'logout':
// This is where we receive SAML LogoutResponse
_saml = new SAML(service);
_saml.validateLogoutResponse(req.query.SAMLResponse, function (err, result) {
if (!err) {
var logOutUser = function (inResponseTo) {
10 years ago
if (Accounts.saml.settings.debug) {
10 years ago
console.log('Logging Out user via inResponseTo ' + inResponseTo);
}
var loggedOutUser = Meteor.users.find({
'services.saml.inResponseTo': inResponseTo
}).fetch();
10 years ago
if (loggedOutUser.length === 1) {
10 years ago
if (Accounts.saml.settings.debug) {
10 years ago
console.log('Found user ' + loggedOutUser[0]._id);
}
Meteor.users.update({
_id: loggedOutUser[0]._id
}, {
$set: {
10 years ago
'services.resume.loginTokens': []
}
});
Meteor.users.update({
_id: loggedOutUser[0]._id
}, {
$unset: {
10 years ago
'services.saml': ''
}
});
} else {
10 years ago
throw new Meteor.error('Found multiple users matching SAML inResponseTo fields');
}
10 years ago
};
Fiber(function () {
logOutUser(result);
}).run();
res.writeHead(302, {
'Location': req.query.RelayState
});
res.end();
}
10 years ago
// else {
// // TBD thinking of sth meaning full.
// }
});
break;
10 years ago
case 'sloRedirect':
var idpLogout = req.query.redirect;
res.writeHead(302, {
// credentialToken here is the SAML LogOut Request that we'll send back to IDP
'Location': idpLogout
});
res.end();
break;
10 years ago
case 'authorize':
service.callbackUrl = Meteor.absoluteUrl('_saml/validate/' + service.provider);
service.id = samlObject.credentialToken;
_saml = new SAML(service);
_saml.getAuthorizeUrl(req, function (err, url) {
10 years ago
if (err) {
throw new Error('Unable to generate authorize url');
}
res.writeHead(302, {
'Location': url
});
res.end();
});
break;
10 years ago
case 'validate':
_saml = new SAML(service);
Accounts.saml.RelayState = req.body.RelayState;
10 years ago
_saml.validateResponse(req.body.SAMLResponse, req.body.RelayState, function (err, profile/*, loggedOut*/) {
if (err) {
throw new Error('Unable to validate response url: ' + err);
}
var credentialToken = profile.inResponseToId || profile.InResponseTo || samlObject.credentialToken;
10 years ago
if (!credentialToken) {
throw new Error('Unable to determine credentialToken');
}
Accounts.saml._loginResultForCredentialToken[credentialToken] = {
profile: profile
};
closePopup(res);
});
break;
default:
10 years ago
throw new Error('Unexpected SAML action ' + samlObject.actionName);
}
} catch (err) {
closePopup(res, err);
}
};
var samlUrlToObject = function (url) {
10 years ago
// req.url will be '/_saml/<action>/<service name>/<credentialToken>'
if (!url) {
return null;
10 years ago
}
var splitPath = url.split('/');
// Any non-saml request will continue down the default
// middlewares.
10 years ago
if (splitPath[1] !== '_saml') {
return null;
10 years ago
}
var result = {
actionName: splitPath[2],
serviceName: splitPath[3],
credentialToken: splitPath[4]
};
10 years ago
if (Accounts.saml.settings.debug) {
console.log(result);
}
return result;
};
var closePopup = function (res, err) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
var 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');
};