Skip to content

Commit 46305eb

Browse files
authored
feat: add support for per-participant bindings during SLO (#135)
1 parent 1d72b80 commit 46305eb

5 files changed

Lines changed: 218 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
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.
44

5+
## [7.1.0](https://github.com/auth0/node-samlp/compare/v7.0.2...v7.1.0) (2023-07-24)
6+
7+
8+
### Features
9+
10+
* add support for per-participant bindings during SLO ([9f21610](https://github.com/auth0/node-samlp/commit/9f21610d18c685765d4cd5ac11deca39938d31ac))
11+
12+
### [7.0.2](https://github.com/auth0/node-samlp/compare/v7.0.1...v7.0.2) (2022-06-09)
13+
14+
15+
### Bug Fixes
16+
17+
* Update saml and ejs dependencies ([#132](https://github.com/auth0/node-samlp/issues/132)) ([26b8cbd](https://github.com/auth0/node-samlp/commit/26b8cbd50bde051e68bcb32fce61421641276b72))
18+
519
### [7.0.1](https://github.com/auth0/node-samlp/compare/v7.0.0...v7.0.1) (2022-05-19)
620

721

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ Options
9696
| 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 |
9797

9898
#### Notes
99+
99100
- options.cert: This is the public certificate of the IdP
100101
- options.key: This is the private key of the IdP. The IdP will sign its SAML `LogoutRequest` and `LogoutResponse` with this key.
101102
- 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 = {
108109
nameIdFormat: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', // Format of the NameId
109110
sessionIndex: '1', // The session index generated by the IdP
110111
serviceProviderLogoutURL: 'https://foobarsupport.zendesk.com/logout', // The logout URL of the Session Participant
111-
cert: sp1_credentials.cert // The Session Participant public certificate, used to verify the signature of the SAML requests made by this SP
112+
cert: sp1_credentials.cert, // The Session Participant public certificate, used to verify the signature of the SAML requests made by this SP
113+
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
112114
};
113115
```
114116

117+
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`.
115118

116119
Add the middleware as follows:
117120

lib/logout.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module.exports.logout = function (options) {
4343

4444
options.destination = participant.serviceProviderLogoutURL;
4545
options.relayState = relayState;
46+
options.participantBinding = participant.binding;
4647
// Send logout request
4748
prepareAndSendToken(req, res, 'LOGOUT_REQUEST', logoutRequest, options, next);
4849
});
@@ -81,6 +82,7 @@ module.exports.logout = function (options) {
8182
options.destination = data.serviceProviderLogoutURL || options.destination;
8283
// We stored the relay state of the initial request
8384
options.relayState = transaction.relayState;
85+
options.participantBinding = transaction.binding;
8486
prepareAndSendToken(req, res, 'LOGOUT_RESPONSE', logoutResponse, options, next);
8587
});
8688
});
@@ -185,9 +187,14 @@ module.exports.logout = function (options) {
185187
id: requestData.id,
186188
serviceProviderLogoutURL: (session|| {}).serviceProviderLogoutURL || options.destination
187189
},
188-
relayState: req.query.RelayState || (req.body && req.body.RelayState)
190+
relayState: req.query.RelayState || (req.body && req.body.RelayState),
189191
};
190192

193+
if (session && session.binding) {
194+
// record the client-specific binding, if there is one.
195+
spData.binding = session.binding;
196+
}
197+
191198
options.store.save(req, spData, function (err, transactionId) {
192199
if (err) { return next(err); }
193200

@@ -297,10 +304,11 @@ function parseIncomingLogoutRequest(req, samlRequest, options, callback) {
297304
}
298305

299306
function prepareAndSendToken(req, res, element_type, token, options, cb) {
307+
const binding = options.participantBinding || options.protocolBinding;
300308
var type = constants.ELEMENTS[element_type].PROP;
301-
309+
302310
var send = function (params) {
303-
if (options.protocolBinding !== BINDINGS.HTTP_REDIRECT) {
311+
if (binding !== BINDINGS.HTTP_REDIRECT) {
304312
// HTTP-POST
305313
res.set('Content-Type', 'text/html');
306314
return res.send(templates.form({
@@ -327,7 +335,7 @@ function prepareAndSendToken(req, res, element_type, token, options, cb) {
327335
// canonical request
328336
token = trim_xml(token);
329337

330-
if (options.protocolBinding !== BINDINGS.HTTP_REDIRECT || !options.deflate) {
338+
if (binding !== BINDINGS.HTTP_REDIRECT || !options.deflate) {
331339
// HTTP-POST or HTTP-Redirect without deflate encoding
332340
try {
333341
token = signers.signXml(options, token);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "samlp",
3-
"version": "7.0.1",
3+
"version": "7.1.0",
44
"engines": {
55
"node": ">=12"
66
},

test/samlp.logout.session_store.tests.js

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var fs = require('fs');
1212
var path = require('path');
1313
var SPs = require('../lib/sessionParticipants');
1414
const timekeeper = require('timekeeper');
15+
const BINDINGS = require('../lib/constants').BINDINGS;
1516

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

95+
function prepareOneParticipant(binding) {
96+
sessions.splice(0);
97+
sessions.push({ ...sessionParticipant1, binding: binding });
98+
}
99+
100+
function prepareTwoParticipants(secondBinding) {
101+
sessions.splice(0);
102+
sessions.push(sessionParticipant1);
103+
sessions.push({ ...sessionParticipant2, binding: secondBinding });
104+
}
105+
106+
function logoutGetSPInitiated(callback) {
107+
// SAMLRequest: base64 encoded + deflated + URLEncoded
108+
// Signature: URLEncoded
109+
// SigAlg: URLEncoded
110+
111+
// <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">
112+
// <saml:Issuer>https://foobarsupport.zendesk.com</saml:Issuer>
113+
// <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
114+
// <saml:SessionIndex>1</saml:SessionIndex>
115+
// </samlp:LogoutRequest>
116+
request.get(
117+
{
118+
followRedirect: false,
119+
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',
120+
},
121+
function (err, response) {
122+
if (err) return callback(err);
123+
callback(null, response);
124+
}
125+
);
126+
}
127+
128+
function logoutPostSPInitiated(callback) {
129+
// SAMLRequest: base64 encoded + deflated + URLEncoded
130+
// Signature: URLEncoded
131+
// SigAlg: URLEncoded
132+
133+
// <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">
134+
// <saml:Issuer>https://foobarsupport.zendesk.com</saml:Issuer>
135+
// <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
136+
// <saml:SessionIndex>1</saml:SessionIndex>
137+
// </samlp:LogoutRequest>
138+
request.post({
139+
followRedirect: false,
140+
uri: "http://localhost:5050/logout",
141+
json: true,
142+
body: {
143+
SAMLRequest: "PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4NmZlNjU3ZTMtMWE3Zi04OTNlLWY2OTAtZjdmYzUxNjJlYTExIiBJc3N1ZUluc3RhbnQ9IjIwMTYtMTItMTNUMTg6MDE6MTJaIiBWZXJzaW9uPSIyLjAiPg0KICAgICAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9mb29iYXJzdXBwb3J0LnplbmRlc2suY29tPC9zYW1sOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgPGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz4NCiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4NmZlNjU3ZTMtMWE3Zi04OTNlLWY2OTAtZjdmYzUxNjJlYTExIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPjxkczpEaWdlc3RWYWx1ZT55SnpIbmRqL3NuaVJzTG1kcHFSZ0Yvdmp6L0k9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPk56bU42R0RLcHNpMVU4NndaTXNjWjY2aExHNDVhMzhhMGhvaCtpdFdCTWQzNS9RMnF1Y2N2NEJaTGhSbU1xYmFIL3l4VnZ4bWUvWXExR24xbEkrVlpwZkZsYURXQnZTcXUxdWJVemVEbEtVUDdHUmVnakNSTFErSkhxZnQ2aHRDdENQdkttQ0NTaVNEVlZydmcvc0ZLVXBuVDhPWEhkK25ENDBLSVQ4NHQ2OERiM2pTN3g2amx6VDMzYk1Vdm83dVNFUDVnSnFUbG9RMVVWY280WmszUGVxK0tDOWF6TUFkVHVnMWZZRDJXVWtXOEZCd084b1ZBUWpDMGo4VkVyVVpiUUpRS2hhdTMxcjNVcU1VUExNS0NJaFZxZ0tPRVd6MWt1a1NWY2MzdTJjR0owT1FJU093N0xQbkRDSTdPclVMaGU4NEJESTMzR01JMDNXazFMNG5Mdz09PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YS8+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPg0KICAgICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPmZvb0BleGFtcGxlLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICAgIDxzYW1sOlNlc3Npb25JbmRleD4xPC9zYW1sOlNlc3Npb25JbmRleD4NCiAgICAgIDwvc2FtbHA6TG9nb3V0UmVxdWVzdD4=",
144+
RelayState: "123",
145+
},
146+
},
147+
function (err, response) {
148+
if (err) return callback(err);
149+
callback(null, response);
150+
}
151+
);
152+
}
153+
154+
function logoutGetIDPInitiated(callback) {
155+
request.get({
156+
followRedirect: false,
157+
uri: 'http://localhost:5050/logout'
158+
}, function (err, response) {
159+
if(err) return callback(err);
160+
161+
callback(null, response);
162+
});
163+
}
164+
165+
function assertPostResponse(response) {
166+
// Ensure we get a POST response,
167+
// this means responding with an HTML form that will self-submit.
168+
// The rest is covered by other tests.
169+
expect(response).to.be.ok;
170+
expect(response.statusCode).to.equal(200);
171+
}
172+
173+
function assertRedirectResponse(response) {
174+
// Ensure we get a Redirect response,
175+
// The rest is covered by other tests.
176+
expect(response).to.be.ok;
177+
expect(response.statusCode).to.equal(302);
178+
}
179+
94180
describe('HTTP Redirect', function () {
95181
describe('SP initiated - Should fail if No Issuer is present', function () {
96182
var logoutResultValue;
@@ -800,6 +886,66 @@ describe('samlp logout with Session Participants - Session Provider', function (
800886
});
801887
});
802888
});
889+
890+
describe('SP initiated - 1 Session Participant with POST binding', function () {
891+
var logoutResponse;
892+
893+
before(function () {
894+
prepareOneParticipant(BINDINGS.HTTP_POST);
895+
});
896+
897+
before(function (done) {
898+
logoutGetSPInitiated(function(err, response){
899+
if (err) return done(err);
900+
logoutResponse = response;
901+
done();
902+
});
903+
});
904+
905+
it('Should return POST request', function () {
906+
assertPostResponse(logoutResponse);
907+
});
908+
});
909+
910+
describe('SP initiated - 2 Session Participants with POST binding', function() {
911+
var logoutResponse;
912+
913+
before(function () {
914+
prepareTwoParticipants(BINDINGS.HTTP_POST);
915+
});
916+
917+
before(function (done) {
918+
logoutGetSPInitiated(function(err, response){
919+
if (err) return done(err);
920+
logoutResponse = response;
921+
done();
922+
});
923+
});
924+
925+
it('Should return POST request', function () {
926+
assertPostResponse(logoutResponse);
927+
});
928+
});
929+
930+
describe('IDP initiated - 1 Session Participant with POST binding', function() {
931+
var logoutResponse;
932+
933+
before(function () {
934+
prepareOneParticipant(BINDINGS.HTTP_POST);
935+
});
936+
937+
before(function (done) {
938+
logoutGetIDPInitiated(function(err, response){
939+
if (err) return done(err);
940+
logoutResponse = response;
941+
done();
942+
});
943+
});
944+
945+
it('Should return POST request', function () {
946+
assertPostResponse(logoutResponse);
947+
});
948+
});
803949
});
804950

805951
describe('HTTP POST', function () {
@@ -1374,6 +1520,46 @@ describe('samlp logout with Session Participants - Session Provider', function (
13741520
expect(response.body).to.equal('Invalid Session Participant');
13751521
});
13761522
});
1523+
1524+
describe('SP initiated - 1 Session Participant with Redirect binding', function () {
1525+
var logoutResponse;
1526+
1527+
before(function () {
1528+
prepareOneParticipant(BINDINGS.HTTP_REDIRECT);
1529+
});
1530+
1531+
before(function (done) {
1532+
logoutPostSPInitiated(function(err, response){
1533+
if (err) return done(err);
1534+
logoutResponse = response;
1535+
done();
1536+
});
1537+
});
1538+
1539+
it('Should return Redirect request', function () {
1540+
assertRedirectResponse(logoutResponse);
1541+
});
1542+
});
1543+
1544+
describe('SP initiated - 2 Session Participants with Redirect binding', function() {
1545+
var logoutResponse;
1546+
1547+
before(function () {
1548+
prepareTwoParticipants(BINDINGS.HTTP_REDIRECT);
1549+
});
1550+
1551+
before(function (done) {
1552+
logoutPostSPInitiated(function(err, response){
1553+
if (err) return done(err);
1554+
logoutResponse = response;
1555+
done();
1556+
});
1557+
});
1558+
1559+
it('Should return Redirect request', function () {
1560+
assertRedirectResponse(logoutResponse);
1561+
});
1562+
});
13771563
});
13781564
});
13791565

@@ -1429,6 +1615,7 @@ describe('samlp logout with Session Participants - Session Provider', function (
14291615
}
14301616
}, function (err, response){
14311617
if (err) { return done(err); }
1618+
14321619
expect(response.statusCode).to.equal(200);
14331620
$ = cheerio.load(response.body);
14341621
var SAMLResponse = $('input[name="SAMLResponse"]').attr('value');

0 commit comments

Comments
 (0)