-
Notifications
You must be signed in to change notification settings - Fork 477
Description
It's possible that passport-saml responds to IdP initiated logout request that sessions are closed even though sessions are still alive. This can happen at least in following situation:
- end user has blocked third party cookies
- SAML IdP is at another top level domain than SAML SP (passport-saml) application. Ie. SAML SP site is "third party site" from browsers' cookie handling point of view when browser is rendering page at SAML IdP site.
- end user triggers global logout from some other service (passport-saml receives IdP initiated LogoutRequest)
- SAML IdP has implemented logout propagation with combination of iframes and javascripts
- https://wiki.shibboleth.net/confluence/display/IDP30/LogoutConfiguration#LogoutConfiguration-Overview
- https://simplesamlphp.org/docs/stable/simplesamlphp-idp-more#section_1
Possible result from end use point of view: IdP reports that session related to passport-saml site is successfully closed even though it is not touched during logout process in anyway.
This problem has potential to hit a lot of end users when chrome starts to block third party cookies by default.
Following code is sending LogoutRequests to passport-saml like
- browser without third party cookie blocking enabled
- browser with third party cookie block enabled
console.logs written by example code are available at the end of the issue.
"use strict";
// Purpose: to demonstrate LogoutRequest handling issue.
// Drop this code to passport-saml's test/ directory and run tests.
// This is not meant to be added to passport-saml's testsuite as-is.
// This test code was "tested" against passport-saml version ac7939fc1f74c3a350cee99d68268de7391db41e
//
// Add following libraries to devDependencies
// "supertest": "4.0.2",
// "express-session": "1.17.0",
// "cookie-parser": "1.4.4",
// "chai": "4.2.0",
// "chai-string": "1.5.0",
// "memorystore": "1.6.1",
// "cookiejar": "2.1.2"
//
//
//
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const bodyParser = require("body-parser");
const SamlStrategy = require("../lib/passport-saml/index.js").Strategy;
const memorystore = require("memorystore")(session)
const supertest = require("supertest");
const chai = require("chai");
chai.use(require("chai-string"));
const expect = chai.expect;
const url = require("url");
const zlib = require("zlib");
const xmldom = require("xmldom");
const SignedXml = require("xml-crypto").SignedXml;
const fs = require("fs");
const xml2js = require("xml2js");
const cookiejar = require("cookiejar");
// These constants do not play any other role except to highlight
// that in order to replicate this issue with live SAML IdP & SP
// you have to setup those to different domains.
//
// SP must be in a domain which is from browsers' cookie handlng point
// of view a third party site when browser is executing SLO orchestration
// scripts at IdP site.
//
// For additional information about IdPs SLO behaviour / configuration:
// https://wiki.shibboleth.net/confluence/display/IDP30/LogoutConfiguration#LogoutConfiguration-Overview
// https://simplesamlphp.org/docs/stable/simplesamlphp-idp-more#section_1
const SAML_SP_DOMAIN = "passport-saml-powered-SAML-SP.qwerty.local";
const IDENTITY_PROVIDER_DOMAIN = "identity-provider.asdf.local";
// these endpoints consume:
// - IdP's responses to SP initiated login
// - IdP's responses to SP initiated logout (SLO logout responses with status of SLO process
// if user return from IdP to SP after SLO has been complited by IdP)
// - IdP initiated logout request when another SP which participates to same SSO
// session has triggered SLO. IdP ends up propagating logout request to this SAML SP instance.
// SP must respond with SAML logout response message which contains result of logout request
// processing at SP side (if session designated by logout request was terminated successfully along
// with possible application level sessions).
//
// Names/paths of these endpoints reflect these concepts:
// https://wiki.shibboleth.net/confluence/display/CONCEPT/MetadataForSP
const SP_SIDE_ASSERTION_CONSUME_SERVICE_ENDPOINT = "/samlsp/assertionconsumeserviceendpoint";
const SP_SIDE_SINGLE_LOGOUT_SERVICE_ENDPOINT = "/samlsp/singlelogoutserviceendpoint"
// term login initiator is borrowed from Shibboleth SP so that those who are
// more familiar with Shibboleth SP understand this endpoints role
const SP_LOGIN_INITIATOR = "/logininitiator";
const SECURED_CONTENT_ENDPOINT = "/secured";
const IDP_ENTRY_POINT_URL = "https://" + IDENTITY_PROVIDER_DOMAIN + "/idp";
const IDP_ISSUER = "idp-issuer";
const AUDIENCE = "https://" + SAML_SP_DOMAIN + "/samlsp";
const SP_ASSERTION_CONSUME_SERVICE_URL = "https://" + SAML_SP_DOMAIN + SP_SIDE_ASSERTION_CONSUME_SERVICE_ENDPOINT;
const SP_SINGLE_LOGOUT_SERVICE_URL = "https://" + SAML_SP_DOMAIN + SP_SIDE_SINGLE_LOGOUT_SERVICE_ENDPOINT;
const IDP_CERT = fs.readFileSync(__dirname + '/static/cert.pem');
const IDP_KEY = fs.readFileSync(__dirname + '/static/key.pem');
describe("IdP initiated SLO must work without cookies", function() {
it("reference case (with appliaction's session mgmt cookie available during LogoutRequest handling)", async function() {
// make it easier to spend some time
// at debug breakpoints
this.timeout(999999999);
const {app, sessionstore} = initializeSAMLSPApp();
const agent = supertest.agent(app);
// Host is masked to following value to
// "document" requests which are made to saml sp
// in a produced debug log
agent.set("Host", SAML_SP_DOMAIN);
// check that secured content cannot be accessed prior
// to authenctication
let res = await agent.get(SECURED_CONTENT_ENDPOINT);
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(403);
// initiate login process
res = await agent.get(SP_LOGIN_INITIATOR)
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(302);
expect(res.header.location).to.startWith(IDP_ENTRY_POINT_URL + "?SAMLRequest=");
const NAME_ID = "aaaaaaaaa@aaaaaaaa.local";
const SESSION_INDEX = "_1111111111111111111111";
const inResponseTo = "firstAuthnRequest";
// end user has been authenticate at IdP and is forwarded back to SP
res = await agent.post(SP_SIDE_ASSERTION_CONSUME_SERVICE_ENDPOINT)
.send("SAMLResponse=" + encodeURIComponent(buildLoginResponse(NAME_ID, SESSION_INDEX, inResponseTo)));
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(302);
// he/she can now access following content
res = await agent.get(SECURED_CONTENT_ENDPOINT);
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(200);
expect(res.text).to.contain(NAME_ID);
//
// Now end user decides to use other services which participate to same IdP SSO.
//
// Time goes by and finally end user decides to trigger SLO process.
// SLO is initiated from some random SAML SP enabled service. From
// this case point of view it is important to keep in mind that this
// passport-saml instance didn't trigger it.
//
// If IdP is Shibboleth IdPv3 end user ends up to this process
// https://wiki.shibboleth.net/confluence/display/IDP30/LogoutConfiguration#LogoutConfiguration-Overview
// "....user chooses SLO, the logout-propagate.vm view is rendered and the browser mediates (i.e. front-channel)
// a series of logout messages coordinated via iframes, javascript...".
//
// Links to actual implementation:
// https://git.shibboleth.net/view/?p=java-identity-provider.git;a=blob;f=idp-conf/src/main/resources/views/logout-propagate.vm;h=86b3fa14d650073428c3688aabddee6f5f49bb47;hb=refs/heads/maint-3.4
// https://git.shibboleth.net/view/?p=java-identity-provider.git;a=blob;f=idp-conf/src/main/resources/system/views/logout/propagate.vm;h=8a71905a5831714cb0a317321c5a72524922130f;hb=refs/heads/maint-3.4
//
// Following http request is executed from a browser which is currently rendering
// logout-propage page at IdP site:
await callSLOEndpointAndAssertResult(sessionstore, agent, NAME_ID, SESSION_INDEX);
// Logout propagation page at IdP site has switched status of "passport-saml site" to indicate that
// user was successfully logged out (because passport-saml returned LogoutResponse with status Success).
//
// End user walks away from computer thinking that he/she does not have any open login sessions
// anymore.
//
// After few moments user or someone with the access to computer writes to browser's
// address bar following address which must not be available without authentication
// (remember that moments ago IdP indicated that all sessions were terminated)
//
// access to secured content must not work
res = await agent.get(SECURED_CONTENT_ENDPOINT);
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(403);
// Everything worked (access was blocked) as expected (in this case).
// Move on to the next case...
});
it("IdP initiated LogoutRequest must work without additional information from e.g. cookies", async function() {
// this is similar case than "reference case" until IdP-initiated LogoutRequest is sent
// from browser.
//
// In this case end user has configured his/her browser to block third party cookies.
//
// NOTE: chrome is planning to block third party cookies by default so this scenario
// is going to be very common.
//
//
this.timeout(999999999);
const {app, sessionstore} = initializeSAMLSPApp();
const agent = supertest.agent(app);
agent.set("Host", SAML_SP_DOMAIN);
let res = await agent.get(SECURED_CONTENT_ENDPOINT);
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(403);
res = await agent.get(SP_LOGIN_INITIATOR)
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(302);
expect(res.header.location).to.startWith(IDP_ENTRY_POINT_URL + "?SAMLRequest=");
const NAME_ID = "bbbbbbbbbbbbbbb@bbbbbbbbbbb.local";
const SESSION_INDEX = "_222222222222222222222222";
const inResponseTo = "secondAuthnRequest";
res = await agent.post(SP_SIDE_ASSERTION_CONSUME_SERVICE_ENDPOINT)
.send("SAMLResponse=" + encodeURIComponent(buildLoginResponse(NAME_ID, SESSION_INDEX, inResponseTo)));
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(302);
// he/she can now access following content
res = await agent.get(SECURED_CONTENT_ENDPOINT);
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(200);
expect(res.text).to.contain(NAME_ID);
// Proceeding to SLO...
//
// This is similar situation with "reference case" but because third party cookies are blocked
// by end user's browser following request is executed without express-session's session cookie.
// HTTP calls from within iframe do not contain cookies when third party cookies are blocked.
//
// This situation is simulated by this demonstration code so that superagent's cookiejar is cleared
// prior to following HTTP call.
//
// store current cookies so that these can be restored
const cookies = agent.jar.getCookies(cookiejar.CookieAccessInfo.All);
//console.log("cookies\n:" + cookies);
// clear cookie jar by expiring each cookie (cookiejar doesn't provide clear all or similar function)
cookies.forEach(function(cookie) {
const modifiedExpiration = new cookiejar.Cookie("" + cookie);
modifiedExpiration.expiration_date = 0;
agent.jar.setCookie(modifiedExpiration);
});
// execute same IdP-initiated logout request as in "reference case"
await callSLOEndpointAndAssertResult(sessionstore, agent, NAME_ID, SESSION_INDEX);
// restore cookiejar content. NOTE: this is not something that end user would
// have to do in order to replicate this issue. His/her browser remembers cookies and
// uses thosee when he/she navigates back to site via bookmark or via some weblink.
agent.jar.setCookies(cookies);
// passport-saml returned logout response with Success.
// This means that passport-saml was able to logout user successfully using information
// in logout request (nameId and sessionIndex) or passport-saml failed and reported
// Success anyway.
//
// Eitherway:
// Logout propagation page at IdP site has switched status of "passport-saml site" to indicate that
// user was successfully logged out (because passport-saml returned LogoutResponse with status Success).
//
// End user walks away from computer thinking that he/she does not have any open login sessions
// anymore.
//
// After few moments user or someone with the access to computer writes to browser's
// address bar following address which must not be available without authentication
// (remember that moments ago IdP indicated that all sessions were terminated)
//
// access to secured content must not work
res = await agent.get(SECURED_CONTENT_ENDPOINT);
logRequestResponse(sessionstore, res);
expect(res.statusCode).to.equal(403);
// this case failed (content was returned)...what went wrong?
//
// This didn't work during LogoutRequest handling:
// req.logout()
// https://github.com/bergie/passport-saml/blob/v1.2.0/lib/passport-saml/strategy.js#L46
// It was executed without checking if session is same as
// SAML logout request indicated with nameId and sessionIndex values.
//
// It was not checked if req contains authenticated session in the first place.
//
// LogoutRequest processing returns always Success if request's signature is valid. There
// aren't any additional verifications whether correct session (or session at all) is
// terminated when LogoutRequest was handled.
});
});
async function callSLOEndpointAndAssertResult(sessionstore, agent, nameId, sessionIndex) {
const idpInitiatedLogoutRequest = buildIdPInitiatedLogoutRequest(nameId, sessionIndex);
const urlEncodedLogoutRequest = "SAMLRequest=" + encodeURIComponent(idpInitiatedLogoutRequest);
const res = await agent.post(SP_SIDE_SINGLE_LOGOUT_SERVICE_ENDPOINT).send(urlEncodedLogoutRequest);
logRequestResponse(sessionstore, res,
urlEncodedLogoutRequest + "\n\nSAML request as decoded:\n" + Buffer.from(idpInitiatedLogoutRequest, "base64"));
// Check that logout response is: "session designated by nameId&sessionIndex is terminated successfully"
expect(res.statusCode).to.equal(302);
expect(res.header.location).to.startWith(IDP_ENTRY_POINT_URL + "?SAMLResponse=");
const logoutResponse = getSamlMessageFromRedirectResponse(res);
const logoutResponseJson = await xml2js.parseStringPromise(logoutResponse);
// console.log("XML\n" + logoutResponse + "\nXML2JS:\n" + JSON.stringify( logoutResponseJson, null, 2));
expect(logoutResponseJson["samlp:LogoutResponse"]["samlp:Status"][0]["samlp:StatusCode"][0]['$'].Value)
.to
.equal("urn:oasis:names:tc:SAML:2.0:status:Success")
}
function initializeSAMLSPApp() {
const app = express();
const memoryStoreInstance = new memorystore({checkPeriod: 86400000});
app.use(bodyParser.urlencoded({encoded: true}));
app.use(session({
// this is false so that session is written to memory store
// when it is authenticated. Makes it easier to demonstrate
// passport-saml SLO issue.
saveUninitialized: false,
secret: "secret_used_to_sign_cookie",
store: memoryStoreInstance
}));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser( function(user, done) { done(null, user); } );
passport.deserializeUser( function(user, done) { done(null, user); } );
passport.use(new SamlStrategy({
callbackUrl: SP_ASSERTION_CONSUME_SERVICE_URL,
entryPoint: IDP_ENTRY_POINT_URL,
issuer: "passport-saml-issuer",
// in response to validation disabled
// because it is irrelevant from SLO logout issue
// demonstration case point of view
validateInResponseTo: false,
cert: IDP_CERT.toString(),
acceptedClockSkewMs: 0,
idpIssuer: IDP_ISSUER,
audience: AUDIENCE,
}, function(profile, done) { done(null, profile) }));
app.get(SP_LOGIN_INITIATOR, passport.authenticate("saml", {} ));
app.post(SP_SIDE_ASSERTION_CONSUME_SERVICE_ENDPOINT, passport.authenticate("saml", {} ), function(req, res){res.redirect("/")});
app.post(SP_SIDE_SINGLE_LOGOUT_SERVICE_ENDPOINT, passport.authenticate("saml", {} ));
// this endpoint is used to test whether user has authenticated session or not
app.get(SECURED_CONTENT_ENDPOINT, function(req, res) {
if (req.isAuthenticated()) {
res.send("hello " + req.user.nameID);
} else {
res.sendStatus(403);
}
});
return {
app: app,
sessionstore: memoryStoreInstance.store
}
}
function buildIdPInitiatedLogoutRequest(nameId, sessionIndex) {
// const nameId = "asdf@qwerty.local";
// const sessionIndex = "_ababababababababababab";
const issuer = IDP_ISSUER;
const spNameQualifier = AUDIENCE;
const destination = SP_SINGLE_LOGOUT_SERVICE_URL;
const idpInitiatedLogoutRequest =
`<samlp:LogoutRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_adcdabcd"
Version="2.0"
IssueInstant="2020-01-01T01:01:00Z"
Destination="${destination}">
<saml:Issuer>${issuer}</saml:Issuer>
<saml:NameID
SPNameQualifier="${spNameQualifier}"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">${nameId}</saml:NameID>
<samlp:SessionIndex>${sessionIndex}</samlp:SessionIndex>
</samlp:LogoutRequest>`
return Buffer.from(signXml(idpInitiatedLogoutRequest)).toString("base64");
}
function buildLoginResponse(nameId, sessionIndex, inResponseTo) {
// const nameId = "asdf@qwerty.local";
// const inResponseTo = "_ccccccccccccc";
// const sessionIndex = "_ababababababababababab";
const notBefore = "1980-01-01T01:00:00Z"
const issueInstant = "1980-01-01T01:01:00Z";
const notOnOrAfter = "4980-01-01T01:01:00Z";
const issuer = IDP_ISSUER;
const audience = AUDIENCE;
const destination = SP_ASSERTION_CONSUME_SERVICE_URL;
const loginResponse =
`<samlp:Response
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Version="2.0"
IssueInstant="${issueInstant}"
Destination="${destination}"
InResponseTo="${inResponseTo}">
<saml:Issuer>${issuer}</saml:Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
ID="_bbbbbbbbbbbbbbbbbbbbbbbb"
Version="2.0" IssueInstant="${issueInstant}">
<saml:Issuer>${issuer}</saml:Issuer>
<saml:Subject>
<saml:NameID
SPNameQualifier="${audience}"
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">${nameId}</saml:NameID>
<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
NotOnOrAfter="${notOnOrAfter}"
Recipient="${destination}"
InResponseTo="${inResponseTo}"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions
NotBefore="${notBefore}"
NotOnOrAfter="${notOnOrAfter}">
<saml:AudienceRestriction>
<saml:Audience>${audience}</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement
AuthnInstant="${issueInstant}"
SessionNotOnOrAfter="${notOnOrAfter}"
SessionIndex="${sessionIndex}">
<saml:AuthnContext>
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
</samlp:Response>`
return Buffer.from(signXml(loginResponse)).toString("base64");
}
function signXml(xml) {
const sig = new SignedXml();
sig.addReference('/*',
["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"],
"http://www.w3.org/2001/04/xmlenc#sha256", "", "", "", false
);
sig.signatureAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
sig.signingKey = IDP_KEY;
sig.computeSignature(xml);
return sig.getSignedXml();
}
function logRequestResponse(sessionstore, res, requestBody) {
let msg = "---- BEGIN ----------------------------------------------------------------\n";
msg += "HTTP REQUEST:\n";
msg += res.req._header + (requestBody ? requestBody : "");
msg += "\n\nHTTP RESPONSE:\n";
msg += "HTTP/" + res.res.httpVersion + " " + res.statusCode + " " + res.res.statusMessage + "\n";
Object.keys(res.header).forEach(function (header) {msg += header + ": " + res.header[header] + "\n"});
msg += "\n";
msg += res.text ? res.text : "";
let xml = getSamlMessageFromRedirectResponse(res);
if (xml) {
msg += "\n\n-------\n";
msg += "HTTP redirect response's saml message decoded:\n" + xml + "\n";
}
msg += "\n\n-------\n";
msg += "Content of session store per sessionId AFTER the request has been processed:\n";
sessionstore.keys().forEach(function(sid) {
msg += "content of " + sid + " :\n" + JSON.stringify(JSON.parse(sessionstore.get(sid)), null, 2) + "\n"
});
msg += "\n---- END -------------------------------------------------------------------\n";
console.log(msg);
}
function getSamlMessageFromRedirectResponse(res) {
if (res.header && res.header.location) {
const location = url.parse(res.header.location, true);
if (location.query['SAMLRequest']) {
return decodeXmlMessage(location.query['SAMLRequest']);
} else
if (location.query['SAMLResponse']) {
return decodeXmlMessage(location.query['SAMLResponse']);
}
}
}
function decodeXmlMessage(msg) {
const decoded = Buffer.from(msg, "base64");
const inflated = Buffer.from(zlib.inflateRawSync(decoded), "utf-8");
return new xmldom.DOMParser({}).parseFromString(inflated.toString());
}Output of reference case (with appliaction's session mgmt cookie available during LogoutRequest handling)
... logs after SAML login is completed .....
---- BEGIN ----------------------------------------------------------------
HTTP REQUEST:
GET /secured HTTP/1.1
Host: passport-saml-powered-SAML-SP.qwerty.local
Accept-Encoding: gzip, deflate
User-Agent: node-superagent/3.8.3
Cookie: connect.sid=s%3AwKYuE4TZyfzrUBP8yLJHYy53n4KlxC0s.DUFFvSzgv%2BKT3pjUb7O7jnh%2BlVB7o2pjAvDoixhz5%2FE
Connection: close
HTTP RESPONSE:
HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 30
etag: W/"1e-pkiapmYFJr5Z2ZLWfKJs1kp+5k4"
date: Sun, 02 Feb 2020 17:17:07 GMT
connection: close
hello aaaaaaaaa@aaaaaaaa.local
-------
Content of session store per sessionId AFTER the request has been processed:
content of wKYuE4TZyfzrUBP8yLJHYy53n4KlxC0s :
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"passport": {
"user": {
"issuer": "idp-issuer",
"inResponseTo": "firstAuthnRequest",
"sessionIndex": "_1111111111111111111111",
"nameID": "aaaaaaaaa@aaaaaaaa.local",
"nameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
"spNameQualifier": "https://passport-saml-powered-SAML-SP.qwerty.local/samlsp"
}
}
}
---- END -------------------------------------------------------------------
---- BEGIN ----------------------------------------------------------------
HTTP REQUEST:
POST /samlsp/singlelogoutserviceendpoint HTTP/1.1
Host: passport-saml-powered-SAML-SP.qwerty.local
Accept-Encoding: gzip, deflate
User-Agent: node-superagent/3.8.3
Content-Type: application/x-www-form-urlencoded
Cookie: connect.sid=s%3AwKYuE4TZyfzrUBP8yLJHYy53n4KlxC0s.DUFFvSzgv%2BKT3pjUb7O7jnh%2BlVB7o2pjAvDoixhz5%2FE
Content-Length: 2150
Connection: close
SAMLRequest=PHNhbWxwOkxvZ291dFJlcXVlc3QgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il9hZGNkYWJjZCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDEtMDFUMDE6MDE6MDBaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9wYXNzcG9ydC1zYW1sLXBvd2VyZWQtU0FNTC1TUC5xd2VydHkubG9jYWwvc2FtbHNwL3NpbmdsZWxvZ291dHNlcnZpY2VlbmRwb2ludCI%2BCiAgICA8c2FtbDpJc3N1ZXI%2BaWRwLWlzc3Vlcjwvc2FtbDpJc3N1ZXI%2BCiAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3Bhc3Nwb3J0LXNhbWwtcG93ZXJlZC1TQU1MLVNQLnF3ZXJ0eS5sb2NhbC9zYW1sc3AiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50Ij5hYWFhYWFhYWFAYWFhYWFhYWEubG9jYWw8L3NhbWw6TmFtZUlEPgogICAgPHNhbWxwOlNlc3Npb25JbmRleD5fMTExMTExMTExMTExMTExMTExMTExMTwvc2FtbHA6U2Vzc2lvbkluZGV4Pgo8U2lnbmF0dXJlIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48U2lnbmVkSW5mbz48Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8%2BPFJlZmVyZW5jZSBVUkk9IiNfYWRjZGFiY2QiPjxUcmFuc2Zvcm1zPjxUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L1RyYW5zZm9ybXM%2BPERpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPjxEaWdlc3RWYWx1ZT50RlF1V0luWjJMV1NiQ2lFeE1IeXg0WjhuUVRrbmxabEtVZE1qeVY0WUw0PTwvRGlnZXN0VmFsdWU%2BPC9SZWZlcmVuY2U%2BPC9TaWduZWRJbmZvPjxTaWduYXR1cmVWYWx1ZT5nUHRQcDM3T29YN0MyUEMvdmpGRlViUnF4NVRUNnZ6UkJxYk1XWHdDTmxsOHIrOE8wblJUMHNRcHpxaDlUbk5CTTlBejBYdjhpSkFCMTNBWHQvWGNFY3NBTSswZEdFRmd5dlZHV25hdTFHd3h0MjV5Nm1Bblo5NVhIK2txUHNUZTJaRHVzK3k4aWtZL1drY0FIZ1lGOEFJb1dWY2VsdmNUalhQSk5zYTJOMllDdUtVZXBMMVlBRmcxM2x6NnhHNjJMUEtlV2Frd0M2VlBMQ2FlWVpwaVpucElXWENWVkR6NFByL1FUZWpsSng2NFJ1eGthYXBQK3JZMkpwaTVmL0VNNStYTmJQQlVOT2xnTFdxalI3YkFxekpXQ2pVZHBvbndxVjBCMTBOeFdTZGQ3aEk4eXhaODllODZjcEhuYjZoMWM2WGExZmk1MFp3T29rMGkwN0tja0E9PTwvU2lnbmF0dXJlVmFsdWU%2BPC9TaWduYXR1cmU%2BPC9zYW1scDpMb2dvdXRSZXF1ZXN0Pg%3D%3D
SAML request as decoded:
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_adcdabcd" Version="2.0" IssueInstant="2020-01-01T01:01:00Z" Destination="https://passport-saml-powered-SAML-SP.qwerty.local/samlsp/singlelogoutserviceendpoint">
<saml:Issuer>idp-issuer</saml:Issuer>
<saml:NameID SPNameQualifier="https://passport-saml-powered-SAML-SP.qwerty.local/samlsp" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">aaaaaaaaa@aaaaaaaa.local</saml:NameID>
<samlp:SessionIndex>_1111111111111111111111</samlp:SessionIndex>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><Reference URI="#_adcdabcd"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>tFQuWInZ2LWSbCiExMHyx4Z8nQTknlZlKUdMjyV4YL4=</DigestValue></Reference></SignedInfo><SignatureValue>gPtPp37OoX7C2PC/vjFFUbRqx5TT6vzRBqbMWXwCNll8r+8O0nRT0sQpzqh9TnNBM9Az0Xv8iJAB13AXt/XcEcsAM+0dGEFgyvVGWnau1Gwxt25y6mAnZ95XH+kqPsTe2ZDus+y8ikY/WkcAHgYF8AIoWVcelvcTjXPJNsa2N2YCuKUepL1YAFg13lz6xG62LPKeWakwC6VPLCaeYZpiZnpIWXCVVDz4Pr/QTejlJx64RuxkaapP+rY2Jpi5f/EM5+XNbPBUNOlgLWqjR7bAqzJWCjUdponwqV0B10NxWSdd7hI8yxZ89e86cpHnb6h1c6Xa1fi50ZwOok0i07KckA==</SignatureValue></Signature></samlp:LogoutRequest>
HTTP RESPONSE:
HTTP/1.1 302 Found
x-powered-by: Express
location: https://identity-provider.asdf.local/idp?SAMLResponse=fVFNa8MwDP0rwfd8NDTpMG3KWC%2BF7rKWHnYZqq12gcQ2llK2fz8tXVkHo6CLnt6Tnp%2Fny4%2B%2BS84YqfVuoSZZoZbNnKDvgt74kx%2F4BSl4R5gI0ZEeRws1RKc9UEvaQY%2Bk2ejt4%2FNGl1mhQ%2FTsje%2FUjeS%2BAogwsjhQyXq1UG%2BHCqaItq7K49TUMHmYTY1K9leXIhEi0YBrRwyOBSrKIi1Kqd1kpqWKWVZX9atKVkjcOuBR%2Bc4cSOd5a9Fxy5%2BpeD1LEzMge8w6b6CTYZD17vrwnRdHYI2Fg7HqEo4er8cmiPPgI6ffYNqO4Dy%2FZfxkuWXggf52T95isoduwPvp0MjW28EYJFJ5c7nwuzT%2F77%2BaLw%3D%3D
content-length: 0
date: Sun, 02 Feb 2020 17:17:07 GMT
connection: close
-------
HTTP redirect response's saml message decoded:
<?xml version="1.0"?><samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_b5a4eed652f4c6a1874c" Version="2.0" IssueInstant="2020-02-02T17:17:07.656Z" Destination="https://identity-provider.asdf.local/idp" InResponseTo="_adcdabcd"><saml:Issuer>passport-saml-issuer</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status></samlp:LogoutResponse>
-------
Content of session store per sessionId AFTER the request has been processed:
content of wKYuE4TZyfzrUBP8yLJHYy53n4KlxC0s :
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"passport": {}
}
---- END -------------------------------------------------------------------
---- BEGIN ----------------------------------------------------------------
HTTP REQUEST:
GET /secured HTTP/1.1
Host: passport-saml-powered-SAML-SP.qwerty.local
Accept-Encoding: gzip, deflate
User-Agent: node-superagent/3.8.3
Cookie: connect.sid=s%3AwKYuE4TZyfzrUBP8yLJHYy53n4KlxC0s.DUFFvSzgv%2BKT3pjUb7O7jnh%2BlVB7o2pjAvDoixhz5%2FE
Connection: close
HTTP RESPONSE:
HTTP/1.1 403 Forbidden
x-powered-by: Express
content-type: text/plain; charset=utf-8
content-length: 9
etag: W/"9-PatfYBLj4Um1qTm5zrukoLhNyPU"
date: Sun, 02 Feb 2020 17:17:07 GMT
connection: close
Forbidden
-------
Content of session store per sessionId AFTER the request has been processed:
content of wKYuE4TZyfzrUBP8yLJHYy53n4KlxC0s :
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"passport": {}
}
---- END -------------------------------------------------------------------
Output of IdP initiated LogoutRequest must work without additional information from e.g. cookies
... logs after SAML login is completed .....
---- BEGIN ----------------------------------------------------------------
HTTP REQUEST:
GET /secured HTTP/1.1
Host: passport-saml-powered-SAML-SP.qwerty.local
Accept-Encoding: gzip, deflate
User-Agent: node-superagent/3.8.3
Cookie: connect.sid=s%3A-9Ii5aSBusDH0PO7Ec3qaEhJ1io7zyCa.rf3kRGWh6%2FDNURxdpRfiKk%2FTpL%2FIsje3byfPjDphkNg
Connection: close
HTTP RESPONSE:
HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 39
etag: W/"27-78k8vTxTl1L2wj/JDDGs3fBzmvk"
date: Sun, 02 Feb 2020 17:21:50 GMT
connection: close
hello bbbbbbbbbbbbbbb@bbbbbbbbbbb.local
-------
Content of session store per sessionId AFTER the request has been processed:
content of -9Ii5aSBusDH0PO7Ec3qaEhJ1io7zyCa :
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"passport": {
"user": {
"issuer": "idp-issuer",
"inResponseTo": "secondAuthnRequest",
"sessionIndex": "_222222222222222222222222",
"nameID": "bbbbbbbbbbbbbbb@bbbbbbbbbbb.local",
"nameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
"spNameQualifier": "https://passport-saml-powered-SAML-SP.qwerty.local/samlsp"
}
}
}
---- END -------------------------------------------------------------------
---- BEGIN ----------------------------------------------------------------
HTTP REQUEST:
POST /samlsp/singlelogoutserviceendpoint HTTP/1.1
Host: passport-saml-powered-SAML-SP.qwerty.local
Accept-Encoding: gzip, deflate
User-Agent: node-superagent/3.8.3
Content-Type: application/x-www-form-urlencoded
Content-Length: 2162
Connection: close
SAMLRequest=PHNhbWxwOkxvZ291dFJlcXVlc3QgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il9hZGNkYWJjZCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMjAtMDEtMDFUMDE6MDE6MDBaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9wYXNzcG9ydC1zYW1sLXBvd2VyZWQtU0FNTC1TUC5xd2VydHkubG9jYWwvc2FtbHNwL3NpbmdsZWxvZ291dHNlcnZpY2VlbmRwb2ludCI%2BCiAgICA8c2FtbDpJc3N1ZXI%2BaWRwLWlzc3Vlcjwvc2FtbDpJc3N1ZXI%2BCiAgICA8c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3Bhc3Nwb3J0LXNhbWwtcG93ZXJlZC1TQU1MLVNQLnF3ZXJ0eS5sb2NhbC9zYW1sc3AiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50Ij5iYmJiYmJiYmJiYmJiYmJAYmJiYmJiYmJiYmIubG9jYWw8L3NhbWw6TmFtZUlEPgogICAgPHNhbWxwOlNlc3Npb25JbmRleD5fMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyPC9zYW1scDpTZXNzaW9uSW5kZXg%2BCjxTaWduYXR1cmUgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxTaWduZWRJbmZvPjxDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8%2BPFNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48UmVmZXJlbmNlIFVSST0iI19hZGNkYWJjZCI%2BPFRyYW5zZm9ybXM%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8%2BPFRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvVHJhbnNmb3Jtcz48RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8%2BPERpZ2VzdFZhbHVlPlRFeExvVGRYS3Q0dEtQK0l3cTU4OHlGbnF2VnVvNDVHOUo3T2E3SkZiUjA9PC9EaWdlc3RWYWx1ZT48L1JlZmVyZW5jZT48L1NpZ25lZEluZm8%2BPFNpZ25hdHVyZVZhbHVlPmFGM3YvZkNwWE5HdXJiTkZDUm1XVVZzcDhleEpKRFdYNkZGRXZ1bkVGcFhIQlFiZDdMR3VqQnNIMEZ3Z2Y2SUd2dncxeDArS2pwZnBLTTZLK2pzUGVTUTVSSVJSeTJKM3dlbENKMlhxUStMRjMyR2ZoWW9tM3ZYRm1lL2t2ajdlemdpVHEzV0dUNTQzS0FPWDNjMnArbk1yUkJQL1VBT3EyQm9iNUZXREhPbFluOXFEelZSc3p2VUkvQWZkOTNrSkV4N2tHV1hKdlFCalVhbUxOd1RrbW0wUmlGSHJQblg0cDV1U2xzT3ZBUTJ1UVhyclUxOUR2UE0zUWczTlVJMnorOW5xWWY0NWxJR05NamVkOFFqVXFqNG8yYm1wYTFPQkpIMitjK01tVW9nVXdsbC9idlNac21aa2haMEZybXRhTEJiT0hYWGhkYjBHMmNwOGFuQjlWdz09PC9TaWduYXR1cmVWYWx1ZT48L1NpZ25hdHVyZT48L3NhbWxwOkxvZ291dFJlcXVlc3Q%2B
SAML request as decoded:
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_adcdabcd" Version="2.0" IssueInstant="2020-01-01T01:01:00Z" Destination="https://passport-saml-powered-SAML-SP.qwerty.local/samlsp/singlelogoutserviceendpoint">
<saml:Issuer>idp-issuer</saml:Issuer>
<saml:NameID SPNameQualifier="https://passport-saml-powered-SAML-SP.qwerty.local/samlsp" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">bbbbbbbbbbbbbbb@bbbbbbbbbbb.local</saml:NameID>
<samlp:SessionIndex>_222222222222222222222222</samlp:SessionIndex>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><Reference URI="#_adcdabcd"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>TExLoTdXKt4tKP+Iwq588yFnqvVuo45G9J7Oa7JFbR0=</DigestValue></Reference></SignedInfo><SignatureValue>aF3v/fCpXNGurbNFCRmWUVsp8exJJDWX6FFEvunEFpXHBQbd7LGujBsH0Fwgf6IGvvw1x0+KjpfpKM6K+jsPeSQ5RIRRy2J3welCJ2XqQ+LF32GfhYom3vXFme/kvj7ezgiTq3WGT543KAOX3c2p+nMrRBP/UAOq2Bob5FWDHOlYn9qDzVRszvUI/Afd93kJEx7kGWXJvQBjUamLNwTkmm0RiFHrPnX4p5uSlsOvAQ2uQXrrU19DvPM3Qg3NUI2z+9nqYf45lIGNMjed8QjUqj4o2bmpa1OBJH2+c+MmUogUwll/bvSZsmZkhZ0FrmtaLBbOHXXhdb0G2cp8anB9Vw==</SignatureValue></Signature></samlp:LogoutRequest>
HTTP RESPONSE:
HTTP/1.1 302 Found
x-powered-by: Express
location: https://identity-provider.asdf.local/idp?SAMLResponse=fVHBbsIwDP2VKve2abcysKBoGhckdhmIwy6TSc1WqSRR7KLt75eVoTEJIeXi5%2Ffs55fp%2FPPQJUcK3Do7U0Wm1byeMh46Dyv37np5IfbOMiWRaBmG1kz1wYJDbhksHohBDKwfn1dQZhp8cOKM69SF5LYCmSlIdKCS5WKm3mhfaJqM72hU4ei%2B2lVYjVWyPbuMkkhk7mlpWdBKhHSpU13GtykeoCyg0tmkGL2qZEEsrUUZlB8iniHP24astPKVRq%2FHWIQMudlnnTPYxaaP4%2B358I2LjrAxDe5Mo07hwLA91D469y5I%2BgOm7QBO80vGb5ZrQen5f%2FXkGkq22PV0Ox0e2LDujSFmldenDX9D82v%2FVX8D
content-length: 0
date: Sun, 02 Feb 2020 17:21:50 GMT
connection: close
-------
HTTP redirect response's saml message decoded:
<?xml version="1.0"?><samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_ef10e983e65a645b5a58" Version="2.0" IssueInstant="2020-02-02T17:21:50.916Z" Destination="https://identity-provider.asdf.local/idp" InResponseTo="_adcdabcd"><saml:Issuer>passport-saml-issuer</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status></samlp:LogoutResponse>
-------
Content of session store per sessionId AFTER the request has been processed:
content of -9Ii5aSBusDH0PO7Ec3qaEhJ1io7zyCa :
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"passport": {
"user": {
"issuer": "idp-issuer",
"inResponseTo": "secondAuthnRequest",
"sessionIndex": "_222222222222222222222222",
"nameID": "bbbbbbbbbbbbbbb@bbbbbbbbbbb.local",
"nameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
"spNameQualifier": "https://passport-saml-powered-SAML-SP.qwerty.local/samlsp"
}
}
}
---- END -------------------------------------------------------------------
---- BEGIN ----------------------------------------------------------------
HTTP REQUEST:
GET /secured HTTP/1.1
Host: passport-saml-powered-SAML-SP.qwerty.local
Accept-Encoding: gzip, deflate
User-Agent: node-superagent/3.8.3
Cookie: connect.sid=s%3A-9Ii5aSBusDH0PO7Ec3qaEhJ1io7zyCa.rf3kRGWh6%2FDNURxdpRfiKk%2FTpL%2FIsje3byfPjDphkNg
Connection: close
HTTP RESPONSE:
HTTP/1.1 200 OK
x-powered-by: Express
content-type: text/html; charset=utf-8
content-length: 39
etag: W/"27-78k8vTxTl1L2wj/JDDGs3fBzmvk"
date: Sun, 02 Feb 2020 17:21:50 GMT
connection: close
hello bbbbbbbbbbbbbbb@bbbbbbbbbbb.local
-------
Content of session store per sessionId AFTER the request has been processed:
content of -9Ii5aSBusDH0PO7Ec3qaEhJ1io7zyCa :
{
"cookie": {
"originalMaxAge": null,
"expires": null,
"httpOnly": true,
"path": "/"
},
"passport": {
"user": {
"issuer": "idp-issuer",
"inResponseTo": "secondAuthnRequest",
"sessionIndex": "_222222222222222222222222",
"nameID": "bbbbbbbbbbbbbbb@bbbbbbbbbbb.local",
"nameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
"spNameQualifier": "https://passport-saml-powered-SAML-SP.qwerty.local/samlsp"
}
}
}
---- END -------------------------------------------------------------------