mirror of
https://github.com/netzbegruenung/meteor-accounts-saml.git
synced 2024-05-09 23:23:41 +02:00
Logout work with OpenAM
stille need to remove console debug output
This commit is contained in:
parent
96996a842b
commit
e680c55ac5
|
@ -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)));
|
||||
});
|
||||
|
||||
|
||||
|
|
129
saml_server.js
129
saml_server.js
|
@ -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
|
||||
};
|
||||
|
|
166
saml_utils.js
166
saml_utils.js
|
@ -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': {
|
||||
|
|
Loading…
Reference in a new issue