This is a SAML 2.0 authentication provider for Passport, the Node.js authentication library.
Passport-SAML has been tested to work with OneLogin, Okta, Shibboleth, SimpleSAMLphp based Identity Providers, and with Active Directory Federation Services.
npm install @node-saml/passport-samlPassport-SAML is the service provider (SP) side of a SAML login: it sends users to your identity
provider (IdP) to log in, and validates the response the IdP sends back. Register your site with the
IdP first; most IdPs accept the
metadata that Passport-SAML
generates. From the IdP you need its single sign-on URL, for entryPoint, and its signing
certificate, for idpCert.
The examples use ES modules: save them as .mjs files, or set "type": "module" in package.json.
With CommonJS, require() the same names. The examples use https://sp.example.com for your site
and https://idp.example.com for the IdP.
Most of the configuration options for the Strategy constructor are passed through to the underlying
node-saml library. For more details on the configuration options and how the underlying SAML flows
work, see the
node-saml documentation
These are the Strategy parameters related directly to passport-saml. For the full list of
parameters, see the
node-saml documentation
additionalParams: dictionary of additional query params to add to all requests; if an object with this key is passed toauthenticate, the dictionary of additional query params will be appended to those present on the returned URL, overriding any specified by initialization options' additional parameters (additionalParams,additionalAuthorizeParams, andadditionalLogoutParams)passReqToCallback: if truthy,reqwill be passed as the first argument to the verify callback (default:false)name: Optionally, provide a custom name. (default:saml). Useful if you want to instantiate the strategy multiple times with different configurations, allowing users to authenticate against multiple different SAML targets from the same site. You'll need to use a unique set of URLs for each target, and use this custom name when callingpassport.authenticate()as well.
The IdP sends its response to callbackUrl;
Provide the authentication callback adds the route for it.
import { readFileSync } from "node:fs";
import passport from "passport";
import { Strategy as SamlStrategy } from "@node-saml/passport-saml";
const samlStrategy = new SamlStrategy(
{
callbackUrl: "https://sp.example.com/login/callback",
entryPoint: "https://idp.example.com/sso",
issuer: "https://sp.example.com/metadata",
idpCert: readFileSync("./idp-signing-cert.pem", "utf-8"),
},
// Sign-on: return the user the IdP authenticated
async (profile, done) => {
try {
done(null, await findUserByEmail(profile.email));
} catch (err) {
done(err);
}
},
// Logout: return the user the IdP is logging out
async (profile, done) => {
try {
done(null, await findUserByNameID(profile.nameID));
} catch (err) {
done(err);
}
},
);
passport.use(samlStrategy);findUserByEmail() and findUserByNameID() stand in for your own user lookups. profile holds the
SAML assertion's nameID and attributes; profile.email comes from an email or mail attribute,
if the IdP sends one.
- Sign-on: call
done(null, user)to log the user in. A missing user (null,undefinedorfalse) fails the login, anddone(err)reports an error. - Logout: runs when the IdP sends a
LogoutRequest. Passport-SAML compares the user you return withreq.user, usingassert.deepStrictEqual, and tells the IdP the logout succeeded only if they match. It logs out the current session either way.
To choose the SAML configuration per request, use MultiSamlStrategy and pass it a getSamlOptions
function:
import passport from "passport";
import { MultiSamlStrategy } from "@node-saml/passport-saml";
passport.use(
new MultiSamlStrategy(
{
passReqToCallback: true, // pass `req` to the sign-on and logout functions below
getSamlOptions: async (req, done) => {
try {
const provider = await findProvider(req);
done(null, provider.samlOptions);
} catch (err) {
done(err);
}
},
},
// Sign-on
async (req, profile, done) => {
try {
done(null, await findUserByEmail(profile.email));
} catch (err) {
done(err);
}
},
// Logout
async (req, profile, done) => {
try {
done(null, await findUserByNameID(profile.nameID));
} catch (err) {
done(err);
}
},
),
);getSamlOptions runs on every request that reaches the strategy. findProvider() stands in for
your own lookup. Call done(null, options) with that provider's configuration, or done(err). The
options are merged over the ones passed to MultiSamlStrategy, so settings shared by every provider
can go in the constructor. callbackUrl, issuer and idpCert must be set after the merge.
MultiSamlStrategy builds a new node-saml SAML instance from the merged options for each
request. If those options have no cacheProvider, each instance starts with its own empty in-memory
cache, and the callback never finds the request ID recorded when the login started. With
validateInResponseTo: "always", every login is then rejected, and with "ifPresent", every
response whose Response element carries InResponseTo. Return a cacheProvider from
getSamlOptions, the same one for a provider on every call, so a response finds the cache its
request was recorded in. One cacheProvider passed to the MultiSamlStrategy constructor also
works, but all providers share it, so a response to one provider's request passes this check at
another provider's callback.
A cacheProvider is an object with saveAsync(key, value), getAsync(key) and removeAsync(key)
methods, described by the CacheProvider TypeScript type. Back it with a store that every process
handling your logins can reach, such as Redis or your database. node-saml removes a request ID
only when a response to it arrives, so have the store expire entries after
requestIdExpirationPeriodMs (8 hours by default); otherwise the IDs of abandoned logins stay
forever.
The IdP posts its response to callbackUrl as a form. Add a route for that path that parses the
form body before passport.authenticate(). express.urlencoded() is built into Express 4.16 and
later, including Express 5.
app.post(
"/login/callback",
express.urlencoded({ extended: false }),
passport.authenticate("saml", { failureRedirect: "/" }),
(req, res) => {
res.redirect("/");
},
);Use passport.authenticate(), specifying saml as the strategy, on the route that starts a login.
It sends the browser to the IdP's entryPoint:
app.get("/login", passport.authenticate("saml"));...or, if you wish to add or override query string parameters:
app.get(
"/login",
passport.authenticate("saml", { additionalParams: { username: "user@example.com" } }),
);These parameters go on the redirect URL, so a login with authnRequestBinding: "HTTP-POST" does not
send them.
In addition to passing the additionalParams option to passport.authenticate, you can also pass
samlFallback, either as "login-request" or "logout-request". By default, this is set to
"login-request". However, in the case of the req.query and the req.body not containing a
SAMLRequest or SAMLResponse, this can be used to dictate which request handler is used in cases
where it can not be determined by these standard properties.
RelayState is a short value that goes to the IdP with a SAML request and comes back, unchanged,
with the IdP's response. Use it to send users back to the page they asked for before they had to log
in.
To send one, put RelayState in the query string or form body of the request that starts the login.
Passport-SAML sends it to the IdP alongside the AuthnRequest: as a query parameter with the
HTTP-Redirect binding, or as a hidden form field with HTTP-POST. The /login route above needs no
changes:
// When a page needs a logged-in user, send them to log in and remember where they were going
res.redirect(`/login?RelayState=${encodeURIComponent(req.originalUrl)}`);The IdP posts it back to your callback URL along with the SAMLResponse, where it is available as
req.body.RelayState. Passport-SAML passes it through without acting on it.
RelayState is not part of the signed SAML response: the IdP posts it as a separate form field, so
anyone can change it on its way back to you. Treat it as untrusted input. Redirecting to it without
checking it turns your callback into an
open redirect.
For example, follow it only when it is a path on your own site:
app.post(
"/login/callback",
express.urlencoded({ extended: false }),
passport.authenticate("saml", { failureRedirect: "/" }),
(req, res) => {
res.redirect(localPathOr(req.body.RelayState, "/"));
},
);
// Returns `value` if it is a path on this site; otherwise `fallback`.
function localPathOr(value, fallback) {
if (typeof value !== "string") return fallback;
const base = "https://sp.invalid"; // placeholder origin, used only to parse `value`
try {
const url = new URL(value, base);
// A browser reads a path that starts with "//" as the address of another site
if (url.origin === base && !url.pathname.startsWith("//")) {
return url.pathname + url.search + url.hash;
}
} catch {
// not a valid URL
}
return fallback;
}The SAML 2.0 bindings specification
(sections 3.4.3 and 3.5.3)
limits RelayState to 80 bytes, so an IdP may reject a longer value. It also recommends protecting
the value from tampering "by using a checksum, a pseudo-random value, or similar means". If your
paths can be longer than 80 bytes, or you want that protection, store the destination on the server
under a short random key and send the key as RelayState instead.
Logout works the same way. RelayState on the request that starts a logout, through
strategy.logout(req, callback) or samlFallback: "logout-request", is sent with the
LogoutRequest and comes back with the IdP's LogoutResponse, in req.query.RelayState or
req.body.RelayState depending on the binding. When the IdP starts the logout, Passport-SAML
returns the IdP's RelayState in its LogoutResponse, as the specification requires.
RelayState only comes back if the IdP received your request. If entryPoint is a link that starts
an IdP-initiated login, rather than the IdP's single sign-on service URL (the SingleSignOnService
location in its metadata), the IdP ignores the AuthnRequest and the RelayState along with it. In
an IdP-initiated login, req.body.RelayState holds whatever the IdP is configured to send, if
anything.
Generates your SP's metadata, which you can give to the IdP when you register your site. Serve it from a route:
app.get("/metadata", (req, res) => {
res.type("application/xml").send(samlStrategy.generateServiceProviderMetadata(null));
});Pass decryptionCert when you configure decryptionPvk, and signingCert when you configure
privateKey; otherwise pass null for decryptionCert and leave out signingCert. For details,
see Service provider metadata in
the node-saml documentation.
The generateServiceProviderMetadata method is also available on the MultiSamlStrategy, but needs
an extra request and a callback argument
(generateServiceProviderMetadata(req, decryptionCert, signingCert, callback)). It passes req to
getSamlOptions to retrieve the correct configuration, then calls callback(err, metadata).
Here is a configuration for ADFS:
const adfsOptions = {
entryPoint: "https://adfs.example.com/adfs/ls/",
issuer: "https://sp.example.com/login/callback",
callbackUrl: "https://sp.example.com/login/callback",
idpCert: "MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W==",
authnContext: ["http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/windows"],
identifierFormat: null,
};ADFS needs a relying party trust for your service, with issuer as its identifier.
By default, ADFS signs only the assertion in its response, and Passport-SAML rejects a response
whose outer Response element is not signed. Have ADFS sign both:
Set-AdfsRelyingPartyTrust -TargetName "<relying party name>" -SamlResponseSignature MessageAndAssertionIf you can't change the relying party trust, set wantAuthnResponseSigned: false instead. The
assertion's signature is still required.
For more detailed instructions, see ADFS documentation.
Passport-SAML has built in support for SLO from Node-SAML.
Note: Fully functional IdP initiated SLO support is not provided out of the box. You have to inspect your use cases / implementation / deployment scenarios (location of IdP in respect to SP) and consider things / cases listed e.g. at issue(s) #221 and #419. This library provides you a mechanism to veto "Success" result but it does not provide hooks/interfaces to implement support for IdP initiated SLO which would work under all circumstances. You have to do it yourself.
See Releases to find the changes that go into each release. Additionally, see the CHANGELOG.
Gerard Braad has provided an example app at https://github.com/gbraad/passport-saml-example/. It
was written for passport-saml 1.x, so some of its option names have changed since; for example,
cert is now idpCert. Use this README for the current options.
We only support Long-Term Support versions of Node.
We specifically limit our support to LTS versions of Node, not because this package won't work on other versions, but because we have a limited amount of time, and supporting LTS offers the greatest return on that investment.
It's possible this package will work correctly on newer versions of Node. It may even be possible to use this package on older versions of Node, though that's more unlikely as we'll make every effort to take advantage of features available in the oldest LTS version we support.
As each Node LTS version reaches its end-of-life we will remove that version from the node
engines property of our package's package.json file. Removing a Node version is considered a
breaking change and will entail the publishing of a new major version of this package. We will not
accept any requests to support an end-of-life version of Node. Any merge requests or issues
supporting an end-of-life version of Node will be closed.
We will accept code that allows this package to run on newer, non-LTS, versions of Node.
The project was started by @bergie in 2012 based on Michael Bosworth's express-saml library. From 2014 - 2016, @ploer served as primary maintainer. @markstos served as the primary maintainer from 2017 till 2020 when he created the node-saml organization. With a goal to create a team of maintainers, invitations were sent to major contributors and fork authors to work together to maintain all the improvements in one place.
Since 2020, @cjbarth emerged as the primary maintainer, with major contributions from @gugu and
@zoellner. Major updates from the team included rewriting the project in TypeScript and splitting
off a node-saml module which can be used without Passport. Almost 100 other developers have
contributed improvements to the project.
The project continues to be maintained by volunteers. Contributions small and large are welcome.
“OASIS”, “SAML”, and “Security Assertion Markup Language” are trademarks of OASIS, the open standards consortium where the SAML specification is owned and developed. SAML is a copyrighted © work of OASIS Open. All rights reserved.


