Why a signature can't be found?
I've checked the code during execution and saw that signatures
was empty:
if (signatures.length !== 1) {
return false;
}
Because an ID
from Response
didn't match URI
from Reference
<saml2p:Response ID="K0WO32GJF96P5SJBU9I5V0YI53OBOCIUSG6G4RX9" ...>
...
<ds:Reference URI="#Q65FHLGKK1SSG05I7UWVEQS5URLD0EUVKIP0GY06">
passport-saml
source code:
an excerpt from it:
const validateSignature = (fullXml, currentNode, certs) => {
const xpathSigQuery = ".//*[" +
"local-name(.)='Signature' and " +
"namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#' and " +
"descendant::*[local-name(.)='Reference' and @URI='#" +
currentNode.getAttribute("ID") +
"']" +
"]";
const signatures = exports.xpath.selectElements(currentNode, xpathSigQuery);
// it fails here coz it's empty
if (signatures.length !== 1) {
return false;
}
...
}
Content ofxpathSigQuery
variable:
const xpathSigQuery = './/*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#' and descendant::*[local-name(.)='Reference' and @URI='#K0WO32GJF96P5SJBU9I5V0YI53OBOCIUSG6G4RX9']]'
Content of fullXml
argument:
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" Destination="https://ppt2cf-8080.csb.app/login-idp/callback" ID="K0WO32GJF96P5SJBU9I5V0YI53OBOCIUSG6G4RX9" InResponseTo="_ed3b8b9d9b6bb14d1e9660ca902a8c0da7708c9e" IssueInstant="2023-06-23T05:40:25.157Z" Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://ppt2cf-8080.csb.app/login-idp/callback</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion ID="Q65FHLGKK1SSG05I7UWVEQS5URLD0EUVKIP0GY06" IssueInstant="2023-06-23T05:40:25.157Z" Version="2.0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://ppt2cf-8080.csb.app/login-idp/callback</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#Q65FHLGKK1SSG05I7UWVEQS5URLD0EUVKIP0GY06">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>yHH5fUXBy+hkTKCvhTutPTvzJc/vwNnlwE3tZiDVJBI=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>AWGSLsBapRN9O2YTOwb/HBe/kALxU6wmhNLoSNwb+UO1kFC50sMQ+wM8DDmR0Zgq08iB68Y8ckI5KXQZhFH0xUvgmt1gqB8NCXx3C7Zx9W0vhX+vEURkasBigybFSUWf22VqndUTIhHbS7/QXj+xi4C9Ujz3WLP8Txx+DGFc4GcnAIvD46YAzD0NhU65+T5sCoyTmVIQ3WcEpyQkvWjxvzAq3yTHj2YJ54w7c+X8RTtFMKh0aq48Ec+FJHpl1VdRx7vXkD089qpUpA6CvOZu88WlUJ9+82hF+JVRb10WG6TzVnw3IjrD5n6r2BmdnaGUQ9yG52CP+nk5auB7YCwnVg==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIDgDCCAmgCCQCLAP0JHUDNijANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCVUExEDAOBgNVBAgMB0RvbmJhc3MxDTALBgNVBAcMBEtpZXYxEjAQBgNVBAoMCURpZ2l0YWxpczENMAsGA1UECwwERGV2czEMMAoGA1UEAwwDRGFuMSAwHgYJKoZIhvcNAQkBFhFkZW5pc0BheG9ub3BzLmNvbTAeFw0yMzA2MTIwMzI4MDRaFw0yNjA2MTEwMzI4MDRaMIGBMQswCQYDVQQGEwJVQTEQMA4GA1UECAwHRG9uYmFzczENMAsGA1UEBwwES2lldjESMBAGA1UECgwJRGlnaXRhbGlzMQ0wCwYDVQQLDAREZXZzMQwwCgYDVQQDDANEYW4xIDAeBgkqhkiG9w0BCQEWEWRlbmlzQGF4b25vcHMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA43efCJashd9GkzSK8hPtCZ1DburaGB6dow1wLVneAkidS7O7hnueGtbWGyKj7tMZ+6fh7cXl5DqtO/xu/Fz6LCT3EdGPCA3P+ozyqn62aF+qlQGm3QpR5sRVJ0/hYV5f5u883CR+72rV+S4xeXGISXuRqbobqnHnqB4f2rcT1tOpujXpMu2Awa+bjObjRGBCwkRvgIMGs0xbhbIiMPi35xqkWo7IfpFsFXXyHij+mdKBeaUOQEMHUJ2Xt6z8gYOMRy+hPxexYVTZl/chs918pWSMzU3uu3B+nzEXflJ8T4UMx2pYfnAFP7Ifccj2aQZm+p2GFnOc1TyE3iNTssJaqwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAP+/SKddgUzT+lLvBR3mQT++DuII2WLr4b7/kYMXp5ohOJsh3D7a2TbGxzHEsK7SNZ1kKjxUhlFJ9kLWUmn3AakPDl46C/b7E0FEmjVbU/H6kJw8BPrRj6zyMVZBNWpvi8IyhGDKAgfVHXY6wicKy70QXSZyoR/WhpJ9SEwr4iodeAjVGsDjPYn5S+/4n+WHqH5IWYvukHU0VCNltRfy4Axwhl28eUW4u7ivQXBchUsaSEdCMFWgJYUteU6mC1myfG9hlbiZybqBbsSfVXVFMUxrtdWrSc926qfwJd1rYSUOJwzhGg0aAa8ngatq2HUDb5KnxKapkwEjm7M7zl4ZsB</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">denis@axonops.com</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData InResponseTo="_ed3b8b9d9b6bb14d1e9660ca902a8c0da7708c9e" NotOnOrAfter="2023-06-23T05:45:25.157Z" Recipient="https://ppt2cf-8080.csb.app/login-idp/callback"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2023-06-23T05:35:25.157Z" NotOnOrAfter="2023-06-23T05:45:25.157Z">
<saml2:AudienceRestriction>
<saml2:Audience>https://ppt2cf-8080.csb.app/saml/metadata</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement AuthnInstant="2023-06-23T05:40:25.157Z" SessionIndex="2fe7b4b9-36ce-4f47-933d-df071e906e16">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2p:Response>
passport-saml config:
const passport = require("passport");
const passportSaml = require("@node-saml/passport-saml");
const fs = require("fs");
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
const strategy = new passportSaml.Strategy(
{
entryPoint: "https://sso.jumpcloud.com/saml2/test-app",
issuer: "https://sso.jumpcloud.com",
path: "login-idp/callback",
cert: fs.readFileSync("./certs/cert.pem", "utf-8"),
privateKey: fs.readFileSync("./certs/private.pem", "utf-8"),
signatureAlgorithm: "sha256",
identifierFormat: null,
// From the metadata document
audience: "https://ppt2cf-8080.csb.app/saml/metadata",
},
(profile, done) => {
return done(null, profile);
}
);
passport.use(strategy);
module.exports = {
passport,
strategy,
};
app.js
const express = require("express");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const path = require("path");
const bodyParser = require("body-parser");
const { passport: passportConfig } = require("./config/passport");
const router = require("./routes/index");
const app = express();
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
// cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }, // 7 days
})
);
app.use(cookieParser());
// app.use(express.static(path.join(__dirname, "public")));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passportConfig.initialize());
app.use(passportConfig.session());
app.use(router);
const listener = app.listen(8080, function () {
console.log("Listening on port " + listener.address().port);
});
routes.js
const express = require("express");
const fs = require("fs");
const bodyParser = require("body-parser");
const { passport, strategy } = require("./../config/passport");
const router = express.Router();
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) return next();
// else return res.redirect("/login-idp");
else
return res.send(
`<button onclick="location.pathname='/login-idp'">Login</botton>`
);
}
router.get("/", ensureAuthenticated, function (req, res) {
res.send("Authenticated");
});
router.get(
"/login-idp",
passport.authenticate("saml", {
failureFlash: true,
failureRedirect: "/saml/auth/error",
// successRedirect: '/saml/auth/success',
}),
function (req, res) {
console.log("==> saml/login callback");
res.redirect("/");
}
);
router.post(
"/login-idp/callback",
bodyParser.urlencoded({ extended: false }),
passport.authenticate("saml", {
failureRedirect: "/login/fail",
failureFlash: true,
}),
function (req, res) {
console.log("==> login/callback");
res.redirect("/");
}
);
router.get("/login/fail", function (req, res) {
res.status(401).send("Login failed");
});
router.get("/saml/metadata", function (req, res) {
res.type("application/xml");
res
.status(200)
.send(
strategy.generateServiceProviderMetadata(
fs.readFileSync("./certs/cert.pem", "utf8"),
)
);
});
module.exports = router;
package.json:
{
"name": "express-example-starter",
"version": "1.0.0",
"description": "An Express-based application skeleton",
"main": "app.js",
"scripts": {
"start": "nodemon app.js localhost 8080"
},
"dependencies": {
"@node-saml/passport-saml": "^4.0.4",
"body-parser": "^1.20.2",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-session": "^1.17.3",
"morgan": "~1.9.1",
"passport": "^0.6.0",
"saml2js": "^0.1.2",
"samlify": "^2.8.10",
"useragent": "^2.3.0"
},
"devDependencies": {
"nodemon": "1.18.4"
},
"keywords": [
"expressjs",
"express"
]
}
This question (with same content as this SO question) is answered here: https://github.com/node-saml/passport-saml/discussions/870#discussioncomment-6278844
Short answer: Your IdP is signing only assertion. Starting from @node-saml/passport-saml's version 4.0.0 default configuration is to require signed top/response level signature and signed assertion. It is not finding top/response level signature thus it reports it cannot find signature. Developer may configure different settings. For more information see linked answer from passport-saml discussions.