/* CodeQ: an online programming tutor. Copyright (C) 2015 UL FRI This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ var fs = require('fs'), url = require('url'), zlib = require('zlib'), querystring = require('querystring'), Promise = require('bluebird'), log4js = require('log4js'), // the logger express = require('express'), xmlCrypto = require('xml-crypto'), xmldom = require('xmldom'), xmlbuilder = require('xmlbuilder'), crypto = require('crypto'), bodyParser = require('body-parser'), http_app = express(), http_server = require('http').createServer(http_app); http_app.use(bodyParser.urlencoded({extended: true})); http_app.use(bodyParser.json()); http_server.listen(8085, 'localhost'); // ================================================================================ // The logger // ================================================================================ log4js.loadAppender('file'); log4js.addAppender(log4js.appenders.file(process.env.CODEQ_SAML_LOG || 'codeq-saml.log'), 'log'); var logger = log4js.getLogger('log'); var logException = function (msg, error) { if ((error instanceof Error) && (typeof error.stack === 'string')) msg += '\n' + error.stack; logger.error(msg); }; // prevent the node from exiting because of an unhandled exception Promise.onPossiblyUnhandledRejection(function (error) { logException('Unhandled promise rejection: ' + error, error); }); process.on('uncaughtException', function (error) { logException('Uncaught exception: ' + error, error); }); // ================================================================================ // SP and IdP definitions // ================================================================================ // SP (that's us) //--------------- // our URL where the IdP responds with a login result var assertionConsumerServiceURL = 'https://codeq.si/saml/assert', // must be a POST // our issuer ID SPIssuer = 'https://codeq.si/sp/201509281041', // our encryption key for signing requests privateCert = fs.readFileSync('/etc/shibboleth/sp-key.pem', 'utf8'), // the status value that signals a success of an operation (a SAML constant) SamlStatusSuccess = 'urn:oasis:names:tc:SAML:2.0:status:Success'; /* // IdP: idp.aai.arnes.si //---------------------- // the IdP's single-sign-on page URL, where a user is redirected to perform his/her login var SSOServiceURL = 'https://idp.aai.arnes.si/simplesaml/saml2/idp/SSOService.php', // must be a GET // the IdP's single-logout page URL, where a user is redirected to perform his/her (global) logout SLOServiceURL = 'https://idp.aai.arnes.si/simplesaml/saml2/idp/SingleLogoutService.php', // must be a GET // the IdP's issuer ID IdPIssuer = 'https://idp.aai.arnes.si/idp/20090116', // the IdP's public certificate to verify signatures with IdPKeyInfoProvider = new xmlCrypto.FileKeyInfo('/etc/shibboleth/idp.crt'); */ // IdP: idp.uni-lj.si //-------------------- /*var SSOServiceURL = 'https://idp.uni-lj.si/simplesaml/saml2/idp/SSOService.php', SLOServiceURL = 'https://idp.uni-lj.si/simplesaml/saml2/idp/SingleLogoutService.php',*/ var SSOServiceURL = 'https://codeq.si/idp/simplesaml/saml2/idp/SSOService.php', SLOServiceURL = 'https://codeq.si/idp/simplesaml/saml2/idp/SingleLogoutService.php', IdPIssuer = 'https://idp.uni-lj.si/idp/20100525', IdPKeyInfoProvider = new xmlCrypto.FileKeyInfo('/etc/shibboleth/idp.uni-lj.si.crt'); // ================================================================================ // Misc // ================================================================================ var idChars = '0123456789abcdef', makeRequestId = function () { var idParts = [], i; for (i = 0; i < 20; i++) idParts.push(idChars[Math.floor(Math.random() * 15)]); return idParts.join(''); }; var waitingLogins = {}, // maps sid to waiter waitingLogouts = {}, parkedLoginRequests = {}, // maps SAML request ID to waiter parkedLogoutRequests = {}; var makeSamlRedirectUrl = function (serviceUrl, queryKey, queryObj, additionalData, callback) { var xml = null; try { xml = xmlbuilder.create(queryObj).end(); logger.debug('Redirecting with ' + queryKey + ' to ' + serviceUrl + ':\n' + xml); zlib.deflateRaw(xml, function (err, buffer) { var svcUrl, signer, query, keys, i, key; if (err) { callback(err, null, xml); return; } query = { 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' }; query[queryKey] = buffer.toString('base64'); if (additionalData && (typeof additionalData === 'object')) { keys = Object.keys(additionalData); for (i = keys.length - 1; i >= 0; i--) { key = keys[i]; query[key] = additionalData[key]; } } signer = crypto.createSign('RSA-SHA1'); signer.update(querystring.stringify(query)); query.Signature = signer.sign(privateCert, 'base64'); svcUrl = url.parse(serviceUrl); svcUrl.query = query; delete svcUrl.search; callback(null, url.format(svcUrl), xml); }); } catch (e) { callback(e, null, xml); } }; var makeWaiter = function (sid) { var waiter = { 'sid': sid, 'created': Date.now(), 'listeners': 0 }; waiter.promise = new Promise(function (resolve, reject) { waiter.resolve = resolve; waiter.reject = reject; }); 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 // ================================================================================ var validateXmlSignature = function (xml, signatureNode) { var sig = new xmlCrypto.SignedXml(); sig.keyInfoProvider = IdPKeyInfoProvider; sig.loadSignature(signatureNode.toString()); return sig.checkSignature(xml); }; var xmlStatusOk = function (statusNode) { var node = statusNode.firstChild, attributes, i; while (node) { if (node.localName === 'StatusCode') { attributes = node.attributes; for (i = attributes.length - 1; i >= 0; i--) { if (attributes[i].localName === 'Value') return attributes[i].value == SamlStatusSuccess; } } node = node.nextSibling; } return null; }; var parseAttributeValue = function (attributeNode) { var node = attributeNode.firstChild; while (node) { if (node.localName === 'AttributeValue') { return node.firstChild.data; } node = node.nextSibling; } return null; }; var parseAttributes = function (attributeStatementNode) { var node = attributeStatementNode.firstChild, result = {}, attributes, i, name; while (node) { if (node.localName === 'Attribute') { attributes = node.attributes; for (i = attributes.length - 1; i >= 0; i--) { if (attributes[i].localName === 'Name') { name = attributes[i].value; break; } } if (i < 0) { logger.error('ERROR: an attribute without a name'); } else { result[name] = parseAttributeValue(node); } } node = node.nextSibling; } return result; }; // return the NameID in the format for use with xmlbuilder var parseNameID = function (NameIDNode) { var result = {}, i, attributes; result['#text'] = NameIDNode.firstChild.data; attributes = NameIDNode.attributes; for (i = attributes.length - 1; i >= 0; i--) { switch (attributes[i].localName) { case 'NameQualifier': // NameID namespace result['@NameQualifier'] = attributes[i].value; break; case 'SPNameQualifier': // 2nd level NameID namespace result['@SPNameQualifier'] = attributes[i].value; break; case 'Format': result['@Format'] = attributes[i].value; break; case 'SPProvidedID': result['@SPProvidedID'] = attributes[i].value; break; } } return result; }; var parseSubject = function (subjectNode) { var node = subjectNode.firstChild, result = {}; while (node) { if (node.localName === 'NameID') { result['NameID'] = parseNameID(node); } node = node.nextSibling; } return result; }; var parseAssertion = function (xml, assertionNode) { var node = assertionNode.firstChild, result = {}, attributes, i; while (node) { switch (node.localName) { case 'Signature': result.signatureOk = validateXmlSignature(xml, node); break; case 'Subject': // extract presenter identifier result.subject = parseSubject(node); break; case 'AuthnStatement': // extract the session index attributes = node.attributes; for (i = attributes.length - 1; i >= 0; i--) { if (attributes[i].localName === 'SessionIndex') { result.SessionIndex = attributes[i].value; break; } } break; case 'AttributeStatement': result.attributes = parseAttributes(node); break; } node = node.nextSibling; } return result; }; // ================================================================================ // SAML logout handlers // ================================================================================ // process response to our (global) logout request var handleLogoutResponse = function (xml, form, res) { var errors = [], issuer = null, status = null, waiter = null, responded = false, // don't respond if we already responded (and failed) doc, rootNode, attributes, i, inResponseTo, node, m; try { doc = new xmldom.DOMParser().parseFromString(xml); rootNode = doc.documentElement; if (rootNode.localName != 'LogoutResponse') { errors.push('not a LogoutResponse: ' + rootNode.localName); } else { attributes = rootNode.attributes; for (i = attributes.length - 1; i >= 0; i--) { if (attributes[i].localName === 'InResponseTo') { inResponseTo = attributes[i].value; break; } } if (!inResponseTo) errors.push('request ID that this message is a response to is missing'); else { waiter = parkedLogoutRequests[inResponseTo]; if (!waiter) errors.push('there is no session waiting on request ID ' + inResponseTo); else delete parkedLogoutRequests[inResponseTo]; } node = rootNode.firstChild; while (node) { switch (node.localName) { case 'Issuer': issuer = node.firstChild.data; break; case 'Status': status = xmlStatusOk(node); break; default: // errors.push('Unknown/unsupported top XML node: ' + node.localName + ', prefix: ' + node.prefix); break; } node = node.nextSibling; } if (issuer === null) errors.push('Issuer is not set'); else if (issuer !== IdPIssuer) errors.push('Invalid IdP issuer: ' + issuer); if (status === null) errors.push('response status is not set'); else if (!status) errors.push('response status is not OK'); } if (errors.length > 0) { if (waiter) { waiter.done = Date.now(); waiter.responseReject(new Error(errors.join(', '))); } if (errors.length > 1) m = 'Multiple errors:\n ' + errors.join('\n '); else m = 'Error: ' + errors[0]; logger.error('Received a SAML logout response and encountered errors processing it; response:\n' + xml + '\nErrors:\n ' + errors.join('\n ')); } else { if (waiter) { waiter.done = Date.now(); waiter.responseResolve(); } m = 'OK'; logger.debug('Received a SAML logout response:\n' + xml); } responded = true; res.set('Content-Type', 'text/plain'); res.send(m); } catch (e) { m = 'Error while processing a logout response: ' + e; logException(m + '\n' + xml, e); if (waiter) { waiter.done = Date.now(); waiter.responseReject(e); } if (!responded) { res.set('Content-Type', 'text/plain'); res.send(m); } } }; // a third party initiated the (global) logout, process the request and redirect with a response var handleLogoutRequest = function (xml, form, res) { var errors = [], nameId = null, sessionIndex = null, responded = false, // don't respond if we already responded (and failed) doc, rootNode, attributes, i, requestId, node, responseObj, responseId, m; try { doc = new xmldom.DOMParser().parseFromString(xml); rootNode = doc.documentElement; if (rootNode.localName != 'LogoutRequest') { errors.push('not a LogoutRequest: ' + rootNode.localName); } else { attributes = rootNode.attributes; for (i = attributes.length - 1; i >= 0; i--) { if (attributes[i].localName === 'ID') { requestId = attributes[i].value; break; } } if (!requestId) errors.push('request ID is missing'); node = rootNode.firstChild; while (node) { switch (node.localName) { case 'Issuer': if (node.firstChild.data !== IdPIssuer) errors.push('ERROR: invalid IdP issuer: ' + node.firstChild.data); break; case 'NameID': nameId = parseNameID(node); break; case 'SessionIndex': sessionIndex = node.firstChild && node.firstChild.data; break; } node = node.nextSibling; } if (!nameId) errors.push('NameID not present'); else { if (!nameId['#text']) errors.push('NameID is empty'); } } if (errors.length > 0) { logger.error('A third party requested a global SAML logout, but there were errors; original request:\n' + xml + '\nErrors:\n' + errors.join('\n')); responded = true; res.set('Content-Type', 'text/plain'); res.send(errors.join('\n')); } else { responseId = makeRequestId(); responseObj = { 'samlp:LogoutResponse': { '@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': responseId, '@IssueInstant': new Date().toISOString(), '@InResponseTo': requestId, 'saml:Issuer': { '#text': SPIssuer }, 'samlp:Status': { 'samlp:StatusCode': { '@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success' } } } }; makeSamlRedirectUrl(SLOServiceURL, 'SAMLRequest', responseObj, form.RelayState ? {RelayState: form.RelayState} : null, function (err, svcUrl, responseXml) { if (err) { logException('Error while assembling the SAML logout response; original request:\n' + xml + '\nError: ' + err, err); responded = true; res.set('Content-Type', 'text/plain'); res.send('Failed: ' + err); return; } logger.debug('Responding to a third party logout request:\n' + xml + '\nResponse:\n' + responseXml); responded = true; res.redirect(svcUrl); // redirect to the IdP }); } } catch (e) { m = 'Error while processing a third party logout request: ' + e; logException(m + '\n' + xml, e); if (!responded) { res.set('Content-Type', 'text/plain'); res.send(m); } } }; // ================================================================================ // SAML web request handlers // ================================================================================ // redirect to login at IdP, https://codeq.si/saml/Login http_app.get('/saml/Login', function (req, res) { var sid = req.query.sid, responded = false, // don't respond if we already responded (and failed) waiter, requestId, request; try { if (!sid) { logger.error('Login request without SID, query parameters: ' + JSON.stringify(req.query)); responded = true; res.set('Content-Type', 'text/plain'); res.send('SID not set'); return; } waiter = waitingLogins[sid]; if (!waiter) waitingLogins[sid] = waiter = makeWaiter(sid); requestId = makeRequestId(); request = { 'samlp:AuthnRequest': { '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol', '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion', '@Version': '2.0', '@ProtocolBinding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', '@AssertionConsumerServiceURL': assertionConsumerServiceURL, '@Destination': SSOServiceURL, '@ForceAuthn': 'true', '@ID': requestId, '@IssueInstant': new Date().toISOString(), 'saml:Issuer': { '#text': SPIssuer } } }; makeSamlRedirectUrl(SSOServiceURL, 'SAMLRequest', request, null, function (err, svcUrl, requestXml) { if (err) { logException('Error while assembling the Login SAML request for sid=' + sid + ': ' + err, err); waiter.done = Date.now(); waiter.reject(err instanceof Error ? err : new Error(err)); responded = true; res.set('Content-Type', 'text/plain'); res.send('Failed: ' + err); return; } logger.debug('Redirecting to IdP with a login request:\n' + requestXml); responded = true; res.redirect(svcUrl); // redirect to the IdP parkedLoginRequests[requestId] = waiter; waiter.parked = Date.now(); waiter.requestId = requestId; }); } catch (e) { logException('Exception while processing login request with query parameters ' + JSON.stringify(req.query) + ': ' + e, e); if (waiter) { waiter.done = Date.now(); waiter.reject(e); } if (!responded) { res.set('Content-Type', 'text/plain'); res.send('Error: ' + e); } } }); // redirect to logout at IdP, https://codeq.si/saml/Logout http_app.get('/saml/Logout', function (req, res) { var sid = req.query.sid, responded = false, waiter, requestId, request; try { if (!sid) { logger.error('Logout request without SID, query parameters: ' + JSON.stringify(req.query)); responded = true; res.set('Content-Type', 'text/plain'); res.send('SID not set'); return; } 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, requestXml) { if (err) { logException('Error while assembling the Logout SAML request for sid=' + sid + ': ' + err, err); waiter.done = Date.now(); waiter.responseReject(err instanceof Error ? err : new Error(err)); responded = true; res.set('Content-Type', 'text/plain'); res.send('Failed: ' + err); return; } logger.debug('Redirecting to IdP with a logout request:\n' + requestXml); responded = true; res.redirect(svcUrl); // redirect to the IdP parkedLogoutRequests[requestId] = waiter; waiter.parked = Date.now(); waiter.requestId = requestId; }); }) .catch(function (e) { logException('Exception while processing logout request: ' + e, e); if (waiter) { if (requestId) delete parkedLogoutRequests[requestId]; waiter.done = Date.now(); waiter.responseReject(e); } responded = true; res.set('Content-Type', 'text/plain'); res.send('Error: ' + e); }) .done(); } catch (e) { logException('Exception while processing logout request: ' + e, e); if (waiter) { waiter.done = Date.now(); waiter.reject(e); } if (!responded) { res.set('Content-Type', 'text/plain'); res.send('Error: ' + e); } } }); // user logged in, read the credentials http_app.post('/saml/assert', function (req, res) { var form = req.body, keys = [], errors = [], inResponseTo = null, issuer = null, signature = null, status = null, assertion = null, waiter = null, responded = false, // don't respond if we already responded (and failed) attributes, i, key, value, xml, doc, rootNode, node, NameID, SessionIndex, samlData, userData, m; try { value = form.SAMLResponse; if (!value) { errors.push('no SAMLResponse'); } else { xml = new Buffer(value, 'base64').toString('utf8'); doc = new xmldom.DOMParser().parseFromString(xml); rootNode = doc.documentElement; if (rootNode.localName != 'Response') { errors.push('the root XML node is not a Response: ' + rootNode.localName); } else { attributes = rootNode.attributes; for (i = attributes.length - 1; i >= 0; i--) { if (attributes[i].localName === 'InResponseTo') { inResponseTo = attributes[i].value; break; } } if (!inResponseTo) errors.push('request ID that this message is a response to is missing'); else { waiter = parkedLoginRequests[inResponseTo]; if (!waiter) errors.push('there is no session waiting on request ID ' + inResponseTo); else { waiter.done = Date.now(); delete parkedLoginRequests[inResponseTo]; } node = rootNode.firstChild; while (node) { switch (node.localName) { case 'Issuer': issuer = node.firstChild.data; break; case 'Signature': signature = validateXmlSignature(xml, node); break; case 'Status': status = xmlStatusOk(node); break; case 'Assertion': assertion = parseAssertion(xml, node); break; default: // errors.push('Unknown/unsupported top XML node: ' + node.localName + ', prefix: ' + node.prefix + '
\n'); break; } node = node.nextSibling; } if (issuer === null) errors.push('Issuer is not set'); else if (issuer !== IdPIssuer) errors.push('Invalid IdP issuer: ' + issuer); if (signature === null) errors.push('the response is not signed'); else if (!signature) errors.push('IdP signature is invalid'); if (status === null) errors.push('status is not set'); else if (!status) errors.push('response status is not OK'); if (!assertion) errors.push('no assertions supplied, no attributes present'); else { if ((typeof assertion.signatureOk !== 'undefined') && (!assertion.signatureOk)) errors.push('assertion signature is invalid'); samlData = {}; node = assertion.subject; if (!node) errors.push('assertion subject is not set'); else { node = node.NameID; if (!node) errors.push('assertion subject\'s NameID is not set'); else { if (!node['#text']) errors.push('assertion subject\'s NameID is empty'); samlData['NameID'] = node; } } if (assertion.SessionIndex) samlData['SessionIndex'] = assertion.SessionIndex; node = assertion.attributes; if (!node) errors.push('no attributes supplied'); else { userData = {}; samlData.userData = userData; keys = Object.keys(node); for (i = keys.length - 1; i >= 0; i--) { key = keys[i]; userData[key] = node[key]; } } } } } } if (errors.length > 0) { logger.debug('Errors processing a login response:\nSAMLResponse=' + value + '\nDecoded XML:\n' + xml + '\nErrors:\n' + errors.join('\n')); if (waiter) waiter.reject(new Error(errors.join(', '))); if (errors.length > 1) m = 'Multiple errors:\n ' + errors.join('\n '); else m = 'Error: ' + errors[0]; } else { logger.debug('Received a successful login response:\n' + xml); if (waiter) waiter.resolve(samlData); m = 'OK'; } responded = true; res.set('Content-Type', 'text/plain'); res.send(m); } catch (e) { m = 'Error while processing a login response: ' + e; if (xml) logException(m + '\n' + xml, e); else if (value) logException(m + '\nSAMLResponse=' + value, e); else logException(m, e); if (!responded) { res.set('Content-Type', 'text/plain'); res.send(m); } } }); // user logged out, the GET service http_app.get('/saml/slo', function (req, res) { var form = req.query, delegateToHandler = function (err, buffer, handler) { if (err) { logException('Error while deflating request: ' + err, err); res.send('Failed: ' + err); return; } handler(buffer.toString('utf8'), form, res); }; try { if (form.SAMLRequest) zlib.inflateRaw(new Buffer(form.SAMLRequest, 'base64'), function (err, buffer) { delegateToHandler(err, buffer, handleLogoutRequest); }); else if (form.SAMLResponse) zlib.inflateRaw(new Buffer(form.SAMLResponse, 'base64'), function (err, buffer) { delegateToHandler(err, buffer, handleLogoutResponse); }); else { logger.error('Invalid SAML logout request/response, query string: ' + JSON.stringify(req.query)); res.set('Content-Type', 'text/plain'); res.send('Invalid SAML logout request/response.'); } } catch (e) { logException('Error while processing a SAML logout request/response, querystring=' + JSON.stringify(req.query) + ': ' + e, e); try { res.set('Content-Type', 'text/plain'); res.send('Error: ' + e); } catch (e2) {} // ignore } }); // ================================================================================ // Web API handlers // ================================================================================ http_app.get('/saml/WaitLogin', function (req, res) { var sid = req.query.sid, waiter; if (!sid) { 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('/saml/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}); }).done(); }); // ================================================================================ // A periodic cleaner: rejects and purges timed out requests // ================================================================================ var cleanerTimer = setInterval(function () { // run every 10 seconds var youngestTime = Date.now() - 300000, // request timeout: 5 minutes keys = Object.keys(parkedLoginRequests), i, key, waiter; // parked logins: the user was already redirected at IdP for login for (i = keys.length - 1; i >= 0; i--) { key = keys[i]; waiter = parkedLoginRequests[key]; if (waiter.parked < youngestTime) { if (waiter.promise.isPending()) { logger.warn('A login request timed out at IdP: sid=' + waiter.sid + ', requestId=' + waiter.requestId) waiter.promise.reject(new Error('Request timed out waiting for IdP login reply')); } else { logger.info('Encountered a parked login request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId); } delete parkedLoginRequests[key]; delete waitingLogins[waiter.sid]; } } // waiting logins; the user's sid was registered, but the user itself may not have been redirected at IdP for login yet keys = Object.keys(waitingLogins); for (i = keys.length - 1; i >= 0; i--) { key = keys[i]; waiter = waitingLogins[key]; if ((waiter.parked || waiter.created) < youngestTime) { if (waiter.promise.isPending()) { if (waiter.parked) logger.warn('A login request, marked as parked but not encountered among parked requests, timed out at requester: sid=' + sid + ', requestId=' + waiter.requestId); else logger.warn('A login request timed out at requester, with no outstanding request at IdP: sid=' + waiter.sid); waiter.promise.reject(new Error('Request timed out waiting for user to start the login process')); } else { logger.info('Encountered a waiting login request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId); } delete waitingLogins[key]; if (waiter.requestId) delete parkedLoginRequests[waiter.requestId]; } } // parked logouts: the user was already redirected at IdP for logout, if the registration data was already provided, otherwise it's still waiting for redirection keys = Object.keys(parkedLogoutRequests); for (i = keys.length - 1; i >= 0; i--) { key = keys[i]; waiter = parkedLogoutRequests[key]; if (waiter.parked < youngestTime) { if (waiter.promise.isPending()) { logger.warn('A logout request timed out waiting for registration info: sid=' + waiter.sid); waiter.response.reject(new Error('Timed out waiting for registration info')); if (waiter.responsePromise.isPending()) waiter.responsePromise.reject(new Error('Timed out waiting for registration info')); } else if (waiter.responsePromise.isPending()) { logger.warn('A logout request timed out at IdP: sid=' + waiter.sid + ', requestId=' + waiter.requestId) waiter.responsePromise.reject(new Error('Request timed out waiting for IdP logout reply')); } else { logger.info('Encountered a parked logout request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId); } delete parkedLogoutRequests[key]; delete waitingLogouts[waiter.sid]; } } // waiting logouts; the user's sid and registration data was registered, but the user itself may not have been redirected at IdP for logout yet keys = Object.keys(waitingLogouts); for (i = keys.length - 1; i >= 0; i--) { key = keys[i]; waiter = waitingLogouts[key]; if ((waiter.parked || waiter.created) < youngestTime) { if (waiter.promise.isPending()) { if (waiter.parked) logger.warn('A logout request, marked as parked but not encountered among parked requests, timed out waiting for registration info: sid=' + sid + ', requestId=' + waiter.requestId); // this should be impossible: how can you go for logout at IdP if you have no registration info? else logger.warn('A logout request timed out waiting for registration info, with no outstanding request at IdP: sid=' + waiter.sid); waiter.response.reject(new Error('Timed out waiting for registration info')); if (waiter.responsePromise.isPending()) waiter.responsePromise.reject(new Error('Timed out waiting for registration info')); } else if (waiter.responsePromise.isPending()) { logger.warn('A logout request timed out at IdP: sid=' + waiter.sid + ', requestId=' + waiter.requestId) waiter.responsePromise.reject(new Error('Request timed out waiting for IdP logout reply')); } else { logger.info('Encountered a waiting logout request which has already been resolved: sid=' + waiter.sid + ', requestId=' + waiter.requestId); } delete waitingLogouts[key]; if (waiter.requestId) delete parkedLogoutRequests[waiter.requestId]; } } }, 10000);