Skip to main content

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):

TypeProtocolTypical useNotes
samlSAML 2.0Enterprise IdPs (ADFS, Shibboleth, Keycloak, corporate IdP)client_id is the SP entity ID; no client_secret required
oidcOpenID Connect 1.0Google, Microsoft, Okta, Auth0ID-token signature verified against the provider JWKS; issuer_url required for verification
oauth2OAuth 2.0GitHub and other plain OAuth2 APIsUser 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​

EndpointPurpose
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>/metadataSP 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 (from client_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 fieldDefault candidate attribute names
emailemail, mail, Email, http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
usernameusername, preferred_username, uid, UserName, .../claims/name
namename, displayName, cn
first_namegivenName, first_name, .../claims/givenname
last_namesn, surname, last_name, .../claims/surname

Notes:

  • required_attributes (default ["email"]) lists fields that must resolve, or the login fails with missing_attribute.
  • If no email attribute matches but the NameID contains @, the NameID is used as the email.
  • email_verified is treated as true only when the IdP explicitly attests it (an email_verified / EmailVerified attribute with value true/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
  • InResponseTo matching the request bound to RelayState
  • 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-initiated LogoutRequest (Open ACE builds and returns a signed-out LogoutResponse posted back to the IdP SLO URL) or an SP-initiated LogoutResponse.
  • GET /api/sso/slo-redirect/<name> — HTTP-Redirect binding. Handles SAMLRequest/SAMLResponse query parameters the same way. Called with no SAML parameter but a session_token cookie (or ?session_token=), it initiates SP logout: it builds a LogoutRequest from 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:

FieldRequiredNotes
nameYesUnique provider name used in all endpoint paths
provider_typeYesoidc or oauth2 (default oauth2)
client_id / client_secretYesBoth required for non-SAML providers
authorization_urlYesWhere the browser is redirected to log in
token_urlYesAuthorization-code exchange endpoint
userinfo_urlRecommendedFetches the user profile with the access token
issuer_urlOIDCID-token iss claim and JWKS discovery (<issuer_url>/.well-known/jwks.json)
redirect_uriRecommendedCallback URL; must be /api/sso/callback/<name> on your public origin
scopeRecommendedDefaults 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, and aud before trusting any claims; user info falls back to the userinfo endpoint when needed.
  • OAuth2 providers rely entirely on the userinfo endpoint.
  • The state parameter carries an HMAC-SHA256 signature (key from SSO_RELAYSTATE_SIGNING_KEY or 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 an HttpOnly cookie on the whitelisted frontend redirect.

User Provisioning and Tenant Policy​

  • Auto-provisioning of local users requires the tenant setting auto_provision_users to be enabled; otherwise the login is refused with auto_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 default SSO_NULL_TENANT_POLICY=reject, a user without a resolvable tenant is rejected (warn also 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, with account_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.com matches app.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/https redirect 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 (or idp_metadata_url for SAML) is reachable over HTTP (follows at most two redirects, SSRF-guarded)
  • token_url is reachable — for SAML, replaced by a check that an IdP signing certificate, metadata XML, or metadata URL is configured
  • client_id looks sane (length)
  • scope is a non-empty list (skipped for SAML)

At most 3 concurrent tests run; further requests get HTTP 429.

Common failures​

SymptomLikely cause and fix
invalid_saml_responseThe SAMLResponse is not valid Base64/XML, or exceeds 256 KB
Signature verification failureidp_x509_cert does not match the IdP signing certificate; rotate it, or refresh idp_metadata_url
saml_status_not_successThe IdP returned a non-Success status — check the IdP-side error
Audience / destination / recipient mismatchThe 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_attributerequired_attributes (default ["email"]) not satisfied — fix attribute_mapping or release the attribute on the IdP
Assertion time-window failureClock skew beyond 180 s between servers — sync NTP
redirect_uri_not_allowedSSO_ALLOWED_REDIRECT_DOMAINS missing or does not cover the frontend origin
auto_provision_disabled / tenant rejectionEnable auto_provision_users for the tenant or set default_tenant_id on the provider
account_disabledThe matching local account is deactivated or soft-deleted
Provider update returns 409Optimistic 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).