From 4a781b21db10f82e35b9945109b5f4d41ad0e8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Smodi=C5=A1?= Date: Thu, 15 Oct 2015 18:46:54 +0200 Subject: Server-side support for SAML logout, sessions are destroyed only using an AJAX call. --- saml/saml.js | 156 +++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 53 deletions(-) (limited to 'saml') diff --git a/saml/saml.js b/saml/saml.js index a363c1e..43844f2 100644 --- a/saml/saml.js +++ b/saml/saml.js @@ -118,6 +118,15 @@ var makeWaiter = function (sid) { return waiter; }; +var makeDoubleWaiter = function (sid) { + var waiter = makeWaiter(sid); + waiter.responsePromise = new Promise(function (resolve, reject) { + waiter.responseResolve = resolve; + waiter.responseReject = reject; + }); + return waiter; +}; + // ================================================================================ // XML DOM data extraction functions for SAML data // ================================================================================ @@ -169,7 +178,7 @@ var parseAttributes = function (attributeStatementNode) { } } if (i < 0) { - console.log('ERROR: an attribute without a name'); + logger.error('ERROR: an attribute without a name'); } else { result[name] = parseAttributeValue(node); @@ -258,7 +267,7 @@ var handleLogoutResponse = function (xml, form, res) { var errors = [], issuer = null, status = null, - session = null, + waiter = null, doc, rootNode, attributes, i, inResponseTo, node; doc = new xmldom.DOMParser().parseFromString(xml); rootNode = doc.documentElement; @@ -275,8 +284,12 @@ var handleLogoutResponse = function (xml, form, res) { } if (!inResponseTo) errors.push('request ID that this message is a response to is missing'); else { - session = waitingLogouts[inResponseTo]; - if (!session) errors.push('there is no session waiting on request ID ' + inResponseTo); + waiter = parkedLogoutRequests[inResponseTo]; + if (!waiter) errors.push('there is no session waiting on request ID ' + inResponseTo); + else { + waiter.done = Date.now(); + delete parkedLogoutRequests[inResponseTo]; + } } node = rootNode.firstChild; while (node) { @@ -302,11 +315,11 @@ var handleLogoutResponse = function (xml, form, res) { if (errors.length > 0) { if (errors.length > 1) res.send('Multiple errors:\n ' + errors.join('\n ')); else res.send('Error: ' + errors[0]); - session.samlLogout(errors.join('\n')); + if (waiter) waiter.responseReject(new Error(errors.join(', '))); } else { res.send('OK'); - session.samlLogout(null); + if (waiter) waiter.responseResolve(); } }; @@ -378,7 +391,7 @@ var handleLogoutRequest = function (xml, form, res) { }; makeSamlRedirectUrl(SLOServiceURL, 'SAMLRequest', responseObj, form.RelayState ? {RelayState: form.RelayState} : null, function (err, svcUrl) { if (err) { - console.log('Error while assembling the Logout SAML response: ' + err); + logException('Error while assembling the Logout SAML response: ' + err, err); res.send('Failed: ' + err); return; } @@ -422,9 +435,9 @@ http_app.get('/Shibboleth.sso/Login', function (req, res) { makeSamlRedirectUrl(SSOServiceURL, 'SAMLRequest', request, null, function (err, svcUrl) { if (err) { logException('Error while assembling the Login SAML request: ' + err, err); - res.send('Failed: ' + err); waiter.done = Date.now(); waiter.reject(err instanceof Error ? err : new Error(err)); + res.send('Failed: ' + err); return; } res.redirect(svcUrl); // redirect to the IdP @@ -446,51 +459,56 @@ http_app.get('/Shibboleth.sso/Login', function (req, res) { // redirect to logout on Arnes AAI, https://codeq.si/Shibboleth.sso/Logout http_app.get('/Shibboleth.sso/Logout', function (req, res) { var sid = req.query.sid, - session, samlData, requestId, request; + waiter, requestId, request; if (!sid) { res.send('SID not set'); return; } - session = sessions[sid]; - if (!session) { - res.send('No such session: ' + sid); - return; - } - samlData = session.saml; - if (!samlData) { - res.send('No SAML session associated with session ' + sid); - return; - } - if (!samlData.NameID) { - res.send('The SAML data for session ' + sid + ' does not contain the NameID'); - return; - } - requestId = makeRequestId(); - request = { - 'samlp:LogoutRequest': { - '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', - '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', - '@Version': '2.0', - '@Destination': SLOServiceURL, - '@ID': requestId, - '@IssueInstant': new Date().toISOString(), - 'saml:Issuer' : { - '#text': SPIssuer - }, - 'saml:NameID': samlData.NameID - } - }; - if (samlData.SessionIndex) requestObj['samlp:LogoutRequest']['samlp:SessionIndex'] = {'#text': samlData.SessionIndex}; - makeSamlRedirectUrl(SLOServiceURL, 'SAMLRequest', request, null, function (err, svcUrl) { - if (err) { - console.log('Error while assembling the Logout SAML request: ' + err); - res.send('Failed: ' + err); - return; - } - res.redirect(svcUrl); // redirect to the IdP - parkedLogoutRequests[requestId] = session; - }); + waiter = waitingLogouts[sid]; + if (!waiter) waitingLogouts[sid] = waiter = makeDoubleWaiter(sid); + + waiter.promise + .then(function (samlData) { + requestId = makeRequestId(); + request = { + 'samlp:LogoutRequest': { + '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', + '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', + '@Version': '2.0', + '@Destination': SLOServiceURL, + '@ID': requestId, + '@IssueInstant': new Date().toISOString(), + 'saml:Issuer' : { + '#text': SPIssuer + }, + 'saml:NameID': samlData.NameID + } + }; + if (samlData.SessionIndex) request['samlp:LogoutRequest']['samlp:SessionIndex'] = {'#text': samlData.SessionIndex}; + makeSamlRedirectUrl(SLOServiceURL, 'SAMLRequest', request, null, function (err, svcUrl) { + if (err) { + logException('Error while assembling the Logout SAML request: ' + err, err); + waiter.done = Date.now(); + waiter.responseReject(err instanceof Error ? err : new Error(err)); + res.send('Failed: ' + err); + return; + } + res.redirect(svcUrl); // redirect to the IdP + parkedLogoutRequests[requestId] = waiter; + waiter.parked = Date.now(); + waiter.requestId = requestId; + }); + }) + .catch(function (e) { + logException('Exception while processing login request: ' + e, e); + res.send('Error: ' + e); + if (waiter) { + waiter.done = Date.now(); + waiter.responseReject(e); + } + }) + .done(); }); // user logged in, read the credentials @@ -606,7 +624,7 @@ http_app.get('/Shibboleth.sso/SLO/Redirect', function (req, res) { var form = req.query, delegateToHandler = function (err, buffer, handler) { if (err) { - console.log('Error while deflating request: ' + err); + logException('Error while deflating request: ' + err, err); res.send('Failed: ' + err); return; } @@ -618,25 +636,57 @@ http_app.get('/Shibboleth.sso/SLO/Redirect', function (req, res) { }); // ================================================================================ -// Web API handler +// Web API handlers // ================================================================================ http_app.get('/Shibboleth.sso/WaitLogin', function (req, res) { var sid = req.query.sid, waiter; if (!sid) { - res.set('Content-Type', 'text/plain'); - res.send('Provide sid as a query parameter'); + res.json({'code': -1, 'message': 'sid query parameter not set'}); return; } waiter = waitingLogins[sid]; if (!waiter) waitingLogins[sid] = waiter = makeWaiter(sid); waiter.listeners = waiter.listeners + 1; waiter.promise.then(function (jsonResponse) { + delete waitingLogins[sid]; res.json({'code': 0, 'message': 'OK', 'data': jsonResponse}); + }, function (err) { delete waitingLogins[sid]; + res.json({'code': 1, 'message': '' + err}); + }).done(); +}); + +http_app.get('/Shibboleth.sso/WaitLogout', function (req, res) { + var sid = req.query.sid, + saml = req.query.saml, + waiter, samlData; + if (!sid) { + res.json({'code': -1, 'message': 'sid query parameter not set'}); + return; + } + if (!saml) { + res.json({'code': -2, 'message': 'saml query parameter not set'}); + return; + } + try { + samlData = JSON.parse(saml); + } + catch (e) { + res.json({'code': -3, 'message': 'saml query parameter is not proper JSON: ' + e}); + return; + } + + waiter = waitingLogouts[sid]; + if (!waiter) waitingLogouts[sid] = waiter = makeDoubleWaiter(sid); + waiter.listeners = waiter.listeners + 1; + waiter.resolve(samlData); + waiter.responsePromise.then(function () { + delete waitingLogouts[sid]; + res.json({'code': 0, 'message': 'OK'}); }, function (err) { + delete waitingLogouts[sid]; res.json({'code': 1, 'message': '' + err}); - delete waitingLogins[sid]; }).done(); }); -- cgit v1.2.1