SSO
Enterprise single sign-on with OIDC and SAML identity providers.
The SSO plugin registers enterprise identity providers, verifies their email domains, and runs OIDC or SAML sign-in flows. Users whose email matches a verified domain are routed into the right provider automatically, and accounts (or organizations) are provisioned according to your policy.
Installation
Add the plugin to your server
Install the standalone package, then pass sso() to your Kernia config.
from kernia import KerniaOptions
from kernia.auth import init
from kernia_sso import sso
auth = init(KerniaOptions(
database=adapter,
secret=os.environ["KERNIA_SECRET"],
base_url=os.environ["KERNIA_BASE_URL"],
base_path="/api/auth",
plugins=[sso()],
advanced={"sso": {"saml_validation": "strict"}},
))Add the client plugin
Add the sso client plugin:
import { createAuthClient } from "better-auth/client";
import { ssoClient } from "@better-auth/sso/client";
export const authClient = createAuthClient({
baseURL: "/api/auth",
plugins: [ssoClient()],
});Usage
Register a provider
// OIDC
await authClient.sso.register({
providerId: "acme-oidc",
issuer: "https://login.acme.com",
domain: "acme.com",
oidcConfig: {
clientId: process.env.ACME_CLIENT_ID,
clientSecret: process.env.ACME_CLIENT_SECRET,
scopes: ["openid", "email", "profile"],
},
});
// SAML
await authClient.sso.register({
providerId: "acme-saml",
issuer: "https://idp.acme.com",
domain: "acme.com",
samlConfig: {
entryPoint: "https://idp.acme.com/sso",
cert: process.env.ACME_SAML_CERT,
callbackUrl: "https://app.example.com/api/auth/sso/saml/acs/acme-saml",
},
});Protect provider registration behind your own admin gate — most OIDC endpoints auto-discover from the issuer's OpenID configuration.
Verify a domain
await authClient.sso.requestDomainVerification({ providerId: "acme-oidc" });
await authClient.sso.verifyDomain({ providerId: "acme-oidc" });Domain verification stops arbitrary tenants from claiming a domain they don't control.
Sign in with SSO
await authClient.signIn.sso({
domain: "acme.com", // or providerId / organizationSlug
callbackURL: "/dashboard",
});Once a domain is verified, an email/password attempt for that domain is also
redirected into the SSO flow automatically (controlled by enforce_email_domain).
Options
Configure under advanced["sso"]:
| Option | Type | Default | Description |
|---|---|---|---|
saml_validation | "strict" | "permissive" | "strict" | SAML assertion validation mode (see below). |
enforce_email_domain | bool | True | Redirect /sign-in/email into SSO when the email's domain is verified. |
is_admin | (user) -> bool | role check | Decides who may manage providers. |
disable_admin_check | bool | False | Skip the admin gate on provider CRUD (testing only). |
SAML validation modes
"strict"(default) — runspython3-saml's full XML-DSIG verifier against the IdP's certificate. This is the production-correct setting and works out-of-the-box against the bundledMockSAMLIdPtest fixture (which canonicalizes vialxmlexclusive C14N, so its signature reconstructs byte-for-byte)."permissive"— verifies issuer, audience,NotBefore/NotOnOrAfter,InResponseTo,Status, the Reference URI, and that the embeddedX509Certificatematches the provider's configured PEM, but does not verify the XML-DSIG signature value. Use only for legacy IdPs whose canonicalization isn't libxml2-compatible.
Schema
The plugin adds two tables:
idissuerkindnamedomainsoidcConfigsamlConfiguserInfoMappingorganizationIduserIdcreatedAtupdatedAtiddomainssoProviderIdverifiedverificationTokencreatedAtKeep saml_validation on "strict" in production — "permissive" skips the
cryptographic signature check and should only be used for IdPs whose XML
canonicalization is incompatible with libxml2.