Spillway CMSStructured content like Drupal. Page building like Puck. Setup like Ghost. AI that drafts, people who approve.
Menu

Enterprise sign-in (SSO)

Spillway signs staff in through your identity provider with OpenID Connect or SAML 2.0. Providers are configured in the admin under Site settings → Sign-in (needs the users.manage permission, i.e. the admin role); each enabled provider becomes a Continue with … button above the password form on /admin/login.

What you get:

  • Just-in-time accounts. With Create an account on first sign-in on, a person who authenticates at the IdP and has no Spillway account gets one, with the provider's default roles. Switch it off to admit only people who already exist (matched by email).
  • Group-to-role mapping. Name the claim or attribute that lists groups (groups, isMemberOf, …) and map each group to Spillway roles. When a group claim is configured the mapping replaces the user's roles on every sign-in (falling back to the default roles when nothing matches); when it is empty, existing users keep whatever roles an admin gave them.
  • Domain allow-list. Restrict a provider to given email domains.
  • No password for SSO accounts. An account the IdP created has no password; the password form answers sso_required for it. An admin can set one in Users (the usual password field), after which both ways work. A local account that signs in through SSO is linked to the provider and keeps its password.
  • Audit. Every sign-in writes auth.sso_login to the audit log with the provider, subject id, roles and groups; provider changes write sso_provider.create|update|delete.
  • Secrets at rest. OIDC client secrets are AES-256-GCM encrypted with a key generated into <DATA_DIR>/sso-secret (mode 0600). Back it up with the data directory; without it the stored secrets cannot be read.

Security checks the server performs, so you do not have to: OIDC uses the authorization-code flow with PKCE (S256), a random state and nonce kept in a short-lived signed httpOnly cookie, and verifies the id_token against the provider's JWKS with the configured issuer, the client id as audience and 5 minutes of clock skew. SAML requires a valid XML signature on the Response or the Assertion (with the pasted certificate), checks InResponseTo against the request Spillway issued (each id is single-use), the audience (the SP entity id), NotBefore/NotOnOrAfter with 5 minutes of skew, and refuses unsigned or replayed responses. Emails are required and lowercased. The return path after sign-in may only be a path on this site.

Before you start

Set BASE_URL on the server (for example https://cms.example.edu) so the URLs below match the public address; the admin shows them on each provider:

ProtocolWhat to paste into the IdPValue
OIDCRedirect URI / sign-in redirect / reply URLhttps://cms.example.edu/sso/<slug>/callback
SAMLSP entity id (audience)https://cms.example.edu/sso/<slug>/metadata (or the entity id you set)
SAMLAssertion consumer service URL (HTTP-POST)https://cms.example.edu/sso/<slug>/acs
SAMLSP metadata XMLhttps://cms.example.edu/sso/<slug>/metadata

<slug> is the short name you give the provider (okta, entra, shib, …). The Test button fetches the OIDC discovery document and JWKS, or parses the SAML certificate, and reports what it found.

Okta (OIDC)

  1. Okta Admin → Applications → Create App Integration → OIDC - OpenID Connect, Web Application.
  2. Grant type Authorization Code; Sign-in redirect URIs: https://cms.example.edu/sso/okta/callback; assign the users or groups who may sign in.
  3. Optional groups claim: Sign On tab → OpenID Connect ID Token → Groups claim type Filter, name groups, filter Matches regex .* (or a prefix such as cms-).
  4. In Spillway, add a provider:
FieldValue
ProtocolOpenID Connect
Issuer URLhttps://<your-org>.okta.com (or https://<your-org>.okta.com/oauth2/default when you use a custom authorization server; the issuer shown on the server's settings page)
Client id / secretFrom the app's General tab
Scopesopenid profile email groups (groups only if you added the claim)
Group claimgroups
Group → rolese.g. cms-editors → editor, cms-admins → admin

Okta also works over SAML: create a SAML 2.0 app with the ACS URL and entity id from the table, download the IdP metadata and copy the SingleSignOnService (HTTP-Redirect) URL and the X509Certificate into a SAML provider; add a Group Attribute Statement named groups to map roles.

Microsoft Entra ID (OIDC)

  1. Entra admin center → App registrations → New registration; Redirect URI (Web): https://cms.example.edu/sso/entra/callback.
  2. Certificates & secrets → new client secret (note the value, not the id).
  3. Token configuration → Add groups claim → Security groups (ID tokens). Entra sends group object ids, not names; map those ids. If a user is in more than ~200 groups Entra omits the claim and you must filter the groups assigned to the application.
  4. In Spillway:
FieldValue
Issuer URLhttps://login.microsoftonline.com/<tenant-id>/v2.0 (single tenant; the Overview page shows the tenant id)
Client idApplication (client) ID
Client secretThe secret value
Scopesopenid profile email
Group claimgroups
Group → roles<group object id> → roles
Extra authorize parametersoptional prompt=select_account or domain_hint=example.edu

Entra ID also works over SAML: Enterprise applications → New application → Create your own → Integrate any other application, Single sign-on → SAML, Identifier = the SP entity id, Reply URL = the ACS URL; download Certificate (Base64) and use the Login URL as the single sign-on URL. The group attribute is http://schemas.microsoft.com/ws/2008/06/identity/claims/groups; the email attribute http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress is read automatically.

Google Workspace (OIDC)

  1. Google Cloud console → APIs & Services → Credentials → Create credentials → OAuth client ID, type Web application; Authorized redirect URI: https://cms.example.edu/sso/google/callback. Configure the OAuth consent screen as Internal so only your Workspace domain can sign in.
  2. In Spillway:
FieldValue
Issuer URLhttps://accounts.google.com
Client id / secretFrom the OAuth client
Scopesopenid email profile
Allowed email domainsexample.edu (recommended; the consent screen's Internal setting restricts the Google side)
Extra authorize parametersoptional hd=example.edu to pre-select the Workspace account

Google's id_token carries no groups, so leave the group claim empty: new accounts get the default roles and existing users keep theirs.

Shibboleth / InCommon (SAML)

  1. In Spillway add a SAML provider with slug shib; the SP metadata is served at https://cms.example.edu/sso/shib/metadata. Register it with your campus IdP or submit it to the InCommon Federation Manager (set SP entity id first if your institution requires a specific entity id, e.g. https://cms.example.edu/shibboleth).
  2. From the IdP metadata (https://idp.example.edu/idp/shibboleth) copy:
FieldValue
IdP single sign-on URLthe SingleSignOnService with Binding="…:HTTP-Redirect", typically https://idp.example.edu/idp/profile/SAML2/Redirect/SSO
IdP signing certificatethe <ds:X509Certificate> under <KeyDescriptor use="signing">
Group claimisMemberOf (urn:oid:1.3.6.1.4.1.5923.1.5.1.1) or eduPersonEntitlement
  1. Ask the IdP operators to release mail (urn:oid:0.9.2342.19200300.100.1.3), displayName and eduPersonPrincipalName to your entity id (an attribute-filter.xml rule, or the R&S bundle). Spillway reads emails from mail/email, names from displayName/cn/givenName+sn and uses a persistent NameID as the subject id, falling back to eduPersonPrincipalName when the NameID is transient. Attributes are matched by their friendly name or OID as the IdP sends them.

Login.gov (OIDC)

Login.gov supports only private_key_jwt or PKCE for the token exchange; Spillway uses PKCE, so register a public client and leave the client secret empty.

  1. In the Login.gov Partner Dashboard create an app: OpenID Connect PKCE, Return to app URL and Redirect URI https://cms.example.edu/sso/logingov/callback, attributes email (and all_emails if you need it).
  2. In Spillway:
FieldValue
Issuer URLhttps://idp.int.identitysandbox.gov (sandbox) or https://secure.login.gov (production)
Client idthe issuer string of your Login.gov app, e.g. urn:gov:gsa:openidconnect.profiles:sp:sso:agency:cms
Client secretempty
Scopesopenid email
Extra authorize parametersacr_values=urn:acr.login.gov:auth-only (or http://idmanagement.gov/ns/assurance/ial/1); Login.gov requires it
Create an account on first sign-inusually off: Login.gov vouches for an email, not for a role

Login.gov sends the email through the userinfo endpoint; Spillway fetches it when the id_token has none. There are no groups: manage roles in Users.

GitHub (OAuth)

A protocol: "github" provider signs people in with a GitHub OAuth app (authorization code, read:user user:email, identity = the GitHub user id, email = the primary verified address). It exists for the registry site, where publishers sign in with GitHub; the setup is in registry.md.

User provisioning (SCIM 2.0)

Besides signing people in, an identity provider can provision them: create accounts before the first sign-in, keep names and addresses current, push groups, and deactivate leavers the moment they are removed in the IdP. Spillway speaks SCIM 2.0 (RFC 7643, RFC 7644) at https://cms.example.edu/scim/v2, with one bearer token per provider.

Setting it up. Open the provider under Site settings → Sign-in → Edit and scroll to Provisioning (SCIM 2.0): copy the base URL, click Create token and paste the token into the IdP. The token is shown once; Rotate token replaces it (the old one stops working at once) and Revoke stops provisioning. The same panel shows the last sync and the last 20 SCIM events; every change is also in the audit log with the actor scim:<slug>.

What the IdP can do.

OperationEffect in Spillway
Create userAn account with the provider's default roles, no password (only the provider can sign it in), externalId stored. userName must be the email address (it is what the person signs in with); name/displayName become the display name.
Update user (PUT or PATCH)Changes the address, the name or the externalId. Attributes Spillway does not store (title, phoneNumbers, the enterprise extension, …) are accepted and ignored.
Deactivate (active: false) or deleteThe account is deactivated, never deleted (content keeps its author): every session ends, every API token of the user is revoked, and password or SSO sign-in answers This account has been deactivated. active: true restores it.
Push group / add membersGroups live per provider. A group whose name appears in the provider's Group → roles mapping grants those roles to its members; a person in several mapped groups holds the union; a person in no mapped group holds the default roles. Membership removal, a group rename or a group deletion recomputes the roles at once.
Look up (filter)userName eq, emails.value eq, externalId eq, id eq for users; displayName eq, externalId eq for groups. Paging with startIndex and count.

Existing accounts: a provider's token sees the accounts it owns plus local password accounts, so an IdP that finds an existing staff member by email adopts the account (the password keeps working, the IdP now governs active and the roles). Accounts owned by another provider are invisible to it. Not offered: bulk operations, sorting, /Me, password sync (password is ignored), ETags.

Limits: 1 MB per request, 600 requests per minute per token (429 with Retry-After), application/scim+json or application/json bodies (anything else is 415). Reads are not audited.

Okta

  1. Okta Admin → the Spillway app integration (OIDC or SAML, created above) → General → Edit → Provisioning: SCIM (for an OIDC app you may need to enable SCIM provisioning under App Settings). Save, then open the Provisioning tab.
  2. Integration:
FieldValue
SCIM connector base URLhttps://cms.example.edu/scim/v2
Unique identifier field for usersuserName
Supported provisioning actionsPush New Users, Push Profile Updates, Push Groups (Import Users optional)
Authentication modeHTTP Header; Authorization = the token from Spillway

Test Connector Configuration should report Create/Update/Push Groups as supported.

  1. To App: enable Create Users, Update User Attributes, Deactivate Users. Keep the default attribute mappings; the ones that matter are userName (Okta username, which must be the email), givenName, familyName, email (primary), displayName and externalId (Okta sets it to the Okta user id). Remove password from the mappings or leave it: Spillway ignores it.
  2. Push Groups: push the groups you mapped in Spillway (for example cms-editors, cms-admins) by name, so the pushed displayName matches the mapping exactly. Members of a pushed group get the mapped roles; removing someone from the group in Okta drops them.

Microsoft Entra ID

  1. Entra admin center → Enterprise applications → the Spillway application → Provisioning → Get started, mode Automatic.
  2. Admin credentials: Tenant URL https://cms.example.edu/scim/v2, Secret Token = the token from Spillway, Test Connection.
  3. Mappings → Provision Microsoft Entra ID Users: keep Create, Update, Delete on. Mappings to check:
Entra attributeSCIM attribute
userPrincipalName (or mail, when the UPN is not the address people use)userName
Switch([IsSoftDeleted], , "False", "True", "True", "False")active
mailemails[type eq "work"].value
givenName / surname / displayNamename.givenName / name.familyName / displayName
objectIdexternalId

Remove the mappings Spillway does not store if you want a tidier log (jobTitle, department, phoneNumbers, preferredLanguage, …); they are harmless either way. Entra's "True"/"False" strings and capitalized operations (Replace) are understood.

  1. Mappings → Provision Microsoft Entra ID Groups: on, with displayName → displayName, objectId → externalId, members → members. Name the groups exactly as in the Group → roles mapping (Entra pushes group names over SCIM, unlike the group ids it puts in id_tokens).
  2. Settings → Scope: Sync only assigned users and groups; assign the users and the mapped groups under Users and groups. Turn Provisioning Status on. The initial cycle runs within minutes, later cycles every 40 minutes; Provision on demand pushes one person at once.

Operating notes

  • Disable, then remove. Disabling a provider removes its button at once; accounts it created stay. Removing it keeps the accounts too; those without a password need one set by an admin.
  • Several providers can coexist (for example SAML for staff and Google for contractors). A user is matched first by (provider, subject id), then by email; an email already owned by another provider's account still signs in but keeps its original link.
  • Reverse proxies. The redirect and ACS URLs are built from BASE_URL; when it is unset, from the request origin as the proxy forwarded it. Set BASE_URL in production.
  • Clock skew. Both protocols allow 5 minutes. Keep the server on NTP.
  • Rate limiting. Callbacks and ACS posts share the password form's per-IP limiter (10 failures per 15 minutes); the start URL is not limited.
  • Troubleshooting. A failed sign-in lands on /admin/login?error=<code> with a plain-language banner; the reason (the IdP's error text, the signature failure, the missing claim) is logged at warn level on the server as sso login failed.

Spillway 0.1.0 · Edit this page on GitHub

Was this page helpful?