- OIDC: a discovery document, PKCE (S256), a nonce, and single-use state.
- SAML 2.0: an SP-initiated
AuthnRequestover HTTP-Redirect, a Response posted to the ACS over HTTP-POST, and an XML signature verified against the configured IdP certificate.
Trust model
Org owners choose their own IdP. The framework does not question that choice. It checks that the IdP’s discovery endpoints use HTTPS, but it does not restrict which providers are acceptable. Within one Pylon deployment, the first org to claim a domain owns it. The framework blocks any org from claiming a well-known freemail domain (gmail.com, outlook.com, icloud.com, and others). This stops an org owner from intercepting domain-detection sign-ins for every Gmail user.
Operators on multi-tenant Pylon deployments should set PYLON_SSO_ALLOWED_DOMAINS as an allowlist. Pylon rejects any domain not on the list on PUT /orgs/:id/sso or /saml.
Endpoints
All endpoints are under/api/auth/.
Configuring OIDC SSO
The org’s Owner posts the IdP’s issuer URL and client credentials. Pylon fetches<issuer>/.well-known/openid-configuration and caches the four endpoints: authorization, token, userinfo, and jwks.
issuer_url, client_id, and client_secret.
Optional fields: default_role and email_domains. default_role is member or admin, and defaults to member. Pylon refuses owner as a default role, so an IdP misconfiguration cannot hand over org control. email_domains lists the domains claimed for the /sso/discover flow. Pylon lowercases each domain before it stores them.
Pylon encrypts the client_secret at rest when PYLON_SECRET is set. Without it, Pylon falls back to a plain: envelope in dev mode and prints a warning at boot in production. See Sessions for details on at-rest encryption.
Response on success:
OIDC sign-in flow
- The client redirects the user to
GET /api/auth/orgs/org_acme/sso/start?callback=<success_url>&error_callback=<error_url>. - Pylon validates both URLs against
manifest.auth.trustedOrigins(orPYLON_TRUSTED_ORIGINS). It mints a single-use state, a PKCE verifier, and a nonce, and stores them. Then it redirects (302) to the IdP’s authorization endpoint withscope=openid email profile,response_type=code, andcode_challenge_method=S256. - The IdP authenticates the user and redirects (302) back to
/api/auth/orgs/org_acme/sso/callback?code=...&state=.... - Pylon consumes the state, which is single-use. It exchanges
codefor tokens at the IdP’s token endpoint, using the PKCE verifier. It validates the id_token’snonceclaim under OIDC §3.1.2.1, then fetchesemailandnamefrom userinfo. - Pylon matches the user’s email to an existing User row, or creates a new one. It stamps
emailVerifiedto the current time, since the IdP has already verified it. - If the user is not already a member of the org, Pylon adds them with the configured
default_role. This step is idempotent. Signing in again through SSO does not downgrade an existing admin. - Pylon mints a session and writes the auth cookie. It records a
SignInaudit event withmethod=org_sso, then redirects (302) to the caller’scallbackURL.
error_callback URL with the query parameters ?sso_error=<code>&sso_error_message=<msg>. The client can use these to show the user a clear message.
PYLON_PUBLIC_URL is required so Pylon can construct the redirect URI to register with the IdP. Without it, /sso/start returns 500 REDIRECT_URI_UNAVAILABLE.
Configuring SAML
idp_entity_id, idp_sso_url (must be https://), and idp_x509_cert_pem.
Optional fields: default_role, email_domains, email_attribute, and name_attribute. email_attribute defaults to the standard emailaddress claim URI.
Errors:
SAML sign-in flow
- The client redirects to
GET /api/auth/orgs/org_acme/saml/start?callback=<success_url>&error_callback=<error_url>. - Pylon builds a SAML
AuthnRequest, then deflates and base64-encodes it for the HTTP-Redirect binding. It redirects (302) to the IdP’s SSO URL withSAMLRequest=...&RelayState=<state>. - The IdP authenticates the user and posts a
SAMLResponsetoPOST /api/auth/orgs/org_acme/saml/acs. - Pylon parses the XML and verifies the digital signature against the configured
idp_x509_cert_pem, usingxmlsec1. It validates the assertions and extracts the email and display name from the configured attributes. - Pylon then runs the same user-lookup, auto-join, and session-mint flow as OIDC.
xmlsec1 and libxml2 are required system dependencies. The Pylon Docker image includes both. For self-hosted, from-source builds, install them directly:
Email-domain discovery
A sign-in form can ask for the user’s email first and route to the right SSO automatically:404 NO_SSO_FOR_DOMAIN. The response reveals nothing about the user. It only routes by email domain.
Pylon checks OIDC first. If both protocols claim the same domain, OIDC wins.
Security guarantees
- State, PKCE, and nonce. OIDC uses all three. State is single-use and scoped to the org. A reused or wrong-org state returns
403 INVALID_SSO_STATE. - PKCE S256. This binds the token exchange to the client that started the flow. A leaked authorization code cannot be redeemed without the matching
code_verifier. - Nonce binding. Under OIDC §3.1.2.1, this stops an id_token from being replayed across different sign-in attempts.
- Redirect-URL allowlist. Pylon validates both
callbackanderror_callbackagainstmanifest.auth.trustedOrigins(orPYLON_TRUSTED_ORIGINS) before the IdP sees them. Pylon trusts loopback addresses automatically. - SAML signature verification. Pylon uses
xmlsec1to check the signature. It rejects any assertion without a valid signature from the configured certificate. - HTTPS enforced. Pylon requires
idp_sso_urlto usehttps://. - Freemail domain blocklist. Pylon blocks common consumer-mail domains from being claimed by any org: gmail, yahoo, outlook, icloud, hotmail, live, msn, aol, mail.com, protonmail and proton.me, gmx, yandex, qq, 163, 126, fastmail, mac.com, and me.com. The full list lives in
BLOCKLIST_FREEMAIL_DOMAINSincrates/auth/src/org_sso.rs. - Operator domain allowlist. Set it with
PYLON_SSO_ALLOWED_DOMAINS=acme.com,acme.io,.... - Default role can never be
owner. This stops an IdP misconfiguration from silently handing over org control through IdP-driven role promotion. client_secretencrypted at rest whenPYLON_SECRETis set.- Auto-join is idempotent. Signing in again through SSO does not change an existing member’s role. To re-apply the IdP’s default role, remove the membership first and let the next sign-in re-add it.
Configuration
Where to go next
- Organizations: the org model that SSO joins users into.
- OAuth: global OAuth (Google and GitHub for everyone) versus per-org SSO.