More jsHint fixes

pull/2654/head
Rodrigo Nascimento 10 years ago
parent fc1fd30276
commit 7fe6a57cc8
  1. 18
      .jshintignore
  2. 62
      .jshintrc
  3. 28
      packages/meteor-accounts-saml/saml_client.js
  4. 129
      packages/meteor-accounts-saml/saml_server.js
  5. 222
      packages/meteor-accounts-saml/saml_utils.js
  6. 101
      packages/rocketchat-cas/cas_client.js
  7. 104
      packages/rocketchat-cas/cas_rocketchat.js
  8. 255
      packages/rocketchat-cas/cas_server.js
  9. 11
      packages/rocketchat-cas/package.js

@ -0,0 +1,18 @@
packages/rocketchat-livechat/
public/recorderWorker.js
lib/ua-parser.min.js
private/moment-locales/
packages/autoupdate/
packages/meteor-streams/
packages/rocketchat-migrations/
packages/rocketchat-katex/client/lib/katex.min.js
packages/rocketchat-favico/favico.js
packages/rocketchat-theme/client/minicolors/jquery.minicolors.js
packages/rocketchat-emojione/generateEmojiIndex.js
packages/rocketchat-ui/lib/Modernizr.js
packages/rocketchat-ui/lib/clipboardjs/clipboard.js
packages/rocketchat-ui/lib/jquery.swipebox.min.js
packages/rocketchat-ui/lib/particles.js
packages/rocketchat-ui/lib/recorderjs/recorder.js
packages/rocketchat-ui/lib/textarea-autogrow.js

@ -81,35 +81,37 @@
// Custom Globals
"globals" : {
"_" : true,
"Assets" : true,
"SHA256" : true,
"Accounts" : true,
"Blaze" : true,
"Email" : true,
"check" : true,
"crypto" : true,
"EJSON" : true,
"FlowRouter" : true,
"BlazeLayout" : true,
"Meteor" : true,
"Npm" : true,
"Package" : true,
"Promise" : true, // Avoid "redefinition of Promise" warning
"Random" : true,
"ReactiveVar" : true,
"RocketChat" : true,
"RocketChatFile" : true,
"RocketChatFileAvatarInstance" : true,
"s" : true,
"Session" : true,
"swal" : true,
"t" : true,
"TAPi18n" : true,
"Template" : true,
"TimeSync" : true,
"toastr" : true,
"Logger" : true,
"Tracker" : true
"_" : true,
"Accounts" : true,
"Assets" : true,
"Blaze" : true,
"BlazeLayout" : true,
"check" : true,
"crypto" : true,
"EJSON" : true,
"Email" : true,
"FlowRouter" : true,
"Logger" : true,
"Meteor" : true,
"Npm" : true,
"Package" : true,
"Promise" : true, // Avoid "redefinition of Promise" warning
"Random" : true,
"ReactiveVar" : true,
"RocketChat" : true,
"RocketChatFile" : true,
"RocketChatFileAvatarInstance": true,
"s" : true,
"ServiceConfiguration" : true,
"Session" : true,
"SHA256" : true,
"swal" : true,
"t" : true,
"TAPi18n" : true,
"Template" : true,
"TimeSync" : true,
"toastr" : true,
"Tracker" : true,
"WebApp" : true
}
}

@ -5,18 +5,19 @@ if (!Accounts.saml) {
Accounts.saml.initiateLogin = function (options, callback, dimensions) {
// default dimensions that worked well for facebook and google
var popup = openCenteredPopup(
Meteor.absoluteUrl("_saml/authorize/" + options.provider + "/" + options.credentialToken), (dimensions && dimensions.width) || 650, (dimensions && dimensions.height) || 500);
Meteor.absoluteUrl('_saml/authorize/' + options.provider + '/' + options.credentialToken), (dimensions && dimensions.width) || 650, (dimensions && dimensions.height) || 500);
var checkPopupOpen = setInterval(function () {
var popupClosed;
try {
// Fix for #328 - added a second test criteria (popup.closed === undefined)
// to humour this Android quirk:
// http://code.google.com/p/android/issues/detail?id=21061
var popupClosed = popup.closed || popup.closed === undefined;
popupClosed = popup.closed || popup.closed === undefined;
} catch (e) {
// For some unknown reason, IE9 (and others?) sometimes (when
// the popup closes too quickly?) throws "SCRIPT16386: No such
// interface supported" when trying to read 'popup.closed'. Try
// the popup closes too quickly?) throws 'SCRIPT16386: No such
// interface supported' when trying to read 'popup.closed'. Try
// again in 100ms.
return;
}
@ -44,8 +45,9 @@ var openCenteredPopup = function (url, width, height) {
',left=' + left + ',top=' + top + ',scrollbars=yes');
var newwindow = window.open(url, 'Login', features);
if (newwindow.focus)
if (newwindow.focus) {
newwindow.focus();
}
return newwindow;
};
@ -54,7 +56,7 @@ Meteor.loginWithSaml = function (options, callback) {
var credentialToken = Random.id();
options.credentialToken = credentialToken;
Accounts.saml.initiateLogin(options, function (error, result) {
Accounts.saml.initiateLogin(options, function (/*error, result*/) {
Accounts.callLoginMethod({
methodArguments: [{
saml: true,
@ -65,12 +67,12 @@ Meteor.loginWithSaml = function (options, callback) {
});
};
Meteor.logoutWithSaml = function (options, callback) {
//Accounts.saml.idpInitiatedSLO(options, callback);
Meteor.call("samlLogout", options.provider, function (err, result) {
console.log("LOC " + result);
Meteor.logoutWithSaml = function (options/*, callback*/) {
//Accounts.saml.idpInitiatedSLO(options, callback);
Meteor.call('samlLogout', options.provider, function (err, result) {
console.log('LOC ' + result);
// A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server.
//window.location.replace(Meteor.absoluteUrl("_saml/sloRedirect/" + options.provider + "/?redirect="+result));
window.location.replace(Meteor.absoluteUrl("_saml/sloRedirect/" + options.provider + "/?redirect="+encodeURIComponent(result)));
//window.location.replace(Meteor.absoluteUrl('_saml/sloRedirect/' + options.provider + '/?redirect='+result));
window.location.replace(Meteor.absoluteUrl('_saml/sloRedirect/' + options.provider + '/?redirect='+encodeURIComponent(result)));
});
};
};

@ -1,3 +1,5 @@
/* globals RoutePolicy, SAML */
if (!Accounts.saml) {
Accounts.saml = {
settings: {
@ -16,30 +18,31 @@ Meteor.methods({
samlLogout: function (provider) {
// Make sure the user is logged in before initiate SAML SLO
if (!Meteor.userId()) {
throw new Meteor.Error("not-authorized");
throw new Meteor.Error('not-authorized');
}
var samlProvider = function (element) {
return (element.provider == provider)
}
providerConfig = Accounts.saml.settings.providers.filter(samlProvider)[0];
return (element.provider === provider);
};
var providerConfig = Accounts.saml.settings.providers.filter(samlProvider)[0];
if (Accounts.saml.settings.debug) {
console.log("Logout request from " + JSON.stringify(providerConfig));
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(),
"services.saml.provider": provider
'services.saml.provider': provider
}, {
"services.saml": 1
'services.saml': 1
});
var nameID = user.services.saml.nameID;
var sessionIndex = nameID = user.services.saml.idpSession;
var sessionIndex = user.services.saml.idpSession;
nameID = sessionIndex;
if (Accounts.saml.settings.debug) {
console.log("NameID for user " + Meteor.userId() + " found: " + JSON.stringify(nameID));
console.log('NameID for user ' + Meteor.userId() + ' found: ' + JSON.stringify(nameID));
}
_saml = new SAML(providerConfig);
var _saml = new SAML(providerConfig);
var request = _saml.generateLogoutRequest({
nameID: nameID,
@ -58,15 +61,15 @@ Meteor.methods({
});
var _syncRequestToUrl = Meteor.wrapAsync(_saml.requestToUrl, _saml);
var result = _syncRequestToUrl(request.request, "logout");
var result = _syncRequestToUrl(request.request, 'logout');
if (Accounts.saml.settings.debug) {
console.log("SAML Logout Request " + result);
console.log('SAML Logout Request ' + result);
}
return result;
}
})
});
Accounts.registerLoginHandler(function (loginRequest) {
if (!loginRequest.saml || !loginRequest.credentialToken) {
@ -75,14 +78,14 @@ Accounts.registerLoginHandler(function (loginRequest) {
var loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
if (Accounts.saml.settings.debug) {
console.log("RESULT :" + JSON.stringify(loginResult));
console.log('RESULT :' + JSON.stringify(loginResult));
}
if (loginResult == undefined) {
if (loginResult === undefined) {
return {
type: "saml",
error: new Meteor.Error(Accounts.LoginCancelledError.numericError, "No matching login attempt found")
}
type: 'saml',
error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'No matching login attempt found')
};
}
if (loginResult && loginResult.profile && loginResult.profile.email) {
@ -142,10 +145,10 @@ Accounts.registerLoginHandler(function (loginRequest) {
token: stampedToken.token
};
return result
return result;
} else {
throw new Error("SAML Profile did not contain an email address");
throw new Error('SAML Profile did not contain an email address');
}
});
@ -153,14 +156,14 @@ Accounts.saml._loginResultForCredentialToken = {};
Accounts.saml.hasCredential = function (credentialToken) {
return _.has(Accounts.saml._loginResultForCredentialToken, credentialToken);
}
};
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;
}
};
// Listen to incoming SAML http requests
@ -182,61 +185,64 @@ middleware = function (req, res, next) {
return;
}
if (!samlObject.actionName)
throw new Error("Missing SAML action");
if (!samlObject.actionName) {
throw new Error('Missing SAML action');
}
console.log(Accounts.saml.settings.providers)
console.log(samlObject.serviceName)
console.log(Accounts.saml.settings.providers);
console.log(samlObject.serviceName);
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
if (!service)
throw new Error("Unexpected SAML service " + samlObject.serviceName);
if (!service) {
throw new Error('Unexpected SAML service ' + samlObject.serviceName);
}
var _saml;
switch (samlObject.actionName) {
case "metadata":
case 'metadata':
_saml = new SAML(service);
service.callbackUrl = Meteor.absoluteUrl("_saml/validate/" + service.provider);
service.callbackUrl = Meteor.absoluteUrl('_saml/validate/' + service.provider);
res.writeHead(200);
res.write(_saml.generateServiceProviderMetadata(service.callbackUrl));
res.end();
//closePopup(res);
break;
case "logout":
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) {
if (Accounts.saml.settings.debug) {
console.log("Logging Out user via inResponseTo " + inResponseTo);
console.log('Logging Out user via inResponseTo ' + inResponseTo);
}
var loggedOutUser = Meteor.users.find({
'services.saml.inResponseTo': inResponseTo
}).fetch();
if (loggedOutUser.length == 1) {
if (loggedOutUser.length === 1) {
if (Accounts.saml.settings.debug) {
console.log("Found user " + loggedOutUser[0]._id);
console.log('Found user ' + loggedOutUser[0]._id);
}
Meteor.users.update({
_id: loggedOutUser[0]._id
}, {
$set: {
"services.resume.loginTokens": []
'services.resume.loginTokens': []
}
});
Meteor.users.update({
_id: loggedOutUser[0]._id
}, {
$unset: {
"services.saml": ""
'services.saml': ''
}
});
} else {
throw new Meteor.error("Found multiple users matching SAML inResponseTo fields");
throw new Meteor.error('Found multiple users matching SAML inResponseTo fields');
}
}
};
Fiber(function () {
logOutUser(result);
@ -247,42 +253,46 @@ middleware = function (req, res, next) {
'Location': req.query.RelayState
});
res.end();
} else {
// TBD thinking of sth meaning full.
}
})
// else {
// // TBD thinking of sth meaning full.
// }
});
break;
case "sloRedirect":
var idpLogout = req.query.redirect
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;
case "authorize":
service.callbackUrl = Meteor.absoluteUrl("_saml/validate/" + service.provider);
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");
if (err) {
throw new Error('Unable to generate authorize url');
}
res.writeHead(302, {
'Location': url
});
res.end();
});
break;
case "validate":
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);
_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;
if (!credentialToken)
throw new Error("Unable to determine credentialToken");
if (!credentialToken) {
throw new Error('Unable to determine credentialToken');
}
Accounts.saml._loginResultForCredentialToken[credentialToken] = {
profile: profile
};
@ -290,7 +300,7 @@ middleware = function (req, res, next) {
});
break;
default:
throw new Error("Unexpected SAML action " + samlObject.actionName);
throw new Error('Unexpected SAML action ' + samlObject.actionName);
}
} catch (err) {
@ -299,16 +309,18 @@ middleware = function (req, res, next) {
};
var samlUrlToObject = function (url) {
// req.url will be "/_saml/<action>/<service name>/<credentialToken>"
if (!url)
// req.url will be '/_saml/<action>/<service name>/<credentialToken>'
if (!url) {
return null;
}
var splitPath = url.split('/');
// Any non-saml request will continue down the default
// middlewares.
if (splitPath[1] !== '_saml')
if (splitPath[1] !== '_saml') {
return null;
}
var result = {
actionName: splitPath[2],
@ -326,7 +338,8 @@ var closePopup = function (res, err) {
'Content-Type': 'text/html'
});
var content = '<html><head><script>window.close()</script></head><body><H1>Verified</H1></body></html>';
if (err)
if (err) {
content = '<html><body><h2>Sorry, an annoying error occured</h2><div>' + err + '</div><a onclick="window.close();">Close Window</a></body></html>';
}
res.end(content, 'utf-8');
};

@ -1,3 +1,5 @@
/* globals SAML:true */
var zlib = Npm.require('zlib');
var xml2js = Npm.require('xml2js');
var xmlCrypto = Npm.require('xml-crypto');
@ -5,20 +7,20 @@ var crypto = Npm.require('crypto');
var xmldom = Npm.require('xmldom');
var querystring = Npm.require('querystring');
var xmlbuilder = Npm.require('xmlbuilder');
var xmlenc = Npm.require('xml-encryption');
var xpath = xmlCrypto.xpath;
var Dom = xmldom.DOMParser;
// var xmlenc = Npm.require('xml-encryption');
// var xpath = xmlCrypto.xpath;
// var Dom = xmldom.DOMParser;
var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
// var prefixMatch = new RegExp(/(?!xmlns)^.*:/);
SAML = function (options) {
this.options = this.initialize(options);
};
var stripPrefix = function (str) {
return str.replace(prefixMatch, '');
};
// var stripPrefix = function (str) {
// return str.replace(prefixMatch, '');
// };
SAML.prototype.initialize = function (options) {
if (!options) {
@ -38,19 +40,19 @@ SAML.prototype.initialize = function (options) {
}
if (options.identifierFormat === undefined) {
options.identifierFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
options.identifierFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress';
}
if (options.authnContext === undefined) {
options.authnContext = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport";
options.authnContext = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport';
}
return options;
};
SAML.prototype.generateUniqueID = function () {
var chars = "abcdef0123456789";
var uniqueID = "";
var chars = 'abcdef0123456789';
var uniqueID = '';
for (var i = 0; i < 20; i++) {
uniqueID += chars.substr(Math.floor((Math.random() * 15)), 1);
}
@ -65,37 +67,39 @@ SAML.prototype.signRequest = function (xml) {
var signer = crypto.createSign('RSA-SHA1');
signer.update(xml);
return signer.sign(this.options.privateKey, 'base64');
}
};
SAML.prototype.generateAuthorizeRequest = function (req) {
var id = "_" + this.generateUniqueID();
var id = '_' + this.generateUniqueID();
var instant = this.generateInstant();
// Post-auth destination
var callbackUrl;
if (this.options.callbackUrl) {
callbackUrl = this.options.callbackUrl;
} else {
var callbackUrl = this.options.protocol + req.headers.host + this.options.path;
callbackUrl = this.options.protocol + req.headers.host + this.options.path;
}
if (this.options.id)
if (this.options.id) {
id = this.options.id;
}
var request =
"<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"" + id + "\" Version=\"2.0\" IssueInstant=\"" + instant +
"\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" AssertionConsumerServiceURL=\"" + callbackUrl + "\" Destination=\"" +
this.options.entryPoint + "\">" +
"<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + this.options.issuer + "</saml:Issuer>\n";
'<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="' + id + '" Version="2.0" IssueInstant="' + instant +
'" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="' + callbackUrl + '" Destination="' +
this.options.entryPoint + '">' +
'<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + this.options.issuer + '</saml:Issuer>\n';
if (this.options.identifierFormat) {
request += "<samlp:NameIDPolicy xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Format=\"" + this.options.identifierFormat +
"\" AllowCreate=\"true\"></samlp:NameIDPolicy>\n";
request += '<samlp:NameIDPolicy xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Format="' + this.options.identifierFormat +
'" AllowCreate="true"></samlp:NameIDPolicy>\n';
}
request +=
"<samlp:RequestedAuthnContext xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Comparison=\"exact\">" +
"<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n" +
"</samlp:AuthnRequest>";
'<samlp:RequestedAuthnContext xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Comparison="exact">' +
'<saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n' +
'</samlp:AuthnRequest>';
return request;
};
@ -106,39 +110,39 @@ SAML.prototype.generateLogoutRequest = function (options) {
// sessionIndex: sessionIndex
// --- NO SAMLsettings: <Meteor.setting.saml entry for the provider you want to SLO from
var id = "_" + this.generateUniqueID();
var id = '_' + this.generateUniqueID();
var instant = this.generateInstant();
var request = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
"xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"" + id + "\" Version=\"2.0\" IssueInstant=\"" + instant +
"\" Destination=\"" + this.options.idpSLORedirectURL + "\">" +
"<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + this.options.issuer + "</saml:Issuer>" +
"<saml:NameID Format=\"" + this.options.identifierFormat + "\">" + options.nameID + "</saml:NameID>" +
"</samlp:LogoutRequest>";
request = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
"ID=\"" + id + "\" " +
"Version=\"2.0\" " +
"IssueInstant=\"" + instant + "\" " +
"Destination=\"" + this.options.idpSLORedirectURL + "\" " +
">" +
"<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" + this.options.issuer + "</saml:Issuer>" +
"<saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
"NameQualifier=\"http://id.init8.net:8080/openam\" " +
"SPNameQualifier=\"" + this.options.issuer + "\" " +
"Format=\"" + this.options.identifierFormat + "\">" +
options.nameID + "</saml:NameID>" +
"<samlp:SessionIndex xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">" + options.sessionIndex + "</samlp:SessionIndex>" +
"</samlp:LogoutRequest>";
var request = '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' +
'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="' + id + '" Version="2.0" IssueInstant="' + instant +
'" Destination="' + this.options.idpSLORedirectURL + '">' +
'<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + this.options.issuer + '</saml:Issuer>' +
'<saml:NameID Format="' + this.options.identifierFormat + '">' + options.nameID + '</saml:NameID>' +
'</samlp:LogoutRequest>';
request = '<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' +
'ID="' + id + '" ' +
'Version="2.0" ' +
'IssueInstant="' + instant + '" ' +
'Destination="' + this.options.idpSLORedirectURL + '" ' +
'>' +
'<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' + this.options.issuer + '</saml:Issuer>' +
'<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ' +
'NameQualifier="http://id.init8.net:8080/openam" ' +
'SPNameQualifier="' + this.options.issuer + '" ' +
'Format="' + this.options.identifierFormat + '">' +
options.nameID + '</saml:NameID>' +
'<samlp:SessionIndex xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">' + options.sessionIndex + '</samlp:SessionIndex>' +
'</samlp:LogoutRequest>';
if (Meteor.settings.debug) {
console.log("------- SAML Logout request -----------");
console.log('------- SAML Logout request -----------');
console.log(request);
}
return {
request: request,
id: id
};
}
};
SAML.prototype.requestToUrl = function (request, operation, callback) {
var self = this;
@ -157,10 +161,11 @@ SAML.prototype.requestToUrl = function (request, operation, callback) {
}
}
if (target.indexOf('?') > 0)
if (target.indexOf('?') > 0) {
target += '&';
else
} else {
target += '?';
}
var samlRequest = {
SAMLRequest: base64
@ -172,16 +177,17 @@ SAML.prototype.requestToUrl = function (request, operation, callback) {
}
// TBD. We should really include a proper RelayState here
var relayState;
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
var relayState = Meteor.absoluteUrl();
relayState = Meteor.absoluteUrl();
} else {
var relayState = self.options.provider;
relayState = self.options.provider;
}
target += querystring.stringify(samlRequest) + "&RelayState=" + relayState;
target += querystring.stringify(samlRequest) + '&RelayState=' + relayState;
if (Meteor.settings.debug) {
console.log("requestToUrl: " + target);
console.log('requestToUrl: ' + target);
}
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
@ -192,7 +198,7 @@ SAML.prototype.requestToUrl = function (request, operation, callback) {
callback(null, target);
}
});
}
};
SAML.prototype.getAuthorizeUrl = function (req, callback) {
var request = this.generateAuthorizeRequest(req);
@ -204,39 +210,39 @@ SAML.prototype.getLogoutUrl = function (req, callback) {
var request = this.generateLogoutRequest(req);
this.requestToUrl(request, 'logout', callback);
}
};
SAML.prototype.certToPEM = function (cert) {
cert = cert.match(/.{1,64}/g).join('\n');
cert = "-----BEGIN CERTIFICATE-----\n" + cert;
cert = cert + "\n-----END CERTIFICATE-----\n";
cert = '-----BEGIN CERTIFICATE-----\n' + cert;
cert = cert + '\n-----END CERTIFICATE-----\n';
return cert;
};
function findChilds(node, localName, namespace) {
var res = []
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i]
if (child.localName == localName && (child.namespaceURI == namespace || !namespace)) {
res.push(child)
}
}
return res;
}
// function findChilds(node, localName, namespace) {
// var res = [];
// for (var i = 0; i < node.childNodes.length; i++) {
// var child = node.childNodes[i];
// if (child.localName === localName && (child.namespaceURI === namespace || !namespace)) {
// res.push(child);
// }
// }
// return res;
// }
SAML.prototype.validateSignature = function (xml, cert) {
var self = this;
var doc = new xmldom.DOMParser().parseFromString(xml);
var signature = xmlCrypto.xpath(doc, "//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];
var signature = xmlCrypto.xpath(doc, '//*[local-name(.)=\'Signature\' and namespace-uri(.)=\'http://www.w3.org/2000/09/xmldsig#\']')[0];
var sig = new xmlCrypto.SignedXml();
sig.keyInfoProvider = {
getKeyInfo: function (key) {
return "<X509Data></X509Data>"
getKeyInfo: function (/*key*/) {
return '<X509Data></X509Data>';
},
getKey: function (keyInfo) {
getKey: function (/*keyInfo*/) {
return self.certToPEM(cert);
}
};
@ -257,7 +263,7 @@ SAML.prototype.getElement = function (parentElement, elementName) {
return parentElement['saml2:' + elementName];
}
return parentElement[elementName];
}
};
SAML.prototype.validateLogoutResponse = function (samlResponse, callback) {
var self = this;
@ -267,7 +273,7 @@ SAML.prototype.validateLogoutResponse = function (samlResponse, callback) {
if (err) {
if (Meteor.settings.debug) {
console.log(err)
console.log(err);
}
} else {
var parser = new xml2js.Parser({
@ -278,37 +284,37 @@ SAML.prototype.validateLogoutResponse = function (samlResponse, callback) {
if (response) {
// TBD. Check if this msg corresponds to one we sent
var inResponseTo = response['$'].InResponseTo;
var inResponseTo = response.$.InResponseTo;
if (Meteor.settings.debug) {
console.log("In Response to: " + inResponseTo);
console.log('In Response to: ' + inResponseTo);
}
var status = self.getElement(response, 'Status');
var statusCode = self.getElement(status[0], 'StatusCode')[0]['$'].Value;
var statusCode = self.getElement(status[0], 'StatusCode')[0].$.Value;
if (Meteor.settings.debug) {
console.log("StatusCode: " + JSON.stringify(statusCode));
console.log('StatusCode: ' + JSON.stringify(statusCode));
}
if (statusCode === 'urn:oasis:names:tc:SAML:2.0:status:Success') {
// In case of a successful logout at IDP we return inResponseTo value.
// This is the only way how we can identify the Meteor user (as we don't use Session Cookies)
callback(null, inResponseTo);
} else {
callback("Error. Logout not confirmed by IDP", null);
callback('Error. Logout not confirmed by IDP', null);
}
} else {
callback("No Response Found", null);
callback('No Response Found', null);
}
})
});
}
})
}
});
};
SAML.prototype.validateResponse = function (samlResponse, relayState, callback) {
var self = this;
var xml = new Buffer(samlResponse, 'base64').toString('ascii');
// We currently use RelayState to save SAML provider
if (Meteor.settings.debug) {
console.log("Validating response with relay state: " + xml);
console.log('Validating response with relay state: ' + xml);
}
var parser = new xml2js.Parser({
explicitRoot: true
@ -317,20 +323,20 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
parser.parseString(xml, function (err, doc) {
// Verify signature
if (Meteor.settings.debug) {
console.log("Verify signature");
console.log('Verify signature');
}
if (self.options.cert && !self.validateSignature(xml, self.options.cert)) {
if (Meteor.settings.debug) {
console.log("Signature WRONG");
console.log('Signature WRONG');
}
return callback(new Error('Invalid signature'), null, false);
}
if (Meteor.settings.debug) {
console.log("Signature OK");
console.log('Signature OK');
}
var response = self.getElement(doc, 'Response');
if (Meteor.settings.debug) {
console.log("Got response");
console.log('Got response');
}
if (response) {
var assertion = self.getElement(response, 'Assertion');
@ -338,10 +344,10 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
return callback(new Error('Missing SAML assertion'), null, false);
}
profile = {};
var profile = {};
if (response['$'] && response['$']['InResponseTo']) {
profile.inResponseToId = response['$']['InResponseTo'];
if (response.$ && response.$.InResponseTo) {
profile.inResponseToId = response.$.InResponseTo;
}
var issuer = self.getElement(assertion[0], 'Issuer');
@ -354,10 +360,10 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
if (subject) {
var nameID = self.getElement(subject[0], 'NameID');
if (nameID) {
profile.nameID = nameID[0]["_"];
profile.nameID = nameID[0]._;
if (nameID[0]['$'].Format) {
profile.nameIDFormat = nameID[0]['$'].Format;
if (nameID[0].$.Format) {
profile.nameIDFormat = nameID[0].$.Format;
}
}
}
@ -365,22 +371,22 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
var authnStatement = self.getElement(assertion[0], 'AuthnStatement');
if (authnStatement) {
if (authnStatement[0]['$'].SessionIndex) {
if (authnStatement[0].$.SessionIndex) {
profile.sessionIndex = authnStatement[0]['$'].SessionIndex;
profile.sessionIndex = authnStatement[0].$.SessionIndex;
if (Meteor.settings.debug) {
console.log("Session Index: " + profile.sessionIndex);
console.log('Session Index: ' + profile.sessionIndex);
}
} else {
if (Meteor.settings.debug) {
console.log("No Session Index Found");
console.log('No Session Index Found');
}
}
} else {
if (Meteor.settings.debug) {
console.log("No AuthN Statement found");
console.log('No AuthN Statement found');
}
}
@ -392,9 +398,9 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
attributes.forEach(function (attribute) {
var value = self.getElement(attribute, 'AttributeValue');
if (typeof value[0] === 'string') {
profile[attribute['$'].Name] = value[0];
profile[attribute.$.Name] = value[0];
} else {
profile[attribute['$'].Name] = value[0]['_'];
profile[attribute.$.Name] = value[0]._;
}
});
}
@ -413,7 +419,7 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
profile.email = profile.nameID;
}
if (Meteor.settings.debug) {
console.log("NameID: " + JSON.stringify(profile));
console.log('NameID: ' + JSON.stringify(profile));
}
callback(null, profile, false);
@ -430,7 +436,7 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
});
};
var decryptionCert;
SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
var keyDescriptor = null;
@ -442,7 +448,7 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
if (this.options.privateKey) {
if (!decryptionCert) {
throw new Error(
"Missing decryptionCert while generating metadata for decrypting service provider");
'Missing decryptionCert while generating metadata for decrypting service provider');
}
decryptionCert = decryptionCert.replace(/-+BEGIN CERTIFICATE-+\r?\n?/, '');
@ -480,7 +486,7 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
if (!this.options.callbackUrl && !callbackUrl) {
throw new Error(
"Unable to generate service provider metadata when callbackUrl option is not set");
'Unable to generate service provider metadata when callbackUrl option is not set');
}
var metadata = {
@ -493,8 +499,8 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
'KeyDescriptor': keyDescriptor,
'SingleLogoutService': {
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'@Location': Meteor.absoluteUrl() + "_saml/logout/" + this.options.provider + "/",
'@ResponseLocation': Meteor.absoluteUrl() + "_saml/logout/" + this.options.provider + "/"
'@Location': Meteor.absoluteUrl() + '_saml/logout/' + this.options.provider + '/',
'@ResponseLocation': Meteor.absoluteUrl() + '_saml/logout/' + this.options.provider + '/'
},
'NameIDFormat': this.options.identifierFormat,
'AssertionConsumerService': {
@ -512,4 +518,4 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
indent: ' ',
newline: '\n'
});
};
};

@ -1,67 +1,68 @@
Meteor.loginWithCas = function(callback) {
var credentialToken = Random.id();
var login_url = RocketChat.settings.get("CAS_login_url");
var popup_width = RocketChat.settings.get("CAS_popup_width");
var popup_height = RocketChat.settings.get("CAS_popup_height");
var credentialToken = Random.id();
var login_url = RocketChat.settings.get('CAS_login_url');
var popup_width = RocketChat.settings.get('CAS_popup_width');
var popup_height = RocketChat.settings.get('CAS_popup_height');
if (!login_url) {
return;
}
if (!login_url) {
return;
}
var loginUrl = login_url + "?service=" + Meteor.absoluteUrl('_cas/') + credentialToken;
var loginUrl = login_url + '?service=' + Meteor.absoluteUrl('_cas/') + credentialToken;
var popup = openCenteredPopup(
loginUrl,
popup_width || 800,
popup_height || 600
);
var popup = openCenteredPopup(
loginUrl,
popup_width || 800,
popup_height || 600
);
var checkPopupOpen = setInterval(function() {
try {
// Fix for #328 - added a second test criteria (popup.closed === undefined)
// to humour this Android quirk:
// http://code.google.com/p/android/issues/detail?id=21061
var popupClosed = popup.closed || popup.closed === undefined;
} catch (e) {
// For some unknown reason, IE9 (and others?) sometimes (when
// the popup closes too quickly?) throws "SCRIPT16386: No such
// interface supported" when trying to read 'popup.closed'. Try
// again in 100ms.
return;
}
var checkPopupOpen = setInterval(function() {
var popupClosed;
try {
// Fix for #328 - added a second test criteria (popup.closed === undefined)
// to humour this Android quirk:
// http://code.google.com/p/android/issues/detail?id=21061
popupClosed = popup.closed || popup.closed === undefined;
} catch (e) {
// For some unknown reason, IE9 (and others?) sometimes (when
// the popup closes too quickly?) throws "SCRIPT16386: No such
// interface supported" when trying to read 'popup.closed'. Try
// again in 100ms.
return;
}
if (popupClosed) {
clearInterval(checkPopupOpen);
if (popupClosed) {
clearInterval(checkPopupOpen);
// check auth on server.
Accounts.callLoginMethod({
methodArguments: [{ cas: { credentialToken: credentialToken } }],
userCallback: callback
});
}
}, 100);
// check auth on server.
Accounts.callLoginMethod({
methodArguments: [{ cas: { credentialToken: credentialToken } }],
userCallback: callback
});
}
}, 100);
};
var openCenteredPopup = function(url, width, height) {
var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft;
var screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop;
var outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth;
var outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : (document.body.clientHeight - 22);
// XXX what is the 22?
var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft;
var screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop;
var outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth;
var outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : (document.body.clientHeight - 22);
// XXX what is the 22?
// Use `outerWidth - width` and `outerHeight - height` for help in
// positioning the popup centered relative to the current window
var left = screenX + (outerWidth - width) / 2;
var top = screenY + (outerHeight - height) / 2;
var features = ('width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',scrollbars=yes');
// Use `outerWidth - width` and `outerHeight - height` for help in
// positioning the popup centered relative to the current window
var left = screenX + (outerWidth - width) / 2;
var top = screenY + (outerHeight - height) / 2;
var features = ('width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',scrollbars=yes');
var newwindow = window.open(url, 'Login', features);
if (newwindow.focus) {
newwindow.focus();
}
var newwindow = window.open(url, 'Login', features);
if (newwindow.focus) {
newwindow.focus();
}
return newwindow;
return newwindow;
};

@ -1,64 +1,66 @@
/* globals logger:true */
logger = new Logger('CAS', {});
Meteor.startup(function(){
RocketChat.settings.addGroup('CAS', function() {
this.add("CAS_enabled", false, { type: 'boolean', group: 'CAS', public: true });
this.add("CAS_base_url" , '' , { type: 'string' , group: 'CAS', public: true });
this.add("CAS_login_url" , '' , { type: 'string' , group: 'CAS', public: true });
this.add("CAS_version" , '1.0' , { type: 'select', values: [{ key: '1.0', i18nLabel: '1.0'}], group: 'CAS' });
RocketChat.settings.addGroup('CAS', function() {
this.add('CAS_enabled', false, { type: 'boolean', group: 'CAS', public: true });
this.add('CAS_base_url' , '' , { type: 'string' , group: 'CAS', public: true });
this.add('CAS_login_url' , '' , { type: 'string' , group: 'CAS', public: true });
this.add('CAS_version' , '1.0' , { type: 'select', values: [{ key: '1.0', i18nLabel: '1.0'}], group: 'CAS' });
this.section('CAS Login Layout', function() {
this.add("CAS_popup_width" , '810' , { type: 'string' , group: 'CAS', public: true });
this.add("CAS_popup_height" , '610' , { type: 'string' , group: 'CAS', public: true });
this.add("CAS_button_label_text" , 'CAS' , { type: 'string' , group: 'CAS'});
this.add("CAS_button_label_color", '#FFFFFF' , { type: 'color' , group: 'CAS'});
this.add("CAS_button_color" , '#13679A' , { type: 'color' , group: 'CAS'});
this.add("CAS_autoclose", true , { type: 'boolean' , group: 'CAS'});
});
});
this.section('CAS Login Layout', function() {
this.add('CAS_popup_width' , '810' , { type: 'string' , group: 'CAS', public: true });
this.add('CAS_popup_height' , '610' , { type: 'string' , group: 'CAS', public: true });
this.add('CAS_button_label_text' , 'CAS' , { type: 'string' , group: 'CAS'});
this.add('CAS_button_label_color', '#FFFFFF' , { type: 'color' , group: 'CAS'});
this.add('CAS_button_color' , '#13679A' , { type: 'color' , group: 'CAS'});
this.add('CAS_autoclose', true , { type: 'boolean' , group: 'CAS'});
});
});
});
timer = undefined
function updateServices(record) {
if( typeof timer != 'undefined' ) {
Meteor.clearTimeout(timer);
}
var timer;
function updateServices(/*record*/) {
if( typeof timer !== 'undefined' ) {
Meteor.clearTimeout(timer);
}
timer = Meteor.setTimeout(function() {
data = {
// These will pe passed to 'node-cas' as options
enabled: RocketChat.settings.get("CAS_enabled"),
base_url: RocketChat.settings.get("CAS_base_url"),
login_url: RocketChat.settings.get("CAS_login_url"),
// Rocketchat Visuals
buttonLabelText: RocketChat.settings.get("CAS_button_label_text"),
buttonLabelColor: RocketChat.settings.get("CAS_button_label_color"),
buttonColor: RocketChat.settings.get("CAS_button_color"),
width: RocketChat.settings.get("CAS_popup_width"),
height: RocketChat.settings.get("CAS_popup_height"),
autoclose: RocketChat.settings.get("CAS_autoclose"),
};
timer = Meteor.setTimeout(function() {
var data = {
// These will pe passed to 'node-cas' as options
enabled: RocketChat.settings.get('CAS_enabled'),
base_url: RocketChat.settings.get('CAS_base_url'),
login_url: RocketChat.settings.get('CAS_login_url'),
// Rocketchat Visuals
buttonLabelText: RocketChat.settings.get('CAS_button_label_text'),
buttonLabelColor: RocketChat.settings.get('CAS_button_label_color'),
buttonColor: RocketChat.settings.get('CAS_button_color'),
width: RocketChat.settings.get('CAS_popup_width'),
height: RocketChat.settings.get('CAS_popup_height'),
autoclose: RocketChat.settings.get('CAS_autoclose'),
};
// Either register or deregister the CAS login service based upon its configuration
if( data.enabled ) {
logger.info("Enabling CAS login service")
ServiceConfiguration.configurations.upsert({service: 'cas'}, { $set: data });
} else {
logger.info("Disabling CAS login service");
ServiceConfiguration.configurations.remove({service: 'cas'});
}
}, 2000);
};
// Either register or deregister the CAS login service based upon its configuration
if( data.enabled ) {
logger.info('Enabling CAS login service');
ServiceConfiguration.configurations.upsert({service: 'cas'}, { $set: data });
} else {
logger.info('Disabling CAS login service');
ServiceConfiguration.configurations.remove({service: 'cas'});
}
}, 2000);
}
function check_record (record) {
if( /^CAS_.+/.test( record._id )){
updateServices( record );
}
};
if( /^CAS_.+/.test( record._id )){
updateServices( record );
}
}
RocketChat.models.Settings.find().observe({
added: check_record,
changed: check_record,
removed: check_record
added: check_record,
changed: check_record,
removed: check_record
});

@ -1,3 +1,5 @@
/* globals RoutePolicy, logger */
var Fiber = Npm.require('fibers');
var url = Npm.require('url');
var CAS = Npm.require('cas');
@ -8,80 +10,80 @@ RoutePolicy.declare('/_cas/', 'network');
// Listen to incoming OAuth http requests
WebApp.connectHandlers.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();
// 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();
});
var middleware = function (req, res, next) {
// Make sure to catch any exceptions because otherwise we'd crash
// the runner
try {
var barePath = req.url.substring(0, req.url.indexOf('?'));
var splitPath = barePath.split('/');
// Any non-cas request will continue down the default
// middlewares.
if (splitPath[1] !== '_cas') {
next();
return;
}
// get auth token
var credentialToken = splitPath[2];
if (!credentialToken) {
closePopup(res);
return;
}
// validate ticket
casTicket(req, credentialToken, function() {
closePopup(res);
});
} catch (err) {
logger.error("Unexpected error : " + err.message);
closePopup(res);
}
// Make sure to catch any exceptions because otherwise we'd crash
// the runner
try {
var barePath = req.url.substring(0, req.url.indexOf('?'));
var splitPath = barePath.split('/');
// Any non-cas request will continue down the default
// middlewares.
if (splitPath[1] !== '_cas') {
next();
return;
}
// get auth token
var credentialToken = splitPath[2];
if (!credentialToken) {
closePopup(res);
return;
}
// validate ticket
casTicket(req, credentialToken, function() {
closePopup(res);
});
} catch (err) {
logger.error('Unexpected error : ' + err.message);
closePopup(res);
}
};
var casTicket = function (req, token, callback) {
// get configuration
if (!RocketChat.settings.get("CAS_enabled")) {
logger.error("Got ticket validation request, but CAS is not enabled");
callback();
}
// get ticket and validate.
var parsedUrl = url.parse(req.url, true);
var ticketId = parsedUrl.query.ticket;
var baseUrl = RocketChat.settings.get("CAS_base_url");
logger.debug("Using CAS_base_url: " + baseUrl);
var cas = new CAS({
base_url: baseUrl,
service: Meteor.absoluteUrl() + "_cas/" + token
});
cas.validate(ticketId, function(err, status, username) {
if (err) {
logger.error("error when trying to validate " + err);
} else {
if (status) {
logger.info("Validated user: " + username);
_casCredentialTokens[token] = { id: username };
} else {
logger.error("Unable to validate ticket: " + ticketId);
}
}
callback();
});
return;
// get configuration
if (!RocketChat.settings.get('CAS_enabled')) {
logger.error('Got ticket validation request, but CAS is not enabled');
callback();
}
// get ticket and validate.
var parsedUrl = url.parse(req.url, true);
var ticketId = parsedUrl.query.ticket;
var baseUrl = RocketChat.settings.get('CAS_base_url');
logger.debug('Using CAS_base_url: ' + baseUrl);
var cas = new CAS({
base_url: baseUrl,
service: Meteor.absoluteUrl() + '_cas/' + token
});
cas.validate(ticketId, function(err, status, username) {
if (err) {
logger.error('error when trying to validate ' + err);
} else {
if (status) {
logger.info('Validated user: ' + username);
_casCredentialTokens[token] = { id: username };
} else {
logger.error('Unable to validate ticket: ' + ticketId);
}
}
callback();
});
return;
};
/*
@ -89,72 +91,73 @@ var casTicket = function (req, token, callback) {
* It is call after Accounts.callLoginMethod() is call from client.
*
*/
Accounts.registerLoginHandler(function (options) {
if (!options.cas)
return undefined;
if (!_hasCredential(options.cas.credentialToken)) {
throw new Meteor.Error(Accounts.LoginCancelledError.numericError,
'no matching login attempt found');
}
var result = _retrieveCredential(options.cas.credentialToken);
var options = { profile: { name: result.id } };
// Search existing user by its external service id
logger.debug("Looking up user with username: " + result.id );
var user = Meteor.users.findOne({ 'services.cas.external_id': result.id });
if (user) {
logger.debug("Using existing user for '" + result.id + "' with id: " + user._id);
} else {
// Define new user
var newUser = {
username: result.id,
active: true,
globalRoles: ['user'],
services: {
cas: {
external_id: result.id
}
}
};
// Create the user
logger.debug("User '" + result.id + "'does not exist yet, creating it");
var userId = Accounts.insertUserDoc({}, newUser);
// Fetch and use it
user = Meteor.users.findOne(userId);
logger.debug("Created new user for '" + result.id + "' with id: " + user._id);
logger.debug('Joining user to default channels');
Meteor.runAsUser(user._id, function() {
Meteor.call('joinDefaultChannels');
});
}
return { userId: user._id };
Accounts.registerLoginHandler(function (options) {
if (!options.cas) {
return undefined;
}
if (!_hasCredential(options.cas.credentialToken)) {
throw new Meteor.Error(Accounts.LoginCancelledError.numericError,
'no matching login attempt found');
}
var result = _retrieveCredential(options.cas.credentialToken);
options = { profile: { name: result.id } };
// Search existing user by its external service id
logger.debug('Looking up user with username: ' + result.id );
var user = Meteor.users.findOne({ 'services.cas.external_id': result.id });
if (user) {
logger.debug('Using existing user for \'' + result.id + '\' with id: ' + user._id);
} else {
// Define new user
var newUser = {
username: result.id,
active: true,
globalRoles: ['user'],
services: {
cas: {
external_id: result.id
}
}
};
// Create the user
logger.debug('User \'' + result.id + '\'does not exist yet, creating it');
var userId = Accounts.insertUserDoc({}, newUser);
// Fetch and use it
user = Meteor.users.findOne(userId);
logger.debug('Created new user for \'' + result.id + '\' with id: ' + user._id);
logger.debug('Joining user to default channels');
Meteor.runAsUser(user._id, function() {
Meteor.call('joinDefaultChannels');
});
}
return { userId: user._id };
});
var _hasCredential = function(credentialToken) {
return _.has(_casCredentialTokens, credentialToken);
}
return _.has(_casCredentialTokens, credentialToken);
};
/*
* Retrieve token and delete it to avoid replaying it.
*/
var _retrieveCredential = function(credentialToken) {
var result = _casCredentialTokens[credentialToken];
delete _casCredentialTokens[credentialToken];
return result;
}
var result = _casCredentialTokens[credentialToken];
delete _casCredentialTokens[credentialToken];
return result;
};
var closePopup = function(res) {
res.writeHead(200, {'Content-Type': 'text/html'});
var content = '<html><head><script>window.close()</script></head></html>';
res.end(content, 'utf-8');
}
res.writeHead(200, {'Content-Type': 'text/html'});
var content = '<html><head><script>window.close()</script></head></html>';
res.end(content, 'utf-8');
};

@ -1,8 +1,8 @@
Package.describe({
name: "rocketchat:cas",
summary: "CAS support for accounts",
version: "1.0.0",
git: "https://github.com/rocketchat/rocketchat-cas"
name: 'rocketchat:cas',
summary: 'CAS support for accounts',
version: '1.0.0',
git: 'https://github.com/rocketchat/rocketchat-cas'
});
Package.onUse(function(api) {
@ -18,6 +18,7 @@ Package.onUse(function(api) {
api.use('accounts-base', 'server');
api.use('underscore');
api.use('ecmaescript');
// Server files
api.add_files('cas_rocketchat.js', 'server');
@ -29,5 +30,5 @@ Package.onUse(function(api) {
});
Npm.depends({
cas: "0.0.3"
cas: '0.0.3'
});

Loading…
Cancel
Save