SAML 2.0 SP

SAML 2.0 (OASIS) is the federation protocol financial / government / enterprise IdPs still ship — operators can't always require an OIDC IdP. This primitive implements the SP side only:

- AuthnRequest builder (HTTP-Redirect + HTTP-POST bindings) - Response parser: * Verify Response or Assertion XMLDSig (whichever the IdP signed) using the IdP's signing certificate * Refuse signature-wrapping by enforcing single-element- match on the Reference URI (via xml-c14n) * Validate NotOnOrAfter / NotBefore / Recipient / InResponseTo on SubjectConfirmation * Validate Conditions/NotBefore/NotOnOrAfter/ AudienceRestriction - SP metadata XML emitter - MDQ (RFC 8414-style metadata-query) fetch with strict server-identity per RFC 9525

Operators wire two routes:

/saml/login → returns the AuthnRequest URL (Redirect binding) OR an HTML form (POST binding) the user-agent submits to the IdP's SSO endpoint. /saml/acs → AssertionConsumerService — receives the IdP's SAMLResponse, calls verifyResponse, hydrates the user session.

Storage of InResponseTo / RelayState pre-image / nonce is operator-side via b.cache or b.session — the framework gives the parsing + verification primitive; operators wire freshness + replay defense.

b.auth.saml.sp.create(opts) #

stable0.8.62
{
  {
    entityId:                    string,    // this SP's entityID URL
    assertionConsumerServiceUrl: string,    // SP /saml/acs endpoint
    idpEntityId:                 string,
    idpSsoUrl:                   string,    // IdP single-sign-on endpoint
    idpCertPem:                  string,    // IdP signing cert (PEM)
    audience?:                   string,    // default = entityId
    clockSkewSec?:               number,    // default 60
    nameIdFormat?:               string,    // optional NameIDPolicy/Format
  }
}

Build a SAML 2.0 SP. Operators supply: - the SP entityId (this RP's URL) - assertionConsumerServiceUrl (the /saml/acs route) - idpEntityId + idpSsoUrl + idpCertPem (the trust anchor for this SP — typically rotated quarterly via MDQ)

var sp = b.auth.saml.sp.create({
  entityId:                    "https://sp.example",
  assertionConsumerServiceUrl: "https://sp.example/saml/acs",
  idpEntityId:                 "https://idp.example",
  idpSsoUrl:                   "https://idp.example/sso",
  idpCertPem:                  process.env.IDP_CERT_PEM,
});

b.auth.saml.sp.buildAuthnRequest(opts) #

0.8.62
{
  { relayState?: string }
}

Build a SAMLRequest XML + the URL-safe deflate-base64 encoding for the HTTP-Redirect binding. Returns { id, redirectUrl, raw } where id is the AuthnRequest ID the SP must remember (binds to the response's InResponseTo).

var ar = sp.buildAuthnRequest({ relayState: "/dashboard" });
res.statusCode = 302;
res.setHeader("Location", ar.redirectUrl);
res.end();
// remember ar.id; expect it back in the Response InResponseTo

b.auth.saml.sp.verifyResponse(samlResponseB64, vopts) #

0.8.62
{
  {
    expectedInResponseTo?: string,   // the AuthnRequest ID this is responding to
    now?:                  number,   // timestamp override for tests
  }
}

Parse + verify the IdP's SAMLResponse (the base64-encoded XML the user-agent POSTs to /saml/acs). Validates the XMLDSig (Response-level OR Assertion-level signature), the assertion's SubjectConfirmation Bearer constraints, and Conditions audience + time bounds. Returns { nameId, nameIdFormat, sessionIndex, attributes, audience, inResponseTo, issuer }.

app.post("/saml/acs", function (req, res) {
  var info = sp.verifyResponse(req.body.SAMLResponse, {
    expectedInResponseTo: req.session.samlRequestId,
  });
  // → { nameId, nameIdFormat, sessionIndex, attributes, audience, inResponseTo, issuer }
});

b.auth.saml.sp.metadata(metaOpts?) #

0.8.62

Emit the SP's EntityDescriptor XML for IdP-side configuration. Operators serve this verbatim at /saml/metadata.

app.get("/saml/metadata", function (req, res) {
  res.setHeader("Content-Type", "application/samlmetadata+xml");
  res.end(sp.metadata());
});

b.auth.saml.sp.buildLogoutRequest(opts) #

stable0.10.16
{
  nameId:         string,                          // user's NameID from the original AuthnResponse
  nameIdFormat:   string,                          // optional NameID Format URI
  sessionIndex:   string,                          // SessionIndex from the original Assertion AuthnStatement
  relayState:     string,                          // optional opaque blob round-tripped to LogoutResponse
  signingKey:     Uint8Array | string | KeyObject, // PQC private key (b.pqcSoftware.ml_dsa_*.keygen()) for ML-DSA;
                                                   // PEM string or node:crypto KeyObject for RSA / ECDSA / Ed25519
  signingAlg:     "rsa-sha256" | "rsa-sha384" | "rsa-sha512" |
                  "ecdsa-sha256" | "ecdsa-sha384" | "ecdsa-sha512" |
                  "ed25519" | "ml-dsa-65" | "ml-dsa-87",   // default omitted → unsigned
}

Build a SAML 2.0 LogoutRequest XML + the URL-safe deflate-base64 encoding for the HTTP-Redirect binding. When signingKey / signingAlg are supplied, computes the binding-§3.4.4.1 canonical query-string signature so the IdP can verify the request originated from a trusted SP. The signature is computed over SAMLRequest=&[RelayState=&]SigAlg= in that exact order (no re-sorting per the spec).

var lr = sp.buildLogoutRequest({
  nameId: "alice@idp", sessionIndex: "_session-9876",
  signingKey: kp.secretKey, signingAlg: "ml-dsa-65",
});
res.statusCode = 302;
res.setHeader("Location", lr.redirectUrl);

b.auth.saml.sp.parseLogoutRequest(samlRequestB64, vopts?) #

stable0.10.16
{
  queryString:        string,             // raw query string (everything after `?` in the redirect URL)
  idpVerifyKey:       Uint8Array,         // IdP's PQC public key
  idpVerifyAlg:       "ml-dsa-65" | "ml-dsa-87" | "ed25519",
}

Parse an inbound LogoutRequest (IdP-initiated SLO). Returns { id, nameId, nameIdFormat, sessionIndex, issuer, issueInstant }. When vopts.idpVerifyKey is supplied with vopts.queryString, verifies the HTTP-Redirect-binding signature against the IdP key.

var req = sp.parseLogoutRequest(req.query.SAMLRequest, {
  queryString: req.url.split("?")[1],
  idpVerifyKey: idpKp.publicKey,
  idpVerifyAlg: "ml-dsa-65",
});

b.auth.saml.sp.buildLogoutResponse(opts) #

stable0.10.16
{
  inResponseTo: string,                          // required — LogoutRequest ID being responded to
  destination:  string,                          // required — IdP SLO endpoint URL the response posts to
  statusCode:   string,                          // optional — SAML status URI; default Success
  relayState:   string,                          // optional — opaque blob from the matching LogoutRequest
  signingKey:   Uint8Array,                      // PQC private key (b.pqcSoftware.ml_dsa_*.keygen())
  signingAlg:   "ml-dsa-65" | "ml-dsa-87" | "ed25519",   // default omitted → unsigned
}

Build a SAML 2.0 LogoutResponse to an IdP-initiated LogoutRequest. Status defaults to urn:oasis:names:tc:SAML:2.0:status:Success. Same HTTP-Redirect binding + optional canonical-query signature as buildLogoutRequest.

var resp = sp.buildLogoutResponse({
  inResponseTo: incoming.id,
  destination:  "https://idp.example/slo",
  signingKey:   kp.secretKey,
  signingAlg:   "ml-dsa-65",
});
res.writeHead(302, { Location: resp.redirectUrl });

b.auth.saml.sp.parseLogoutResponse(samlResponseB64, vopts?) #

stable0.10.16
{
  queryString:   string,             // raw URL query (everything past `?`)
  idpVerifyKey:  Uint8Array,
  idpVerifyAlg:  "ml-dsa-65" | "ml-dsa-87" | "ed25519",
  expectedInResponseTo: string,      // optional — refuses on mismatch
}

Parse + verify an inbound SAML 2.0 LogoutResponse (the IdP's acknowledgement of a previously-issued LogoutRequest). Returns { id, inResponseTo, statusCode, issuer, success } where success is true when statusCode equals the spec's success URN. When vopts.idpVerifyKey is supplied, verifies the redirect-binding signature against the IdP key (same shape as parseLogoutRequest).

var resp = sp.parseLogoutResponse(req.query.SAMLResponse, {
  queryString: req.url.split("?")[1],
  idpVerifyKey: idpPub,
  idpVerifyAlg: "ml-dsa-65",
  expectedInResponseTo: storedLogoutRequestId,
});
resp.success;   // → true on Success status

b.auth.saml.sp.buildLogoutRequestPost(opts) #

stable0.10.16
{
  nameId, nameIdFormat, sessionIndex, relayState — same as buildLogoutRequest
  signingKey, signingAlg — PQC ml-dsa-65 / ml-dsa-87 (Ed25519 also
                           accepted; URN identifies the alg)
}

HTTP-POST variant of buildLogoutRequest. Returns the base64-encoded SAMLRequest body for the IdP's /slo POST endpoint along with an embedded XMLDSig-Enveloped signature (when signingKey is supplied). The signature is computed over the canonical SignedInfo element per XMLDSig §4.5 — the referenced LogoutRequest is canonicalized via exclusive-c14n, SHA3-512 digested, and that digest goes into the Reference's DigestValue before SignedInfo itself is canonicalized + signed.

var lr = sp.buildLogoutRequestPost({
  nameId: "alice@idp", signingKey: kp.secretKey, signingAlg: "ml-dsa-65",
});
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.end(lr.formHtml);   // auto-POSTs SAMLRequest to lr.action

b.auth.saml.sp.parseLogoutRequestPost(samlRequestB64, vopts?) #

stable0.10.16
{
  idpVerifyKey: Uint8Array,                      // optional — verify embedded XMLDSig signature against this IdP key
  idpVerifyAlg: "ml-dsa-65" | "ml-dsa-87" | "ed25519",   // required when idpVerifyKey is supplied
}

HTTP-POST variant of parseLogoutRequest. Decodes the base64 SAMLRequest body, parses the XML, and (when idpVerifyKey / idpVerifyAlg are supplied) verifies the embedded XMLDSig- Enveloped signature against the IdP key. Refuses when the signature element is missing, the Reference URI doesn't match the document root ID, the digest doesn't match the canonicalized referenced element (signature-wrapping defense), or the SignedInfo signature doesn't verify.

var req = sp.parseLogoutRequestPost(req.body.SAMLRequest, {
  idpVerifyKey: idpPubKey, idpVerifyAlg: "ml-dsa-65",
});
// req.nameId / req.sessionIndex / req.issuer

b.auth.saml.sp.buildLogoutRequestSoap(opts) #

stable0.10.16
{
  same as buildLogoutRequestPost
}

SOAP variant of buildLogoutRequest (SAML Bindings §3.2 — synchronous back-channel binding). Wraps the LogoutRequest in ... for an HTTP POST to the IdP's SOAP endpoint. Embeds an XMLDSig-Enveloped signature on the LogoutRequest itself (not the SOAP envelope) when signingKey is supplied — matching the IdP-side parse expectation.

var lr = sp.buildLogoutRequestSoap({ nameId: "alice@idp" });
var resp = await b.httpClient.request(lr.action, {
  method:  "POST",
  body:    lr.body,
  headers: { "Content-Type": "text/xml; charset=utf-8" },
});
var result = sp.parseLogoutResponseSoap(resp.body);

b.auth.saml.sp.parseLogoutResponseSoap(soapXml, vopts?) #

stable0.10.16
{
  idpVerifyKey: Uint8Array,                      // optional — verify embedded XMLDSig signature against this IdP key
  idpVerifyAlg: "ml-dsa-65" | "ml-dsa-87" | "ed25519",   // required when idpVerifyKey is supplied
}

Parse a SOAP-wrapped LogoutResponse from the IdP's synchronous back-channel reply. Unwraps the soapenv:Body, optionally verifies the XMLDSig signature, and returns the same shape as parseLogoutResponse.

var result = sp.parseLogoutResponseSoap(resp.body, {
  idpVerifyKey: idpPubKey, idpVerifyAlg: "ml-dsa-65",
});
// result.success / result.statusCode / result.inResponseTo

b.auth.saml.fetchMdq(opts) #

stable0.8.62
{
  {
    baseUrl:        string,
    entityId:       string,
    trustCertPem?:  string,    // PEM of the federation operator's signing cert
  }
}

Fetch an entity's signed metadata from a Metadata Query (MDQ) server per SAML 2.0 MDQ. Composes b.httpClient with strict server- identity (RFC 9525) and verifies the metadata XMLDSig against the operator-supplied trust cert. Returns the raw metadata XML on success.

The MDQ URL pattern is /entities/{sha1(entityId)} per the spec — operators with a federation MDQ deployment supply the baseUrl + their pinned trust cert.

var xml = await b.auth.saml.fetchMdq({
  baseUrl:      "https://mdq.federation.example",
  entityId:     "https://idp.example",
  trustCertPem: process.env.FEDERATION_TRUST_CERT_PEM,
});

Last updated 2026-08-08T16:39:49.652Z by seeder.