SAML 2.0 — HTTP-Redirect for the request, HTTP-POST for the response.
This is why callbackMethod exists
SAML’s response is not a redirect: the identity provider returns the person by POSTing a form to the callback.
The adapter declares 'POST', the plugin mounts the route that serves it, and
nothing about the seam had to change to add a second protocol shape — which was
the whole bet the SSO port made.
Install
npm install @apograph/identity-provider-samlimport { createSamlProvider } from '@apograph/identity-provider-saml';
IdentityPlugin(config.plugins.identity, {
sso: {
providers: [{ name: 'saml', provider: createSamlProvider(settings) }]
}
});Configuration
| Variable | What it does |
|---|---|
SSO_SAML_ENTRY_POINT | The identity provider’s sign-in URL |
SSO_SAML_IDP_CERT | Its signing certificate |
SSO_SAML_ISSUER | This service provider’s entity id. Defaults to the public base URL |
SSO_SAML_SUBJECT_ATTRIBUTE | An immutable directory id, when the NameID will not do |
SSO_SAML_EMAIL_ATTRIBUTE | The attribute carrying the address |
SSO_SAML_GROUPS_ATTRIBUTE | The attribute carrying group membership |
SSO_SAML_EMAIL_VERIFIED | Assert this directory owns the addresses it reports |
SSO_SAML_NAME / SSO_SAML_LABEL | Registration name and button text |
The certificate is the trust relationship
SAML has no discovery document and no key endpoint, so the entry point and the certificate are both required — and a certificate rollover is a configuration change rather than something that heals itself.
That is the practical difference from OIDC, where key rotation is survived without a restart.
The XML is @node-saml/node-saml’s
Canonicalisation, signature validation and the conditions checks are its job.
That is a deliberate dependency: XML signature validation has a long history of wrapping attacks that turn on parser details, and it is not a place to demonstrate independence. It is permitted here and forbidden in the identity domain and server packages.
What this package owns is everything the library has no opinion about — which is where the interesting mistakes live.
emailVerified is always an assertion
SAML carries no verification claim at all
No assertion has the equivalent of email_verified, so there is nothing an
adapter could read and be honest about.
It defaults to false, which means a first sign-in cannot claim an existing
account until an operator sets SSO_SAML_EMAIL_VERIFIED. That is usually true
of a corporate identity provider, and it is still not something to assume on
their behalf.
Two NameID shapes are refused
A transient NameID. It is a different value on every sign-in — that is its entire purpose — so keying an account link on one would mint a new link, and under provisioning a new account, every single time.
A NameID that is an email address. The core refuses it too, but a blank
“sign-in did not complete” tells an operator nothing, so the message here names
the fix: configure a persistent NameID, or point
SSO_SAML_SUBJECT_ATTRIBUTE at an immutable directory id.
Attribute names are configuration with fallbacks
SAML attribute naming differs per identity provider more than anything else about it does. The adapter tries the four common spellings for an address and the four for a display name, and takes an explicit name when given one.
A single group arrives as a string and several as an array — the same attribute, two shapes, depending on how many groups the person is in. Normalised here; anything else is dropped rather than guessed at, because a role-mapping handler acts on it.
Two more decisions
RelayState is where state travels. SAML has no nonce and no PKCE, so the
core-minted RelayState is the only thing tying a response to an attempt. It is
checked before anything is parsed.
validateInResponseTo is off. The core already guarantees one-time use — the
attempt row is burned before anything is exchanged. A second, in-memory,
per-process store on top would be the one that decides, since it runs first, and
it is the one a multi-instance deployment gets wrong.
Single logout answers null
Deliberately. SAML single logout is its own signed, bidirectional exchange, not a URL to redirect to — answering with one would send people somewhere that turns them away.
There is no back-channel logout on this adapter either, so
SSO_SESSION_TTL_SECONDS is the mitigation available: a shorter window after an
offboarding, at the cost of an occasional re-authentication.