SSO Configuration
This guide explains how to configure Single Sign-On for Open ACE. It covers SAML 2.0, OIDC, and OAuth2 providers, the SAML Single Logout (SLO) endpoints, the SSO_ALLOWED_REDIRECT_DOMAINS production requirement, and how to test and troubleshoot a provider. It replaces the older SAML_CONFIG.md quick reference.
SSO Overview and Provider Types
Open ACE acts as a relying party / Service Provider (SP) and supports three provider types (provider_type):
| Type | Protocol | Typical use | Notes |
|---|---|---|---|
saml | SAML 2.0 | Enterprise IdPs (ADFS, Shibboleth, Keycloak, corporate IdP) | client_id is the SP entity ID; no client_secret required |
oidc | OpenID Connect 1.0 | Google, Microsoft, Okta, Auth0 | ID-token signature verified against the provider JWKS; issuer_url required for verification |
oauth2 | OAuth 2.0 | GitHub and other plain OAuth2 APIs | User identity is read from the userinfo_url endpoint; no ID-token validation |
Predefined providers ship with default URLs and scopes: google, microsoft (both OIDC), github (OAuth2), and okta / auth0 (OIDC, per-tenant URLs you must override at registration time). Any other provider is registered as a custom one with full URLs.
Providers are stored in the sso_providers table (one row per name, with a tenant_id for multi-tenant deployments). Secrets are stored encrypted (client_secret_encrypted, Fernet); they are never returned by the detail, list, or export endpoints.
Core Endpoints
| Endpoint | Purpose |
|---|---|
GET /api/sso/login/<provider_name> | Generates a SAML AuthnRequest (or an OAuth2/OIDC authorization URL) and redirects to the IdP |
POST /api/sso/acs/<provider_name> | Assertion Consumer Service for HTTP-POST SAMLResponse callbacks |
GET /api/sso/callback/<provider_name> | OAuth2/OIDC redirect callback (authorization code exchange) |
GET /api/sso/providers/<provider_name>/metadata | SP metadata XML for IdP configuration |
POST /api/sso/slo/<provider_name> | SAML Single Logout, HTTP-POST binding |
GET /api/sso/slo-redirect/<provider_name> | SAML Single Logout, HTTP-Redirect binding |
Management endpoints (admin only unless noted): GET /api/sso/providers (public; lists registered and predefined providers), GET/POST /api/sso/providers, PUT /api/sso/providers/<name> (optimistic locking on updated_at), PATCH .../enable and PATCH .../disable, DELETE /api/sso/providers/<name> (deprecated — disables instead of deleting), POST .../reset (restores a predefined provider's default URLs, keeping client_id/client_secret), POST .../test, and GET /api/sso/providers/export (secrets excluded).
SAML Configuration in Five Steps
Step 1: Register the provider
Register a SAML provider through POST /api/sso/providers:
{
"name": "corp-saml",
"provider_type": "saml",
"client_id": "https://openace.example.com/saml/metadata",
"authorization_url": "https://idp.example.com/sso",
"redirect_uri": "https://openace.example.com/api/sso/acs/corp-saml",
"issuer_url": "https://idp.example.com/metadata",
"extra_params": {
"idp_x509_cert": "MIIC...",
"idp_entity_id": "https://idp.example.com/metadata",
"attribute_mapping": {
"email": "email",
"username": "uid",
"name": "displayName"
}
}
}
client_id is the SP entity ID. SAML providers do not require client_secret.
IdP configuration can be supplied directly with authorization_url, issuer_url,
and idp_x509_cert, or via extra_params.idp_metadata_xml /
extra_params.idp_metadata_url.
Useful extra_params keys understood by the SAML provider: sp_entity_id, acs_url, idp_entity_id, idp_sso_url, idp_slo_url, nameid_format (default urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress), attribute_mapping, required_attributes, allow_email_linking, default_tenant_id.
Step 2: Import the SP metadata into the IdP
Download the SP metadata and import it into your IdP:
curl -o sp-metadata.xml https://openace.example.com/api/sso/providers/corp-saml/metadata
The metadata declares:
entityID— the SP entity ID (fromclient_id/extra_params.sp_entity_id)AssertionConsumerService, HTTP-POST binding, at/api/sso/acs/<name>SingleLogoutService, HTTP-POST binding at/api/sso/slo/<name>, and HTTP-Redirect binding at/api/sso/slo-redirect/<name>AuthnRequestsSigned="false",WantAssertionsSigned="true"(assertions or responses must be signed by the IdP)
If your IdP is configured by hand instead of via metadata, register exactly those ACS and SLO URLs.
The ACS URL is derived from the incoming request host by default. Behind a reverse proxy, set the sso.canonical_base_url configuration value (e.g. https://openace.example.com) so the ACS URL in the metadata, the AuthnRequest, and the validation comparison are pinned to one public scheme+host and cannot drift with spoofed Host headers. This setting affects only the SAML ACS URL; the OAuth2/OIDC callback URL stays request-derived because providers register it out of band.
Step 3: Map attributes
SAML attributes are read from the AttributeStatement. Open ACE maps five user fields via extra_params.attribute_mapping; each value may be a single attribute name or a list of names tried in order:
| Open ACE field | Default candidate attribute names |
|---|---|
email | email, mail, Email, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress |
username | username, preferred_username, uid, UserName, .../claims/name |
name | name, displayName, cn |
first_name | givenName, first_name, .../claims/givenname |
last_name | sn, surname, last_name, .../claims/surname |
Notes:
required_attributes(default["email"]) lists fields that must resolve, or the login fails withmissing_attribute.- If no email attribute matches but the NameID contains
@, the NameID is used as the email. email_verifiedis treated as true only when the IdP explicitly attests it (anemail_verified/EmailVerifiedattribute with valuetrue/1/yes).- The NameID is always stored as the provider-side user identifier (
provider_user_id).
Step 4: Know what the ACS validates
The ACS handler validates:
- XML Signature with the configured IdP certificate
- SAML success status
- IdP issuer
- audience restriction matching the SP entity ID
- response destination and subject recipient matching the ACS URL
InResponseTomatching the request bound toRelayState- assertion time windows with a small clock skew (180 seconds by default)
- required attributes, including email by default
Requests/responses larger than 256 KB are rejected with HTTP 413 before parsing.
Step 5: Configure Single Logout (SLO)
Two endpoints back SAML Single Logout:
POST /api/sso/slo/<name>— HTTP-POST binding. Accepts either an IdP-initiatedLogoutRequest(Open ACE builds and returns a signed-outLogoutResponseposted back to the IdP SLO URL) or an SP-initiatedLogoutResponse.GET /api/sso/slo-redirect/<name>— HTTP-Redirect binding. HandlesSAMLRequest/SAMLResponsequery parameters the same way. Called with no SAML parameter but asession_tokencookie (or?session_token=), it initiates SP logout: it builds aLogoutRequestfrom the NameID/SessionIndex stored at login and redirects to the IdP. If the IdP has no SLO URL configured (extra_params.idp_slo_url, or discovered from metadata), the local session is simply deleted.
The plain DELETE /api/sso/session logout always cascades: the row in sso_sessions and the matching local session in sessions are deleted in one transaction, and the session_token cookie is cleared.
Binding boundaries
Open ACE currently supports Redirect binding for AuthnRequest and HTTP-POST binding for ACS. Signed assertions or signed responses are accepted when the signature matches the configured IdP certificate. Local user provisioning/linking follows the same SSO identity table used by OAuth2/OIDC.
OIDC / OAuth2 Provider Configuration
Register an OIDC or OAuth2 provider with POST /api/sso/providers:
{
"name": "corp-oidc",
"provider_type": "oidc",
"client_id": "my-client-id",
"client_secret": "my-client-secret",
"authorization_url": "https://sso.example.com/oauth2/v1/authorize",
"token_url": "https://sso.example.com/oauth2/v1/token",
"userinfo_url": "https://sso.example.com/oauth2/v1/userinfo",
"issuer_url": "https://sso.example.com",
"redirect_uri": "https://openace.example.com/api/sso/callback/corp-oidc",
"scope": ["openid", "profile", "email"]
}
Field reference:
| Field | Required | Notes |
|---|---|---|
name | Yes | Unique provider name used in all endpoint paths |
provider_type | Yes | oidc or oauth2 (default oauth2) |
client_id / client_secret | Yes | Both required for non-SAML providers |
authorization_url | Yes | Where the browser is redirected to log in |
token_url | Yes | Authorization-code exchange endpoint |
userinfo_url | Recommended | Fetches the user profile with the access token |
issuer_url | OIDC | ID-token iss claim and JWKS discovery (<issuer_url>/.well-known/jwks.json) |
redirect_uri | Recommended | Callback URL; must be /api/sso/callback/<name> on your public origin |
scope | Recommended | Defaults to ["openid", "profile", "email"] for OIDC; GitHub uses ["user:email", "read:user"] |
For a predefined provider, POST with "predefined": true and just the credentials — default URLs are applied automatically. Okta and Auth0 have no default URLs (they depend on your tenant domain), so pass authorization_url, token_url, userinfo_url, and issuer_url explicitly.
Behavior after the callback:
- OIDC providers verify the ID token signature (JWKS, cached),
iss, andaudbefore trusting any claims; user info falls back to the userinfo endpoint when needed. - OAuth2 providers rely entirely on the userinfo endpoint.
- The
stateparameter carries an HMAC-SHA256 signature (key fromSSO_RELAYSTATE_SIGNING_KEYor the instance encryption key). Legacy unsigned states are accepted only during a transition period ending 2027-01-31. - The authenticated user is looked up in
sso_identities; new users are provisioned when tenant policy allows, and the resulting session token is set as anHttpOnlycookie on the whitelisted frontend redirect.
User Provisioning and Tenant Policy
- Auto-provisioning of local users requires the tenant setting
auto_provision_usersto be enabled; otherwise the login is refused withauto_provision_disabled(HTTP 403). - The tenant for a new SSO user is resolved in order: provider
extra_params.default_tenant_id→ the registering admin's tenant → policy check. With the defaultSSO_NULL_TENANT_POLICY=reject, a user without a resolvable tenant is rejected (warnalso rejects today and is deprecated). - Email-based linking to an existing local account only happens when the provider sets
extra_params.allow_email_linking=true(default off). A deactivated or soft-deleted account is refused before any identity binding, withaccount_disabled.
SSO_ALLOWED_REDIRECT_DOMAINS (required in production)
SSO_ALLOWED_REDIRECT_DOMAINS is a comma-separated whitelist of frontend domains that may receive the post-login redirect (and the session_token cookie):
export SSO_ALLOWED_REDIRECT_DOMAINS="openace.example.com,console.example.com"
- Without it, only
localhost/127.0.0.1/[::1]redirects are allowed — fine for development, broken for production. - A configured domain matches itself and all subdomains (
example.commatchesapp.example.com). - If a login succeeds but the redirect target is not whitelisted, the flow returns
redirect_uri_not_allowed(HTTP 400), and the freshly created session rows are deleted — the user cannot log in. This is the first thing to check when "login succeeds but the browser ends up on an error page". - Only
http/httpsredirect URIs are accepted.
Testing and Troubleshooting
Test a provider
POST /api/sso/providers/<name>/test (admin) performs basic checks and reports each one:
authorization_url(oridp_metadata_urlfor SAML) is reachable over HTTP (follows at most two redirects, SSRF-guarded)token_urlis reachable — for SAML, replaced by a check that an IdP signing certificate, metadata XML, or metadata URL is configuredclient_idlooks sane (length)scopeis a non-empty list (skipped for SAML)
At most 3 concurrent tests run; further requests get HTTP 429.
Common failures
| Symptom | Likely cause and fix |
|---|---|
invalid_saml_response | The SAMLResponse is not valid Base64/XML, or exceeds 256 KB |
| Signature verification failure | idp_x509_cert does not match the IdP signing certificate; rotate it, or refresh idp_metadata_url |
saml_status_not_success | The IdP returned a non-Success status — check the IdP-side error |
| Audience / destination / recipient mismatch | The ACS URL or SP entity ID differs between what the IdP was given and what Open ACE uses; re-import SP metadata, and set sso.canonical_base_url behind a proxy |
missing_attribute | required_attributes (default ["email"]) not satisfied — fix attribute_mapping or release the attribute on the IdP |
| Assertion time-window failure | Clock skew beyond 180 s between servers — sync NTP |
redirect_uri_not_allowed | SSO_ALLOWED_REDIRECT_DOMAINS missing or does not cover the frontend origin |
auto_provision_disabled / tenant rejection | Enable auto_provision_users for the tenant or set default_tenant_id on the provider |
account_disabled | The matching local account is deactivated or soft-deleted |
| Provider update returns 409 | Optimistic lock: another admin edited the provider; reload and retry with the fresh updated_at |
Frontend redirects carry the failure as ?sso_error=<code> (for example auth_failed, invalid_request, account_disabled), so the exact code is visible in the browser URL and the audit log (resource_type: sso_session).