Features Apograph CMS on GitHub

SAML 2.0

Identity plugin @apograph/identity-server@apograph/identity-admin

The protocol whose response is a form post, and what that changes.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

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-saml
import { createSamlProvider } from '@apograph/identity-provider-saml';

IdentityPlugin(config.plugins.identity, {
    sso: {
        providers: [{ name: 'saml', provider: createSamlProvider(settings) }]
    }
});

Configuration

VariableWhat it does
SSO_SAML_ENTRY_POINTThe identity provider’s sign-in URL
SSO_SAML_IDP_CERTIts signing certificate
SSO_SAML_ISSUERThis service provider’s entity id. Defaults to the public base URL
SSO_SAML_SUBJECT_ATTRIBUTEAn immutable directory id, when the NameID will not do
SSO_SAML_EMAIL_ATTRIBUTEThe attribute carrying the address
SSO_SAML_GROUPS_ATTRIBUTEThe attribute carrying group membership
SSO_SAML_EMAIL_VERIFIEDAssert this directory owns the addresses it reports
SSO_SAML_NAME / SSO_SAML_LABELRegistration 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.