Logout work with OpenAM

stille need to remove console debug output
This commit is contained in:
Steffo Weber 2015-08-07 17:57:51 +02:00
parent 96996a842b
commit e680c55ac5
4 changed files with 224 additions and 81 deletions

0
resp.json Normal file
View file

View file

@ -28,12 +28,6 @@ Accounts.saml.initiateLogin = function (options, callback, dimensions) {
}, 100);
};
Accounts.saml.idpInitiatedSLO = function (options) {
//Meteor.absoluteUrl("_saml/logout/"+options.provider+"/"+options.credentialToken
console.log("Options: " + JSON.stringify(options));
//location.href(Meteor.absoluteUrl("_saml/sloInit/"+options.provider));
window.open(Meteor.absoluteUrl("_saml/sloInit/" + options.provider));
}
var openCenteredPopup = function (url, width, height) {
var screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft;
@ -75,7 +69,9 @@ Meteor.logoutWithSaml = function (options, callback) {
//Accounts.saml.idpInitiatedSLO(options, callback);
Meteor.call("samlLogout", options.provider, function (err, result) {
console.log("LOC " + result);
window.location.replace(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)));
});

View file

@ -11,30 +11,54 @@ Meteor.methods({
// Make sure the user is logged in before initiate SAML SLO
if (!Meteor.userId()) {
throw new Meteor.Error("not-authorized");
}
}
var samlProvider = function (element) {
return (element.provider == provider)
}
providerConfig = Meteor.settings.saml.filter(samlProvider)[0];
if (Meteor.settings.debug) {
if (Meteor.settings.debug) {
console.log("Logout request from " + JSON.stringify(providerConfig));
}
// This query should respect upcoming array of SAML logins
nameID = Meteor.users.findOne({_id: Meteor.userId(), "services.saml.provider": provider}, {"services.saml":1}).services.saml.nameID;
if (Meteor.settings.debug) {
var user = Meteor.users.findOne({
_id: Meteor.userId(),
"services.saml.provider": provider
}, {
"services.saml": 1
});
var nameID = user.services.saml.nameID;
var sessionIndex = nameID = user.services.saml.idpSession;
if (Meteor.settings.debug) {
console.log("NameID for user " + Meteor.userId() + " found: " + JSON.stringify(nameID));
}
_saml = new SAML(providerConfig);
request = _saml.generateLogoutRequest({nameID: nameID});
if (Meteor.settings.debug) {
console.log("SAML Logout Request " + _saml.requestToUrl(request, "logout", function(){}));
}
return "http://google.com";
_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);
var result = _syncRequestToUrl(request.request, "logout");
if (Meteor.settings.debug) {
console.log("SAML Logout Request " + result);
}
return result;
}
})
@ -43,7 +67,7 @@ Accounts.registerLoginHandler(function (loginRequest) {
return undefined;
}
var loginResult = Accounts.saml.retrieveCredential(loginRequest.credentialToken);
console.log("RESULT :" + JSON.stringify(loginResult));
if (loginResult && loginResult.profile && loginResult.profile.email) {
var user = Meteor.users.findOne({
@ -65,6 +89,7 @@ Accounts.registerLoginHandler(function (loginRequest) {
var samlLogin = {
provider: Accounts.saml.RelayState,
idp: loginResult.profile.issuer,
idpSession: loginResult.profile.sessionIndex,
nameID: loginResult.profile.nameID
};
@ -119,7 +144,6 @@ middleware = function (req, res, next) {
// the runner
try {
var samlObject = samlUrlToObject(req.url);
console.log("In middleware: ");
if (!samlObject || !samlObject.serviceName) {
next();
return;
@ -135,7 +159,6 @@ middleware = function (req, res, next) {
// Skip everything if there's no service set by the saml middleware
if (!service)
throw new Error("Unexpected SAML service " + samlObject.serviceName);
console.log("ACTION: " + samlObject.actionName);
switch (samlObject.actionName) {
case "metadata":
_saml = new SAML(service);
@ -145,30 +168,59 @@ middleware = function (req, res, next) {
closePopup(res);
break;
case "logout":
// This is where we receive SAML LogoutResponse
_saml = new SAML(service);
console.log("Service: " + JSON.stringify(service));
var relayState = Meteor.absoluteUrl(); // used to be redirected back from IDP to our Meteor app
res.writeHead(302, {
'Location': service.logoutUrl + "&RelayState=" + relayState
});
res.end();
//closePopup(res);
break;
case "sloInit":
_saml = new SAML(service);
console.log("LOGOUT INITIATED");
var relayState = Meteor.absoluteUrl();
//debugger
_saml.getLogoutUrl(req, function (err, url) {
if (err)
throw new Error("Unable to generate SAML logout request");
res.writeHead(302, {
'Location': url
});
res.end();
});
_saml.validateLogoutResponse(req.query.SAMLResponse, function (err, result) {
if (!err) {
console.log("Need to logout Meteor user " + result);
var logOutUser = function (inResponseTo) {
console.log("Logging Out user via inResponseTo " + inResponseTo);
var loggedOutUser = Meteor.users.find({
'services.saml.inResponseTo': inResponseTo
}).fetch();
if (loggedOutUser.length == 1) {
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":
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);
@ -193,7 +245,6 @@ middleware = function (req, res, next) {
var credentialToken = profile.inResponseToId || profile.InResponseTo || samlObject.credentialToken;
if (!credentialToken)
throw new Error("Unable to determine credentialToken");
console.log("Checking CT: " + credentialToken);
Accounts.saml._loginResultForCredentialToken[credentialToken] = {
profile: profile
};

View file

@ -107,6 +107,7 @@ SAML.prototype.generateAuthorizeRequest = function (req) {
SAML.prototype.generateLogoutRequest = function (options) {
// options should be of the form
// nameId: <nameId as submitted during SAML SSO>
// sessionIndex: sessionIndex
// --- NO SAMLsettings: <Meteor.setting.saml entry for the provider you want to SLO from
var id = "_" + this.generateUniqueID();
@ -118,51 +119,83 @@ SAML.prototype.generateLogoutRequest = function (options) {
"<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(request);
}
return request;
return {request: request, id: id};
}
SAML.prototype.requestToUrl = function (request, operation, callback) {
var self = this;
var result;
zlib.deflateRaw(request, function (err, buffer) {
if (err) {
return callback(err);
}
var base64 = buffer.toString('base64');
var target = self.options.entryPoint;
if (operation === 'logout') {
if (self.options.logoutUrl) {
target = self.options.logoutUrl;
}
}
if (target.indexOf('?') > 0)
target += '&';
else
target += '?';
var samlRequest = {
SAMLRequest: base64
};
if (self.options.privateCert) {
samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
}
// TBD. We should really include a proper RelayState here
target += querystring.stringify(samlRequest) + "&RelayState=" + self.options.provider;
if (Meteor.settings.debug) {
console.log("requestToUrl: " + target);
if (err) {
return callback(err);
}
var base64 = buffer.toString('base64');
var target = self.options.entryPoint;
if (operation === 'logout') {
if (self.options.logoutUrl) {
target = self.options.logoutUrl;
}
}
if (target.indexOf('?') > 0)
target += '&';
else
target += '?';
var samlRequest = {
SAMLRequest: base64
};
if (self.options.privateCert) {
samlRequest.SigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
samlRequest.Signature = self.signRequest(querystring.stringify(samlRequest));
}
// TBD. We should really include a proper RelayState here
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
var relayState = Meteor.absoluteUrl();
} else {
var relayState = self.options.provider;
}
target += querystring.stringify(samlRequest) + "&RelayState=" + relayState;
if (Meteor.settings.debug) {
console.log("requestToUrl: " + target);
}
if (operation === 'logout') {
// in case of logout we want to be redirected back to the Meteor app.
//console.log("RETURNING TARGET: " + target);
result = target;
return callback(null, target);
} else {
console.log("CALLBACK: " + callback);
callback(null, target);
}
callback(null, target);
});
//console.log("RETURNING TARGET: " + result);
}
SAML.prototype.getAuthorizeUrl = function (req, callback) {
@ -229,13 +262,55 @@ SAML.prototype.getElement = function (parentElement, elementName) {
return parentElement['saml2:' + elementName];
}
return parentElement[elementName];
}
SAML.prototype.validateLogoutResponse = function (samlResponse, callback) {
var self = this;
var compressedSAMLResponse = new Buffer(samlResponse, 'base64');
zlib.inflateRaw(compressedSAMLResponse, function (err, decoded) {
if (err) {
console.log(err)
} else {
var parser = new xml2js.Parser({
explicitRoot: true
});
parser.parseString(decoded, function (err, doc) {
var response = self.getElement(doc, 'LogoutResponse');
if (response) {
// TBD. Check if this msg corresponds to one we sent
var inResponseTo = response['$'].InResponseTo;
console.log("In Response to: " + inResponseTo);
var status = self.getElement(response, 'Status');
var statusCode = self.getElement(status[0], 'StatusCode')[0]['$'].Value;
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);
}
} else {
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
console.log("Validating response with relay state: " + relayState);
console.log("Validating response with relay state: " + xml);
var parser = new xml2js.Parser({
explicitRoot: true
});
@ -287,6 +362,22 @@ SAML.prototype.validateResponse = function (samlResponse, relayState, callback)
}
}
var authnStatement = self.getElement(assertion[0], 'AuthnStatement');
if (authnStatement) {
if (authnStatement[0]['$'].SessionIndex) {
profile.sessionIndex = authnStatement[0]['$'].SessionIndex;
console.log("Session Index: " + profile.sessionIndex);
} else {
console.log("No Session Index Found");
}
} else {
console.log("No AuthN Statement found");
}
var attributeStatement = self.getElement(assertion[0], 'AttributeStatement');
if (attributeStatement) {
var attributes = self.getElement(attributeStatement[0], 'Attribute');
@ -396,6 +487,11 @@ SAML.prototype.generateServiceProviderMetadata = function (callbackUrl) {
'@entityID': this.options.issuer,
'SPSSODescriptor': {
'@protocolSupportEnumeration': 'urn:oasis:names:tc:SAML:2.0:protocol',
'SingleLogoutService': {
'@Binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
'@Location': this.options.sloConsumerUrl,
'@ResponseLocation': this.options.sloConsumerUrl
},
'KeyDescriptor': keyDescriptor,
'NameIDFormat': this.options.identifierFormat,
'AssertionConsumerService': {