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_requiredfor 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_loginto the audit log with the provider, subject id, roles and groups; provider changes writesso_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:
| Protocol | What to paste into the IdP | Value |
|---|---|---|
| OIDC | Redirect URI / sign-in redirect / reply URL | https://cms.example.edu/sso/<slug>/callback |
| SAML | SP entity id (audience) | https://cms.example.edu/sso/<slug>/metadata (or the entity id you set) |
| SAML | Assertion consumer service URL (HTTP-POST) | https://cms.example.edu/sso/<slug>/acs |
| SAML | SP metadata XML | https://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)
- Okta Admin → Applications → Create App Integration → OIDC - OpenID Connect, Web Application.
- Grant type Authorization Code; Sign-in redirect URIs:
https://cms.example.edu/sso/okta/callback; assign the users or groups who may sign in. - Optional groups claim: Sign On tab → OpenID Connect ID Token → Groups claim type Filter, name
groups, filter Matches regex.*(or a prefix such ascms-). - In Spillway, add a provider:
| Field | Value |
|---|---|
| Protocol | OpenID Connect |
| Issuer URL | https://<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 / secret | From the app's General tab |
| Scopes | openid profile email groups (groups only if you added the claim) |
| Group claim | groups |
| Group → roles | e.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)
- Entra admin center → App registrations → New registration; Redirect URI (Web):
https://cms.example.edu/sso/entra/callback. - Certificates & secrets → new client secret (note the value, not the id).
- 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.
- In Spillway:
| Field | Value |
|---|---|
| Issuer URL | https://login.microsoftonline.com/<tenant-id>/v2.0 (single tenant; the Overview page shows the tenant id) |
| Client id | Application (client) ID |
| Client secret | The secret value |
| Scopes | openid profile email |
| Group claim | groups |
| Group → roles | <group object id> → roles |
| Extra authorize parameters | optional 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)
- 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. - In Spillway:
| Field | Value |
|---|---|
| Issuer URL | https://accounts.google.com |
| Client id / secret | From the OAuth client |
| Scopes | openid email profile |
| Allowed email domains | example.edu (recommended; the consent screen's Internal setting restricts the Google side) |
| Extra authorize parameters | optional 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)
- In Spillway add a SAML provider with slug
shib; the SP metadata is served athttps://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). - From the IdP metadata (
https://idp.example.edu/idp/shibboleth) copy:
| Field | Value |
|---|---|
| IdP single sign-on URL | the SingleSignOnService with Binding="…:HTTP-Redirect", typically https://idp.example.edu/idp/profile/SAML2/Redirect/SSO |
| IdP signing certificate | the <ds:X509Certificate> under <KeyDescriptor use="signing"> |
| Group claim | isMemberOf (urn:oid:1.3.6.1.4.1.5923.1.5.1.1) or eduPersonEntitlement |
- Ask the IdP operators to release
mail(urn:oid:0.9.2342.19200300.100.1.3),displayNameandeduPersonPrincipalNameto your entity id (anattribute-filter.xmlrule, or the R&S bundle). Spillway reads emails frommail/email, names fromdisplayName/cn/givenName+snand uses a persistentNameIDas the subject id, falling back toeduPersonPrincipalNamewhen 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.
- 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, attributesemail(andall_emailsif you need it). - In Spillway:
| Field | Value |
|---|---|
| Issuer URL | https://idp.int.identitysandbox.gov (sandbox) or https://secure.login.gov (production) |
| Client id | the issuer string of your Login.gov app, e.g. urn:gov:gsa:openidconnect.profiles:sp:sso:agency:cms |
| Client secret | empty |
| Scopes | openid email |
| Extra authorize parameters | acr_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-in | usually 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.
| Operation | Effect in Spillway |
|---|---|
| Create user | An 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 delete | The 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 members | Groups 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
- 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.
- Integration:
| Field | Value |
|---|---|
| SCIM connector base URL | https://cms.example.edu/scim/v2 |
| Unique identifier field for users | userName |
| Supported provisioning actions | Push New Users, Push Profile Updates, Push Groups (Import Users optional) |
| Authentication mode | HTTP Header; Authorization = the token from Spillway |
Test Connector Configuration should report Create/Update/Push Groups as supported.
- 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),displayNameandexternalId(Okta sets it to the Okta user id). Removepasswordfrom the mappings or leave it: Spillway ignores it. - Push Groups: push the groups you mapped in Spillway (for example
cms-editors,cms-admins) by name, so the pusheddisplayNamematches the mapping exactly. Members of a pushed group get the mapped roles; removing someone from the group in Okta drops them.
Microsoft Entra ID
- Entra admin center → Enterprise applications → the Spillway application → Provisioning → Get started, mode Automatic.
- Admin credentials: Tenant URL
https://cms.example.edu/scim/v2, Secret Token = the token from Spillway, Test Connection. - Mappings → Provision Microsoft Entra ID Users: keep Create, Update, Delete on. Mappings to check:
| Entra attribute | SCIM attribute |
|---|---|
userPrincipalName (or mail, when the UPN is not the address people use) | userName |
Switch([IsSoftDeleted], , "False", "True", "True", "False") | active |
mail | emails[type eq "work"].value |
givenName / surname / displayName | name.givenName / name.familyName / displayName |
objectId | externalId |
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.
- 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). - 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. SetBASE_URLin 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 atwarnlevel on the server assso login failed.
Spillway 0.1.0 · Edit this page on GitHub