Skip to content

Commit

Permalink
feat: add support for per-participant bindings during SLO (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtiomCiumac authored Jul 24, 2023
1 parent 1d72b80 commit 46305eb
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 6 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## [7.1.0](https://github.com/auth0/node-samlp/compare/v7.0.2...v7.1.0) (2023-07-24)


### Features

* add support for per-participant bindings during SLO ([9f21610](https://github.com/auth0/node-samlp/commit/9f21610d18c685765d4cd5ac11deca39938d31ac))

### [7.0.2](https://github.com/auth0/node-samlp/compare/v7.0.1...v7.0.2) (2022-06-09)


### Bug Fixes

* Update saml and ejs dependencies ([#132](https://github.com/auth0/node-samlp/issues/132)) ([26b8cbd](https://github.com/auth0/node-samlp/commit/26b8cbd50bde051e68bcb32fce61421641276b72))

### [7.0.1](https://github.com/auth0/node-samlp/compare/v7.0.0...v7.0.1) (2022-05-19)


Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Options
| store | an object that handles the HTTP Session. Check [this implementation](./test/in_memory_store/) | new SessionStore(options) Uses req.session to store the current state |

#### Notes

- options.cert: This is the public certificate of the IdP
- options.key: This is the private key of the IdP. The IdP will sign its SAML `LogoutRequest` and `LogoutResponse` with this key.
- options.store: Since the logout flow will involve several requests/responses, we need to keep track of the transaction state. The default implementation uses req.session to store the transaction state via the 'flowstate' module
Expand All @@ -108,10 +109,12 @@ var sessionParticipant = {
nameIdFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', // Format of the NameId
sessionIndex: '1', // The session index generated by the IdP
serviceProviderLogoutURL: 'https://foobarsupport.zendesk.com/logout', // The logout URL of the Session Participant
cert: sp1_credentials.cert // The Session Participant public certificate, used to verify the signature of the SAML requests made by this SP
cert: sp1_credentials.cert, // The Session Participant public certificate, used to verify the signature of the SAML requests made by this SP
binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' // Optional, participant-specific binding to use during SLO, if not provided - will use "protocolBinding" from provided options
};
```

In some situations it is possible for session participants to have mixed bindings during one Single Log Out (SLO) transaction. By default the library will use the binding specified in `options.protocolBinding`, however if mixed bindings must be used - each participant must have the binding specified as an additional field. If the binding value is invalid - it will fall back to `HTTP-POST`.

Add the middleware as follows:

Expand Down
16 changes: 12 additions & 4 deletions lib/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports.logout = function (options) {

options.destination = participant.serviceProviderLogoutURL;
options.relayState = relayState;
options.participantBinding = participant.binding;
// Send logout request
prepareAndSendToken(req, res, 'LOGOUT_REQUEST', logoutRequest, options, next);
});
Expand Down Expand Up @@ -81,6 +82,7 @@ module.exports.logout = function (options) {
options.destination = data.serviceProviderLogoutURL || options.destination;
// We stored the relay state of the initial request
options.relayState = transaction.relayState;
options.participantBinding = transaction.binding;
prepareAndSendToken(req, res, 'LOGOUT_RESPONSE', logoutResponse, options, next);
});
});
Expand Down Expand Up @@ -185,9 +187,14 @@ module.exports.logout = function (options) {
id: requestData.id,
serviceProviderLogoutURL: (session|| {}).serviceProviderLogoutURL || options.destination
},
relayState: req.query.RelayState || (req.body && req.body.RelayState)
relayState: req.query.RelayState || (req.body && req.body.RelayState),
};

if (session && session.binding) {
// record the client-specific binding, if there is one.
spData.binding = session.binding;
}

options.store.save(req, spData, function (err, transactionId) {
if (err) { return next(err); }

Expand Down Expand Up @@ -297,10 +304,11 @@ function parseIncomingLogoutRequest(req, samlRequest, options, callback) {
}

function prepareAndSendToken(req, res, element_type, token, options, cb) {
const binding = options.participantBinding || options.protocolBinding;
var type = constants.ELEMENTS[element_type].PROP;

var send = function (params) {
if (options.protocolBinding !== BINDINGS.HTTP_REDIRECT) {
if (binding !== BINDINGS.HTTP_REDIRECT) {
// HTTP-POST
res.set('Content-Type', 'text/html');
return res.send(templates.form({
Expand All @@ -327,7 +335,7 @@ function prepareAndSendToken(req, res, element_type, token, options, cb) {
// canonical request
token = trim_xml(token);

if (options.protocolBinding !== BINDINGS.HTTP_REDIRECT || !options.deflate) {
if (binding !== BINDINGS.HTTP_REDIRECT || !options.deflate) {
// HTTP-POST or HTTP-Redirect without deflate encoding
try {
token = signers.signXml(options, token);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "samlp",
"version": "7.0.1",
"version": "7.1.0",
"engines": {
"node": ">=12"
},
Expand Down
187 changes: 187 additions & 0 deletions test/samlp.logout.session_store.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var fs = require('fs');
var path = require('path');
var SPs = require('../lib/sessionParticipants');
const timekeeper = require('timekeeper');
const BINDINGS = require('../lib/constants').BINDINGS;

var sp1_credentials = {
cert: fs.readFileSync(path.join(__dirname, 'fixture', 'sp1.pem')),
Expand Down Expand Up @@ -91,6 +92,91 @@ describe('samlp logout with Session Participants - Session Provider', function (
});
});

function prepareOneParticipant(binding) {
sessions.splice(0);
sessions.push({ ...sessionParticipant1, binding: binding });
}

function prepareTwoParticipants(secondBinding) {
sessions.splice(0);
sessions.push(sessionParticipant1);
sessions.push({ ...sessionParticipant2, binding: secondBinding });
}

function logoutGetSPInitiated(callback) {
// SAMLRequest: base64 encoded + deflated + URLEncoded
// Signature: URLEncoded
// SigAlg: URLEncoded

// <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="samlr-220c705e-c15e-11e6-98a4-ecf4bbce4318" IssueInstant="2016-12-13T18:01:12Z" Version="2.0">
// <saml:Issuer>https://foobarsupport.zendesk.com</saml:Issuer>
// <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
// <saml:SessionIndex>1</saml:SessionIndex>
// </samlp:LogoutRequest>
request.get(
{
followRedirect: false,
uri: 'http://localhost:5050/logout?SAMLRequest=fVFNS8NAEL0L%2Foew900zaa1xaIOFIgSqBysevG03Uw1md%2BPOBoq%2F3m1aoVZ0DnOY97WPnbEybYcr9%2Br68EgfPXFIdqa1jAMyF7236BQ3jFYZYgwa14v7FeZphp13wWnXihPJ%2FwrFTD40zoqkWs7FXuBlnmf6OrsiqSEuAJrKm0JNJOntZLPRNBlDEfnMPVWWg7JhLvIMphJyCeMnKDADhPxFJM%2FkOZpHOM1EeXmRHGe2D8LBwZdvIXSMo9HWuY3y3Hed8yH9JFsTv6famdnolH7u8hBLVcvkznmjwt9tIYXh0tRyO1CRjGraRV17YhZlTL%2BlnTJdSyeZB%2FNfmesoib2q%2BMRdCUfuj%2BO34oCd%2FWj5BQ%3D%3D&Signature=NkobB0DS0M4kfV89R%2Bma0wp0djNr4GW2ziVemwSvVYy2iF432qjs%2FC4Y1cZDXwuF5OxMgu4DuelS5mW3Z%2B46XXkoMVBizbd%2BIuJUFQcvLtiXHkoaEk8HVU0v5bA9TDoc9Ve7A0nUgKPciH7KTcFSr45vepyg0dMMQtarsUZeYSRPM0QlwxXKCWRQJDwGHLie5dMCZTRNUEcm9PtWZij714j11HI15u6Fp5GDnhp7mzKuAUdSIKHzNKAS2J4S8xZz9n9UTCl3uBbgfxZ3av6%2FMQf7HThxTl%2FIOmU%2FYCAN6DWWE%2BQ3Z11bgU06P39ZuLW2fRBOfIOO6iTEaAdORrdBOw%3D%3D&RelayState=123&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1',
},
function (err, response) {
if (err) return callback(err);
callback(null, response);
}
);
}

function logoutPostSPInitiated(callback) {
// SAMLRequest: base64 encoded + deflated + URLEncoded
// Signature: URLEncoded
// SigAlg: URLEncoded

// <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="samlr-220c705e-c15e-11e6-98a4-ecf4bbce4318" IssueInstant="2016-12-13T18:01:12Z" Version="2.0">
// <saml:Issuer>https://foobarsupport.zendesk.com</saml:Issuer>
// <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
// <saml:SessionIndex>1</saml:SessionIndex>
// </samlp:LogoutRequest>
request.post({
followRedirect: false,
uri: "http://localhost:5050/logout",
json: true,
body: {
SAMLRequest: "PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4NmZlNjU3ZTMtMWE3Zi04OTNlLWY2OTAtZjdmYzUxNjJlYTExIiBJc3N1ZUluc3RhbnQ9IjIwMTYtMTItMTNUMTg6MDE6MTJaIiBWZXJzaW9uPSIyLjAiPg0KICAgICAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9mb29iYXJzdXBwb3J0LnplbmRlc2suY29tPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4NmZlNjU3ZTMtMWE3Zi04OTNlLWY2OTAtZjdmYzUxNjJlYTExIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT55SnpIbmRqL3NuaVJzTG1kcHFSZ0Yvdmp6L0k9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPk56bU42R0RLcHNpMVU4NndaTXNjWjY2aExHNDVhMzhhMGhvaCtpdFdCTWQzNS9RMnF1Y2N2NEJaTGhSbU1xYmFIL3l4VnZ4bWUvWXExR24xbEkrVlpwZkZsYURXQnZTcXUxdWJVemVEbEtVUDdHUmVnakNSTFErSkhxZnQ2aHRDdENQdkttQ0NTaVNEVlZydmcvc0ZLVXBuVDhPWEhkK25ENDBLSVQ4NHQ2OERiM2pTN3g2amx6VDMzYk1Vdm83dVNFUDVnSnFUbG9RMVVWY280WmszUGVxK0tDOWF6TUFkVHVnMWZZRDJXVWtXOEZCd084b1ZBUWpDMGo4VkVyVVpiUUpRS2hhdTMxcjNVcU1VUExNS0NJaFZxZ0tPRVd6MWt1a1NWY2MzdTJjR0owT1FJU093N0xQbkRDSTdPclVMaGU4NEJESTMzR01JMDNXazFMNG5Mdz09PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YS8+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPg0KICAgICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPmZvb0BleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICAgIDxzYW1sOlNlc3Npb25JbmRleD4xPC9zYW1sOlNlc3Npb25JbmRleD4NCiAgICAgIDwvc2FtbHA6TG9nb3V0UmVxdWVzdD4=",
RelayState: "123",
},
},
function (err, response) {
if (err) return callback(err);
callback(null, response);
}
);
}

function logoutGetIDPInitiated(callback) {
request.get({
followRedirect: false,
uri: 'http://localhost:5050/logout'
}, function (err, response) {
if(err) return callback(err);

callback(null, response);
});
}

function assertPostResponse(response) {
// Ensure we get a POST response,
// this means responding with an HTML form that will self-submit.
// The rest is covered by other tests.
expect(response).to.be.ok;
expect(response.statusCode).to.equal(200);
}

function assertRedirectResponse(response) {
// Ensure we get a Redirect response,
// The rest is covered by other tests.
expect(response).to.be.ok;
expect(response.statusCode).to.equal(302);
}

describe('HTTP Redirect', function () {
describe('SP initiated - Should fail if No Issuer is present', function () {
var logoutResultValue;
Expand Down Expand Up @@ -800,6 +886,66 @@ describe('samlp logout with Session Participants - Session Provider', function (
});
});
});

describe('SP initiated - 1 Session Participant with POST binding', function () {
var logoutResponse;

before(function () {
prepareOneParticipant(BINDINGS.HTTP_POST);
});

before(function (done) {
logoutGetSPInitiated(function(err, response){
if (err) return done(err);
logoutResponse = response;
done();
});
});

it('Should return POST request', function () {
assertPostResponse(logoutResponse);
});
});

describe('SP initiated - 2 Session Participants with POST binding', function() {
var logoutResponse;

before(function () {
prepareTwoParticipants(BINDINGS.HTTP_POST);
});

before(function (done) {
logoutGetSPInitiated(function(err, response){
if (err) return done(err);
logoutResponse = response;
done();
});
});

it('Should return POST request', function () {
assertPostResponse(logoutResponse);
});
});

describe('IDP initiated - 1 Session Participant with POST binding', function() {
var logoutResponse;

before(function () {
prepareOneParticipant(BINDINGS.HTTP_POST);
});

before(function (done) {
logoutGetIDPInitiated(function(err, response){
if (err) return done(err);
logoutResponse = response;
done();
});
});

it('Should return POST request', function () {
assertPostResponse(logoutResponse);
});
});
});

describe('HTTP POST', function () {
Expand Down Expand Up @@ -1374,6 +1520,46 @@ describe('samlp logout with Session Participants - Session Provider', function (
expect(response.body).to.equal('Invalid Session Participant');
});
});

describe('SP initiated - 1 Session Participant with Redirect binding', function () {
var logoutResponse;

before(function () {
prepareOneParticipant(BINDINGS.HTTP_REDIRECT);
});

before(function (done) {
logoutPostSPInitiated(function(err, response){
if (err) return done(err);
logoutResponse = response;
done();
});
});

it('Should return Redirect request', function () {
assertRedirectResponse(logoutResponse);
});
});

describe('SP initiated - 2 Session Participants with Redirect binding', function() {
var logoutResponse;

before(function () {
prepareTwoParticipants(BINDINGS.HTTP_REDIRECT);
});

before(function (done) {
logoutPostSPInitiated(function(err, response){
if (err) return done(err);
logoutResponse = response;
done();
});
});

it('Should return Redirect request', function () {
assertRedirectResponse(logoutResponse);
});
});
});
});

Expand Down Expand Up @@ -1429,6 +1615,7 @@ describe('samlp logout with Session Participants - Session Provider', function (
}
}, function (err, response){
if (err) { return done(err); }

expect(response.statusCode).to.equal(200);
$ = cheerio.load(response.body);
var SAMLResponse = $('input[name="SAMLResponse"]').attr('value');
Expand Down

0 comments on commit 46305eb

Please sign in to comment.