An identity provider Apograph does not ship an adapter for is one factory function
returning an SsoProvider, plus a run of the conformance kit.
Check first whether you need one. The OIDC adapter reaches every provider that speaks OpenID Connect, which is most of the market — a vendor there is a preset, not a package. A new package is for a wire that genuinely differs, as GitHub’s and SAML’s do.
Where the port lives
@apograph/identity-domain, which imports nothing. No NestJS, no Drizzle,
no class-validator, no jose — its manifest has no dependencies key at all,
and that is deliberate rather than an oversight.
That is what lets an adapter depend on the port without dragging in the server plugin and everything pinned to it. If a change there needs a dependency, the change belongs in an adapter or in the plugin.
sso-provider.ts # the port: descriptor / authorize / complete / logoutUrl
sso-profile.ts # the normalised profile, and its assertions
redirect-target.ts # the open-redirect guard
conformance.ts # the kit every adapter runsThe three clauses
The port states three obligations the core has no way to verify for itself. The conformance kit is what turns them from prose into tests.
1. complete verifies, or it throws. From the outside a decoded profile and
a verified one are the same object, so an adapter that skips verification
produces a sign-in that looks entirely normal.
2. subject is stable, issuer-scoped, and never the email. The link table
keys on it. The profile assertion refuses the specific mistake — a subject equal
to the email — because a person’s address follows a mailbox, not a person.
3. emailVerified reports what the provider claimed. It is the only gate on
claiming an account that already exists, so an adapter that reports true
because it seems likely has removed the gate.
What the core mints, so you do not
state, the nonce and the PKCE verifier all arrive in the authorize request
and come back in the callback. Adapters generate none of them.
CSRF and replay defence is one rule, and implementing it once where it can be tested once beats implementing it per adapter with a fresh chance to differ.
Never put the code verifier in the URL
Only its S256 challenge belongs there. An adapter that passes the verifier
itself hands the browser — and every log between there and the provider — the
one value PKCE exists to withhold.
The conformance kit checks this directly.
The descriptor
kind and callbackMethod were in the port from the first release, before
there was a second protocol to need them.
Declare callbackMethod: 'POST' and the plugin mounts the form-post callback —
which is what made adding SAML a package rather
than a change to the seam.
Optional: back-channel logout
Implement verifyLogoutToken and the plugin’s back-channel logout route works
for your provider. Do not implement it if you cannot verify — the route then
answers 404 rather than pretending to have acted, which is right, because the
endpoint is unauthenticated and reachable by anyone.
An unverified notification would be an open way to sign arbitrary people out.
The conformance kit
Run it in your own suite, against a stub that signs real tokens with a generated key pair where the protocol has signatures. That is what makes the tampering case mean something: the tampered scenario is a token signed by a key the provider does not publish, and it fails because the signature is checked — not because a flag says so.
The scripted provider is the worked example, and short enough to read in one sitting.
Registering it
IdentityPlugin(config.plugins.identity, {
sso: {
providers: [{ name: 'mine', provider: createMyProvider({ /* … */ }) }]
}
});The name appears in the route and in every identity-link row, so it is permanent — renaming a registration orphans the links naming it. See single sign-on for the handshake settings and what a sign-in is allowed to do.