Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
srd90 opened this issue Feb 2, 2020 · 8 comments · Fixed by #862
Closed

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

srd90 opened this issue Feb 2, 2020 · 8 comments · Fixed by #862

Comments

@srd90
Copy link

srd90 commented Feb 2, 2020

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

  • 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 -------------------------------------------------------------------
@markstos
Copy link
Contributor

markstos commented Feb 6, 2020

Thanks for the detailed report.

@markstos
Copy link
Contributor

markstos commented Feb 6, 2020

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

@srd90
Copy link
Author

srd90 commented Feb 9, 2020

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:

  • this would require adding functionality to "all over the place" (outside of passport module's "sandbox")
  • would increase configuration and implementation requirements of "client application" so that passport-saml could not be sure whether user can access application as authenticated user or not even if nameId+sessionIndex mapping is removed from "saml session cache"

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
Copy link
Contributor

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
Copy link
Author

srd90 commented Mar 24, 2020

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.

mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 20, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 21, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 21, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 21, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 21, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 22, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 22, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 22, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 22, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 22, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 23, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 27, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 28, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 29, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue Apr 30, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 6, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 6, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 6, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 6, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 10, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 10, 2021
- As passport-saml and SAML authentication as a whole is very deeply integrated into the application, tests for it need to unfortunately set up its own instances of almost everything for the app in order to override important configs
    - Session store must use Redis as the Single Logout token system relies on it being available (instead of the default MemoryStore for local development) -> need to override client setup to use a in-memory mock-Redis
    - SAML configuration must use "real" certificates but obviously nothing from a real environment
    - SAML configuration needs additional adjustments to simplify testing
- As simulating real 3rd party cookie situations would be really complex, deleting and re-adding cookies is used to replicate the situation
    - Also implement a reference test case where cookies are available
- Add necessary XML + crypto dependencies for creating and parsing SAML messages
    - Although passport-saml has the SAML class (with missing official types...), it doesn't really provide useful methods/abstractions -- they go too far into request handling instead of just parsing and generating messages -- so decided to impelement these manually
- Based on the wonderful examples from node-saml/passport-saml#419
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 11, 2021
- Many browsers are starting to disable 3rd party cookies by default (even SameSite None) which breaks Single Logout with passport-saml that currently only supports logging out with a cookie
    - Browser details: https://webkit.org/blog/10218/full-third-party-cookie-blocking-and-more/ and https://blog.google/products/chrome/more-intuitive-privacy-and-security-controls-chrome/
    - passport-saml issues: node-saml/passport-saml#419 and node-saml/passport-saml#221
    - This is actually a real security issue as passport-saml will give a successful looking response from `passport.authenticate()` in the logout callback which results in IdPs showing "successfully logged out" messages to the user -- even though the user is still fully logged in!
    - This also causes an usability issue when the user tries to initate SLO from another application, fails to end the eVaka session, attempts a logout from eVaka and gets an ugly JSON error message as a response when APIGW attempts to make a LogoutRequest to the IdP that already ended the session -> will be fixed separately
- Other systems like Shibboleth get around the 3rd party cookie issue with Single Logout by not utilizing cookies at all for this but instead use the SAML nameID (and sessionIndex) properties presented in all SAML LogoutRequest messages
    - Source: https://wiki.shibboleth.net/confluence/display/DEV/IdP+SameSite+Testing#IdPSameSiteTesting-SameSiteandSingleLogout
- When logouts are always only done through SAML there's no need for the logout cookie itself but the idea is actually useful as an effective "secondary index" for Redis:
    - By storing a nameID + sessionIndex keyed item pointing to the session ID, we effectively create a second index that can be used with just the SAML LogoutRequest's nameID and sessionIndex properties
mikkopiu added a commit to espoon-voltti/evaka that referenced this issue May 11, 2021
- As passport-saml and SAML authentication as a whole is very deeply integrated into the application, tests for it need to unfortunately set up its own instances of almost everything for the app in order to override important configs
    - Session store must use Redis as the Single Logout token system relies on it being available (instead of the default MemoryStore for local development) -> need to override client setup to use a in-memory mock-Redis
    - SAML configuration must use "real" certificates but obviously nothing from a real environment
    - SAML configuration needs additional adjustments to simplify testing
- As simulating real 3rd party cookie situations would be really complex, deleting and re-adding cookies is used to replicate the situation
    - Also implement a reference test case where cookies are available
- Add necessary XML + crypto dependencies for creating and parsing SAML messages
    - Although passport-saml has the SAML class (with missing official types...), it doesn't really provide useful methods/abstractions -- they go too far into request handling instead of just parsing and generating messages -- so decided to impelement these manually
- Based on the wonderful examples from node-saml/passport-saml#419
@cjbarth
Copy link
Collaborator

cjbarth commented Jun 17, 2021

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
Copy link
Collaborator

cjbarth commented May 29, 2023

@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
Copy link
Author

srd90 commented May 29, 2023

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. Add support for a failed logout response node-saml#10
  2. Check user matches logout request before reporting logout success #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

  • various session store possibilities [1]
  • ways to configure express-session (possible race conditions due to session update) combined with concurrent request handlings [1]
  • passport's lack of support for "cookieless" scenarios etc. i.e. solving this would require coordinated effort with passport project [1]
  • and due to the reasons you have described [2]

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] #419 (comment) (*)
[2] #419 (comment)

(*) 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants