diff --git a/CHANGELOG.md b/CHANGELOG.md
index c12839a..ea639f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/README.md b/README.md
index 3f4820f..5ee6128 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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:
diff --git a/lib/logout.js b/lib/logout.js
index 77c4172..cb57dee 100644
--- a/lib/logout.js
+++ b/lib/logout.js
@@ -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);
});
@@ -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);
});
});
@@ -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); }
@@ -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({
@@ -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);
diff --git a/package.json b/package.json
index 0646ebd..278517a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "samlp",
- "version": "7.0.1",
+ "version": "7.1.0",
"engines": {
"node": ">=12"
},
diff --git a/test/samlp.logout.session_store.tests.js b/test/samlp.logout.session_store.tests.js
index 89106e5..9d59221 100644
--- a/test/samlp.logout.session_store.tests.js
+++ b/test/samlp.logout.session_store.tests.js
@@ -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')),
@@ -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
+
+ //
+ // https://foobarsupport.zendesk.com
+ // foo@example.com
+ // 1
+ //
+ 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
+
+ //
+ // https://foobarsupport.zendesk.com
+ // foo@example.com
+ // 1
+ //
+ 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;
@@ -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 () {
@@ -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);
+ });
+ });
});
});
@@ -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');