diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 97b0028..e32bc12 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,14 +8,14 @@ jobs:
strategy:
matrix:
- node-version: [12.x, 14.x, 15.x, 16.x]
+ node-version: [22.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v1
+ uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 39bfd73..eba9cbb 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,7 +1,7 @@
name: Semgrep
on:
- pull_request_target: {}
+ pull_request: {}
push:
branches: ["master", "main"]
permissions:
@@ -17,4 +17,4 @@ jobs:
- uses: actions/checkout@v3
- run: semgrep ci
env:
- SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
\ No newline at end of file
+ SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
diff --git a/.gitignore b/.gitignore
index f49454e..ca59716 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules/*
.DS_Store
coverage/*
+.nyc_output
package-lock.json
.idea
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1dc02a4..a7d02fc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,30 @@
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.2.0](https://github.com/DataRecognitionCorporation/node-samlp/compare/v7.1.1...v7.2.0) (2026-04-02)
+
+
+### ⚠ BREAKING CHANGES
+
+* Requires Node.js >= 22
+
+
+### Bug Fixes
+
+* fix metadata endpoint ignoring `X-Forwarded-Host` header for endpoint URLs
+* fix test compatibility with Node.js 22 / OpenSSL 3 (dynamic redirect signature, updated error regex)
+
+
+### Dependency Updates
+
+* saml upgraded to ^4.0.0
+* @auth0/xmldom updated to 0.1.23
+* ejs updated to ^3.1.10
+* mocha pinned to 11.3.0
+* replace istanbul with nyc@15 for coverage
+* update CI workflow to Node.js 22.x; upgrade actions/checkout and actions/setup-node to v4
+
+
### [7.1.1](https://github.com/auth0/node-samlp/compare/v7.1.0...v7.1.1) (2023-11-20)
diff --git a/README.md b/README.md
index 5ee6128..e8ba025 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ SAML Protocol middleware to create SAMLP identity providers for node.js.
### Supported Node Versions
-node >= 12
+node >= 22
## Introduction
@@ -154,6 +154,18 @@ samlp.sendError({
})(req, res, next);
~~~~
+## Testing
+
+Tests require Node.js >= 22 and are run with Mocha:
+
+ npm test
+
+The test suite starts a real Express server (`test/fixture/server.js`) on `http://localhost:5050` before each test file runs and tears it down afterward. The server mounts the actual `samlp` middleware against a hardcoded fake user and fixture certificate/key pair (`test/fixture/samlp.test-cert.{pem,key}`). Tests make real HTTP requests to this server using the `request` library and assert against the responses.
+
+Individual test suites reconfigure the server between tests by directly mutating `server.options`, which is merged into the middleware options on each request — allowing cert, key, signing, and session-participant settings to be swapped without restarting the server.
+
+Test certificates and keys in `test/fixture/` are for local testing only and must not be used in production.
+
## Issue Reporting
If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues.
diff --git a/lib/metadata.js b/lib/metadata.js
index 51409f3..75e10b2 100644
--- a/lib/metadata.js
+++ b/lib/metadata.js
@@ -4,7 +4,7 @@ var encoders = require('./encoders');
var URL_PATH = '/FederationMetadata/2007-06/FederationMetadata.xml';
-function getEndpointAddress (req, endpointPath) {
+function getEndpointAddress (req, endpointPath, absoluteUrls) {
endpointPath = endpointPath ||
(req.originalUrl.substr(0, req.originalUrl.length - URL_PATH.length));
@@ -12,7 +12,7 @@ function getEndpointAddress (req, endpointPath) {
'https' :
(req.headers['x-forwarded-proto'] || req.protocol);
var host = req.headers['x-forwarded-host'] || req.headers['host'];
- return protocol + '://' + host + endpointPath;
+ return absoluteUrls ? endpointPath : protocol + '://' + host + endpointPath;
}
/**
@@ -50,17 +50,18 @@ function metadataMiddleware (options) {
var claimTypes = (options.profileMapper || PassportProfileMapper).prototype.metadata;
var issuer = options.issuer;
var pem = encoders.removeHeaders(options.cert);
+ var absoluteUrls = options.absoluteUrls || false;
return function (req, res) {
- var redirectEndpoint = getEndpointAddress(req, options.redirectEndpointPath);
- var postEndpoint = getEndpointAddress(req, options.postEndpointPath);
+ var redirectEndpoint = getEndpointAddress(req, options.redirectEndpointPath, absoluteUrls);
+ var postEndpoint = getEndpointAddress(req, options.postEndpointPath, absoluteUrls);
options.logoutEndpointPaths = options.logoutEndpointPaths || { redirect: '/logout' };
var logoutEndpoints = {};
['redirect', 'post'].forEach(function (binding) {
if (options.logoutEndpointPaths[binding]) {
- logoutEndpoints[binding] = getEndpointAddress(req, options.logoutEndpointPaths[binding]);
+ logoutEndpoints[binding] = getEndpointAddress(req, options.logoutEndpointPaths[binding], absoluteUrls);
}
});
diff --git a/package.json b/package.json
index 045fd9e..7e469bc 100644
--- a/package.json
+++ b/package.json
@@ -1,14 +1,14 @@
{
"name": "samlp",
- "version": "7.1.1",
+ "version": "7.2.0",
"engines": {
- "node": ">=12"
+ "node": ">=22"
},
"description": "SAML Protocol server middleware",
"main": "lib/index.js",
"scripts": {
"test": "./node_modules/.bin/_mocha -R spec --colors",
- "cover": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha -- -R spec --colors",
+ "cover": "nyc ./node_modules/.bin/_mocha -R spec --colors",
"open_cover": "open coverage/lcov-report/*.html",
"release": "standard-version"
},
@@ -25,12 +25,12 @@
"dependencies": {
"@auth0/thumbprint": "0.0.6",
"auth0-id-generator": "^0.2.0",
- "ejs": "^3.1.8",
+ "ejs": "^3.1.10",
"flowstate": "^0.4.0",
"querystring": "^0.2.0",
- "saml": "^3.0.1",
+ "saml": "^4.0.0",
"xml-crypto": "^2.0.0",
- "@auth0/xmldom": "0.1.21",
+ "@auth0/xmldom": "0.1.23",
"xpath": "0.0.5",
"xtend": "^1.0.3"
},
@@ -41,11 +41,39 @@
"cheerio-select": "~0.0.3",
"express": "^4.17.1",
"express-session": "^1.14.2",
- "istanbul": "^0.4.5",
- "mocha": "~8.2.1",
+ "nyc": "^15.1.0",
+ "mocha": "11.3.0",
"request": "~2.88.0",
"standard-version": "^9.1.0",
"timekeeper": "^2.2.0",
"uid2": "0.0.3"
+ },
+ "overrides": {
+ "@xmldom/xmldom": "^0.8.12",
+ "underscore": "^1.13.8",
+ "serialize-javascript": "^7.0.3",
+ "nth-check": "^2.0.1"
+ },
+ "nyc": {
+ "check-coverage": true,
+ "per-file": true,
+ "lines": 70,
+ "statements": 70,
+ "functions": 70,
+ "branches": 70,
+ "extension": [
+ ".js"
+ ],
+ "exclude": [
+ "test/**/*.js",
+ "coverage/**/*.js"
+ ],
+ "reporter": [
+ "html",
+ "lcov",
+ "text"
+ ],
+ "all": false,
+ "report-dir": "./coverage"
}
}
diff --git a/test/samlp.tests.js b/test/samlp.tests.js
index 9359b29..7036f62 100644
--- a/test/samlp.tests.js
+++ b/test/samlp.tests.js
@@ -5,6 +5,8 @@ var cheerio = require('cheerio');
var xmldom = require('@auth0/xmldom');
var xmlhelper = require('./xmlhelper');
var zlib = require('zlib');
+var crypto = require('crypto');
+var querystring = require('querystring');
var encoder = require('../lib/encoders');
var fs = require('fs');
var path = require('path');
@@ -604,18 +606,26 @@ describe('samlp', function () {
before(function (done) {
var SAMLRequest = 'http://sp';
+ var signingKey = fs.readFileSync(path.join(__dirname, 'fixture/samlp.test-cert.key'));
+ var sigAlg = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
- zlib.deflateRaw(new Buffer(SAMLRequest), function (err, buffer) {
+ zlib.deflateRaw(Buffer.from(SAMLRequest), function (err, buffer) {
if (err) return done(err);
+ var b64SAMLRequest = buffer.toString('base64');
+ var content = querystring.stringify({ SAMLRequest: b64SAMLRequest, RelayState: '123', SigAlg: sigAlg });
+ var signer = crypto.createSign('RSA-SHA1');
+ signer.update(content);
+ var signature = signer.sign(signingKey, 'base64');
+
request.get({
jar: request.jar(),
uri: 'http://localhost:5050/samlp',
qs: {
RelayState: '123',
- SAMLRequest: buffer.toString('base64'),
- Signature: 'HaX739zOyRn4PR2pi1Bud05rHbPGfppz5x5crr2EuOzLbfNuvLeK//ZCNsC/R/8B4CWe2SYYCYJ6UhBRvhCx8G7H92TIw8TjbsTfAWemp6mJh+zBqaI2It8sFZMYntsbd0jfBo4CbuM8872cNQkdedV5V56gaErjBA8z3HoyTWpQi9nH2fjtmDDfoQmoVum5q+vgbm103qxjH0j/gR+OXi5Rne8ijMLhhXgt9EdLmN8OS6l1LRUPe3XDLz6ZKbo9T2k6GR1x+w6bN18JOdeCwDn+nx4fmPbGGrcz/DT/3mTL5MY7TeRDz8rGSCZ5+yDNtmgQ9Nv2O//joonmRBkF6Q==',
- SigAlg: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
+ SAMLRequest: b64SAMLRequest,
+ Signature: signature,
+ SigAlg: sigAlg
}
}, function (err, response, b){
if(err) return done(err);
@@ -626,7 +636,7 @@ describe('samlp', function () {
body = b;
$ = cheerio.load(body);
var SAMLResponse = $('input[name="SAMLResponse"]').attr('value');
- samlResponse = new Buffer(SAMLResponse, 'base64').toString();
+ samlResponse = Buffer.from(SAMLResponse, 'base64').toString();
signedAssertion = /()/.exec(samlResponse)[1];
done();
});
@@ -687,7 +697,7 @@ describe('samlp', function () {
it('should return an error', function (done) {
doRawSAMLRequest(function (response) {
expect(response.statusCode).to.equal(400);
- expect(response.body).to.match(/error:\w+:PEM routines:\w+:no start line/);
+ expect(response.body).to.match(/(PEM routines|DECODER routines)/);
done();
});
});
@@ -736,7 +746,7 @@ describe('samlp', function () {
it('should return an error', function (done) {
doRawSAMLRequest(function (response) {
expect(response.statusCode).to.equal(400);
- expect(response.body).to.match(/error:\w+:PEM routines:\w+:no start line/);
+ expect(response.body).to.match(/(PEM routines|DECODER routines)/);
done();
});
});