Skip to content

Single Sign-On (SAML) ​

The SIS can act as a SAML service provider against one or more identity providers. Users pick a provider on the login page; accounts are created on first successful login.

Administration → SSO Identity Providers (/admin/sso-providers), gated on ADMIN.SSO_IDP (migration 093).

Server configuration ​

VariableMeaningDefault
SAML_SP_BASE_URLThe public base for this instance's SP URLshttp://localhost:5173
SAML_SP_CERT_PATHSP certificate./saml/sp.crt
SAML_SP_KEY_PATHSP private key./saml/sp.key
SAML_METADATA_MAX_AGEHow long fetched IdP metadata is trusted24h

SAML_SP_BASE_URL must keep the path prefix

The ACS URL is built as <base>/api/v1/auth/saml/{slug}/acs, and the identity provider posts to it directly — so it must be reachable exactly as written.

Each instance therefore keeps its prefix:

InstanceSAML_SP_BASE_URL
/sdasishttps://sdasis.puc.edu/sdasis
/demohttps://sdasis.puc.edu/demo
/recordshttps://sdasis.puc.edu/records

This is the opposite of PUBLIC_API_BASE_PATH's neighbours in the same file — do not "simplify" it to the bare hostname. The backend warns at startup if the value is not a public HTTPS URL, because the SP metadata would then be unusable by an IdP.

Each instance also has its own saml volume holding its keypair, so the three instances are separate service providers rather than one shared identity.

Adding an identity provider ​

Providers live in core.sso_identity_providers:

FieldNotes
display_nameShown on the login page
slugAppears in the URLs below. Stable; changing it changes the ACS URL
metadata_urlFetched and refreshed
metadata_xmlPasted metadata, as an alternative to a URL
metadata_fetched_atWhen the URL was last read
is_active, sort_order

Two are configured here: puc (active) and puc-forge (inactive).

Per-provider URLs ​

URLWho calls it
GET /auth/saml/{slug}/metadataThe IdP, to configure its side
GET /auth/saml/{slug}/loginThe browser, to begin a login
POST /auth/saml/{slug}/acsThe IdP, posting the assertion

The metadata endpoint is public — SP metadata has to be fetchable by the identity provider itself. So is GET /auth/sso/providers, which the login page reads before any session exists; it returns only display name and slug.

POST .../refresh-metadata re-fetches a provider's metadata on demand rather than waiting for SAML_METADATA_MAX_AGE to expire.

Steps ​

  1. Set SAML_SP_BASE_URL for the instance, and make sure its saml volume holds a keypair.
  2. Create the provider with its metadata URL.
  3. Give the identity provider your SP metadata URL: https://<host><prefix>/api/v1/auth/saml/<slug>/metadata.
  4. Confirm the IdP's ACS destination matches https://<host><prefix>/api/v1/auth/saml/<slug>/acs.
  5. Activate the provider; it appears on the login page.
  6. Test with a real account.

Relay state is held in Valkey for the duration of the round trip, so the login flow depends on Valkey being available.

What happens on login ​

  1. The assertion is validated against the provider's metadata.
  2. The user is matched, or JIT-provisioned — an account is created in core.auth_users on first successful login.
  3. DeriveRoles runs on every login, so role membership implied by the person's record (student, teacher, advisor) is recomputed rather than being a one-time assignment.

Derived roles are recomputed, not accumulated

Because derivation runs per login, changing someone's underlying record changes their roles at their next sign-in without an administrator touching anything. A duplicated derivation rule once caused 87,000 rows of nightly churn, so if derived-role counts move unexpectedly, look for two rules describing the same condition rather than for a data problem.

The production tier's write gate ​

The /records instance runs with WRITE_MODE=restricted.

A restricted instance refuses writes before the permission check

WRITE_MODE=restricted rejects a write earlier in the request than the permission guard, so a SAML-provisioned administrator with every grant can still be refused. That looked like a permissions bug and was not.

The fix is an allowlist of the paths that must remain writable — SSO administration among them — rather than a broader grant.

Demo mode ​

With DEMO_MODE=true, SSO is deliberately disabled: GET /auth/sso/providers returns the disabled response, and no SAML flow is registered. The demo tier publishes its own local logins instead.

Student Information System Documentation