Skip to main content
Per-org SSO lets each organization plug in its own identity provider (Okta, Auth0, Azure AD, Google Workspace, Keycloak, OneLogin, Ping, JumpCloud, or any OIDC-compliant provider). Members sign in by going to their org’s start URL. The framework handles discovery, PKCE, nonce, state, and auto-join. Both protocols ship in the binary:
  • OIDC: a discovery document, PKCE (S256), a nonce, and single-use state.
  • SAML 2.0: an SP-initiated AuthnRequest over 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.
Required fields: 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:
Errors:

OIDC sign-in flow

  1. The client redirects the user to GET /api/auth/orgs/org_acme/sso/start?callback=<success_url>&error_callback=<error_url>.
  2. Pylon validates both URLs against manifest.auth.trustedOrigins (or PYLON_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 with scope=openid email profile, response_type=code, and code_challenge_method=S256.
  3. The IdP authenticates the user and redirects (302) back to /api/auth/orgs/org_acme/sso/callback?code=...&state=....
  4. Pylon consumes the state, which is single-use. It exchanges code for tokens at the IdP’s token endpoint, using the PKCE verifier. It validates the id_token’s nonce claim under OIDC §3.1.2.1, then fetches email and name from userinfo.
  5. Pylon matches the user’s email to an existing User row, or creates a new one. It stamps emailVerified to the current time, since the IdP has already verified it.
  6. 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.
  7. Pylon mints a session and writes the auth cookie. It records a SignIn audit event with method=org_sso, then redirects (302) to the caller’s callback URL.
On error, Pylon redirects (302) to the 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

Required fields: 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

  1. The client redirects to GET /api/auth/orgs/org_acme/saml/start?callback=<success_url>&error_callback=<error_url>.
  2. 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 with SAMLRequest=...&RelayState=<state>.
  3. The IdP authenticates the user and posts a SAMLResponse to POST /api/auth/orgs/org_acme/saml/acs.
  4. Pylon parses the XML and verifies the digital signature against the configured idp_x509_cert_pem, using xmlsec1. It validates the assertions and extracts the email and display name from the configured attributes.
  5. 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:
OIDC match:
SAML match:
If nothing matches, Pylon returns 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 callback and error_callback against manifest.auth.trustedOrigins (or PYLON_TRUSTED_ORIGINS) before the IdP sees them. Pylon trusts loopback addresses automatically.
  • SAML signature verification. Pylon uses xmlsec1 to check the signature. It rejects any assertion without a valid signature from the configured certificate.
  • HTTPS enforced. Pylon requires idp_sso_url to use https://.
  • 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_DOMAINS in crates/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_secret encrypted at rest when PYLON_SECRET is 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.