node-saml / passport-saml

SAML 2.0 authentication with Passport
MIT License
862 stars 473 forks source link

passport-saml's IdP initiated LogoutRequest handling doesn't always close sessions #419

Closed srd90 closed 1 year ago

srd90 commented 4 years ago

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:

  1. end user has blocked third party cookies
  2. 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.
  3. end user triggers global logout from some other service (passport-saml receives IdP initiated LogoutRequest)
  4. SAML IdP has implemented logout propagation with combination of iframes and javascripts

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

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 -------------------------------------------------------------------
markstos commented 4 years ago

Thanks for the detailed report.

markstos commented 4 years ago

@srd90 You have a clear understanding of the issue. Are you willing to submit a patch for it?

srd90 commented 4 years ago

Original issue description talked about combination where end user has explicitly blocked third party cookies and certain IdP setups (iframe based SLO propagation) in cross-domain IdP initiated SLO situation.

IMHO upcoming change where browser vendors start to apply samesite lax cookie policy by default (https://www.chromestatus.com/feature/5088147346030592) breaks also those cross-domain SLO scenarios (when passport-saml is involved in the loop) which use http redirect hops from one SP to another. passport-saml keeps reporting "Success" as it has done past few years but unlike before invocation of passport's logout() function in LogoutRequest handling implementation has no effect at all. This change of passport-saml's behaviour due sudden lack of session cookie in SLO request would most probably go unnoticed until someone tries to access protected resources after "successfull" SLO.

For additional information about samesite see for example

IMHO in a scenario described above redirect hops from one site to another those redirects would not be treated as "a top level navigation" which would/could mean that cookies are not attached to requests. I haven't actually tested this but it seems quite obvious.

Regarding your question about patch.

It must contain at least functionality which prevents passport-saml to report Success if there is even small chance that session is not logged out and it must not "step out of the passport modules' boundaries/sandbox" (so that it could be used as-is without breaking changes or additional requirements of new middlewares to handle requests etc.).

I don't have skills to provide such a patch and I'm not using or planning to use passport-saml.

Patch could look something like code visible in a diff at below but I can't see how that would ever work in clustered environment with all sort of session stores and possibly concurrent http requests from same browser instance etc.

This diff contains few lines of pseudo-code and must not be used as-is in production:

diff --git a/lib/passport-saml/saml.js b/lib/passport-saml/saml.js
index e7e9283..e68fb97 100644
--- a/lib/passport-saml/saml.js
+++ b/lib/passport-saml/saml.js
@@ -287,7 +287,8 @@ SAML.prototype.generateLogoutResponse = function (req, logoutRequest) {
       },
       'samlp:Status': {
         'samlp:StatusCode': {
-          '@Value': 'urn:oasis:names:tc:SAML:2.0:status:Success'
+          //
+          '@Value': req.samlLogoutResponseStatusCode
         }
       }
     }
diff --git a/lib/passport-saml/strategy.js b/lib/passport-saml/strategy.js
index 4d57599..56c9881 100644
--- a/lib/passport-saml/strategy.js
+++ b/lib/passport-saml/strategy.js
@@ -43,9 +43,46 @@ Strategy.prototype.authenticate = function (req, options) {
       }

       if (loggedOut) {
+        let reqUsersNameIdAndSessionIndexMatchesLogoutRequestInfo = false;
+        // check that we have authenticated session
+        if (req.isAuthenticated()) {
+          // check that LogoutRequest by IdP contains necessary information
+          // to check whether req.user matches with information in logout request
+          if (profile && profile.nameID && profile.sessionIndex) {
+            // check that req.user (which is alias for req.session.passport.user
+            // and content was deserialized by some storage by function provided
+            // by surrounding application) contains same nameID and sessionIndex
+            // as logoutrequest sent by IdP
+            // NOTE: assumes that deserialize function has placed nameId and sessionIndex
+            // into following positions at deserialized user
+            if (req.user.nameID && req.user.sessionIndex) {
+              // check that nameID and sessionIndex matches
+              if (req.user.nameID === profile.nameID && req.user.sessionIndex === profile.sessionIndex) {
+                reqUsersNameIdAndSessionIndexMatchesLogoutRequestInfo = true;
+              }
+            }
+          }
+        }
+        // AFAIK it is possible that same browser instance has triggered two requests to
+        // application which are handled concurrently at different nodejs instances
+        // behind LB. Based on documentation express session resaves automatically
+        // even if there are not any changes to session (and automatically if changes
+        // were made) so its possible that concurrent request handling has a session
+        // which contains user data and that session is rewritten to session store
+        // after logoutrequest handling is ended which means that logout has not happened
+        // because next request is able to deserialize user object again.
         req.logout();
         if (profile) {
           req.samlLogoutRequest = profile;
+          req.samlLogoutResponseStatusCode = "....by default some error code....";
+
+          if (reqUsersNameIdAndSessionIndexMatchesLogoutRequestInfo === true) {
+            //
+            // assumes that req.logout() managed to perform logout successfully
+            // (see comments before req.logout() line).
+            // ugly way to pass information for saml.generateLogoutResponse(...)
+            req.samlLogoutResponseStatusCode = "....success resultcode....";
+          }
           return self._saml.getLogoutResponseUrl(req, options, redirectIfSuccess);
         }
         return self.pass();

If patch is defined as "solution for IdP initiated logout which just works"...

Idea 1: Lets store mapping of nameId+sessionIndex --> sessionID (i.e. let's create secondary index) during authentication process and delete express session somehow from the session store during logout process by looking up express sessionID with nameID and sessionIndex and perform some sessionStore.deleteBySessionId operation.

This would not work

Idea 2: Instead of storing mapping from nameid+sessionindex to express sessionID one could store nameId+sessionIndex --> samlLoginValidNotOnAfterTimestamp to some "saml session cache".

Existence of this mapping could be checked during every http invocation (after deserialize function has provided data to req.user ).

If query with req.user.nameID + req.user.sessionIndex does not return anything session must have been terminated and request would have to be modified so that from that point on it is treated as unauthenticated (for example with invocation of req.logout() which is alias for passport's logout function just removes user property and deletes property from session ... see implementation of that function).

If query returned a timestamp one could check if notOnOrAfter provided by IdP for this SP session has been exceeded and modify request so that it is treated as unauthenticated (and mapping should be removed).

When IdP initiated LogoutRequest is received existence of nameId+sessionIndex mapping is checked and if it exists it is removed from "saml session cache", so that subsequent lookup operations do not return anything. If passport-saml is 100% sure that user cannot access application as authenticated used anymore it could respond with Success.

When SP is logout initiator it must clear aforementioned mapping (along with other cleanup stuff made during logout process) from "saml session cache" before it redirect browser to IdP to initiate SLO.

Problems:

Difference to "idea 1" is that passport-saml controls insert and removal of "saml session cache" content.

Ideas described above are NOT tested in any way.

Links to external sites (from this comment) were valid Sun, 9 Feb 2020

markstos commented 4 years ago

I affirm that other SAML projects are also discussing the impact of Chrome's M80 release and "SameSite=None" cookies on SLO. Here's a description of the issue on the Shibboleth wiki:

https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout

srd90 commented 4 years ago

IMHO wiki entry you referenced says: ShibbolethSP's SLO is unaffected of samesite change because it doesn't use/require cookies during SLO processing. It uses information from logout request (nameid) to invalidate login session managed by ShibbolethSP's own sessionmanagement.

If appliction which sits behind ShibbolethSP rely only on ShibbolethSP's session management and shibd is used in ”active mode” then SLO over redirect and POST binding keeps working from protected application point of view.

If application which sits behind ShibbolethSP has application specific session meaning that if application uses own session management in addition to ShibbolethSP's session mgmt (and if shibd is not used in ”active mode”) it is up to application how it is able to terminate user session upon receiving notification of session termination from ShibbolethSP (which received logout request from IdP via redirect or POST binding). https://wiki.shibboleth.net/confluence/display/SHIB2/SLOWebappAdaptation https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPNotify

This issue is not about ShibbolethSP.

IMO passport-saml SP has locally exploitable vulnerability in SLO handling. It seems to has had it years and recent samesite change has increased ”impact surface” from browser installations which has been configured to block 3rd party cookies to mainstream browser installations which block cross domain cookies by default (case: iframe based SLO orchestration at e.g. Shibboleth IdPv3 side). To make things worst SLO propagation could be working from end user point of as it has been working earlier. Visual feedback could be unaffected. IdP's SLO orchestration page could be showing that passport-saml site (among other sites that participted to same SSO) has terminated session but under the hood sessions are not actually terminated anymore by passport-saml.

IdP trusts that if SAML SP instance provides Success in logout response that session is actually closed at SP side. Passport-saml answers always Success with and without web application's session cookies available during logout request handling. Because passport/express-session's session management expects cookies and passport-saml doesn't have any extra session manamement stuff there is no way passport-saml is reporting correctly under every circumstances.

I suggest adding vulnerability label to this issue.

cjbarth commented 3 years ago

The approach that we seem to be taking with the code the way it is relies on the correct implementation of Express and Passport: https://www.passportjs.org/docs/logout/ and https://www.passportjs.org/docs/configure/ ("sessions" section). You might also reference https://stackoverflow.com/questions/22052258/what-does-passport-session-middleware-do.

The problem that is being exposed is that passport-saml is built upon Passport, which is built upon Express. So, if either Express or Passport can't do their thing correctly, then passport-saml will report the wrong results. There is no feedback from Express or Passport as to whether or not they did their thing correctly. The presumption is that if there is an active user as controlled by an Expression session identifier, and a logout request is received, it is that user that will be logged out. If Express didn't correctly load the session for the user, only then will we have this issue.

Having said all that, it is completely correct to say that we are calling req.logout() before we determine that the logout request that we received actually matches the currently logged in user, and that should be fixed. We probably want to still log out the current user, but we want to reply accurately if we were able to verify that the user for whom the logout was requested was actually logged out. The tricky part is figuring out what information we can 100% rely upon to actually do this checking as not all implementations use serialize and deserialize functions. I have some ideas on how to do this, but I'd like to get passport-saml and node-saml split before taking it on as it will require changes to both halves of the system.

cjbarth commented 1 year ago

@srd90 , I see you describe the changes that we've made as "half fixes" for this problem. Based on my explanation of how Express and Passport are involved here, I'd like to hear what you have to say about potentially getting the other "half" fixed, though I'm not sure there is a way as that appears to me to be outside the scope of node-saml and passport-saml.

srd90 commented 1 year ago

For the record (for those who might read this issue comment in the future) "half fix" refer to these (see also discussions from collapsed code review comments):

  1. https://github.com/node-saml/node-saml/pull/10
  2. https://github.com/node-saml/passport-saml/pull/619

tl;dr; those fixes aim to NOT return "Success" by default unless client code of this library provide function which gives "permission" to report "Success" (i.e. those fixes aim to delegate responsibility of result code of SLO to client libraries).

I described those as "half fixes" because @node-saml/passport-saml and @node-saml/node-saml documentation has been and is stating that these libraries support IdP initiated SLO scenarios over various bindings. IMHO that part of the README.md files can also be read as "this library implements IdP initiated SLO support (out of the box)".

@cjbarth About "other half": I cannot see any way to implement "fully functional out of the box" support due to

Maybe "other half" could be handled by modifying @node-saml/passport-saml and @node-saml/node-saml README.md files' SLO related section so that it states something like:

Fully functional IdP initiated SLO support is not provided out of the box. You have to inspect your use cases / implementation / deployment scenarios (location of IdP in respect to SP) and consider things / cases listed e.g. at issue(s) #221 and #419. Library provides you a mechanism to veto "Success" result but it does not provide hooks/interfaces to implement support for IdP initiated SLO which would work under all circumstances. You have to do it yourself.

or something like that. If client SW implementors are eager to implement fully functional IdP initiated SLO they can inspect currently available comments and implementations (e.g. those that are discoverable via linked PRs / commits of other repositories).


[1] https://github.com/node-saml/passport-saml/issues/419#issuecomment-583877544 (*) [2] https://github.com/node-saml/passport-saml/issues/419#issuecomment-863560464

(*) For the record / FYI for those who might read this comment / issue in the future: this issue and initial comments were created against passport-saml version ac7939fc1f74c3a350cee99d68268de7391db41e. Since then codebase has been moved to node-saml organization, converted to Typescript, splitted to two parts etc. etc.