SAML SSO into your apps (Obexal as IdP)
Obexal issues signed SAML 2.0 assertions to your downstream applications, with a per-tenant signing identity, a strict single-ACS policy and verified-email guarantees.
Many SaaS products (and most legacy enterprise apps) only speak SAML 2.0 for single sign-on. Obexal acts as the SAML Identity Provider for your organization: your users authenticate once against Obexal, and Obexal issues a signed assertion to each registered application (the Service Provider, SP). This is the outbound counterpart of inbound enterprise SSO, where Obexal is the SP.
Your tenant's signing identity
Each organization has its own SAML signing identity: an RSA-2048 private key and a self-signed X.509 certificate (valid 10 years). It is generated automatically the first time your tenant's metadata or SSO endpoint is hit, and the private key is stored encrypted at rest (AES-256-GCM), like the OIDC signing keys. The certificate is published in your IdP metadata; no two tenants ever share a key.
IdP endpoints
{tenant} is your organization slug. The examples use accounts.obexal.com, the default domain; your custom sign-in domain replaces it if you have one.
| Endpoint | Role |
|---|---|
GET /saml/idp/{tenant}/metadata | IdP metadata (XML). The entityID of your IdP is this URL |
GET or POST /saml/idp/{tenant}/sso | SP-initiated SSO (receives the AuthnRequest, Redirect or POST binding) |
GET /saml/idp/{tenant}/apps/{appId}/sso | IdP-initiated SSO into one registered app (RelayState query parameter optional) |
curl https://accounts.obexal.com/saml/idp/acme/metadata
# 200, Content-Type: application/samlmetadata+xmlMost SPs accept this metadata URL directly; otherwise copy the entityID, SSO URL and certificate out of the XML.
Register a service provider
Registration requires the apps:manage permission (console session or an obx_ admin API token). Each SP is declared with its entityID and exactly one ACS URL:
curl -sS -X POST https://accounts.obexal.com/v1/admin/saml-idp/apps \
-H "Authorization: Bearer $OBEXAL_API_TOKEN" -H 'Content-Type: application/json' \
-d '{
"name": "Grafana",
"entityId": "https://grafana.example.eu/saml/metadata",
"acsUrl": "https://grafana.example.eu/saml/acs",
"nameIdFormat": "emailAddress",
"nameIdAttribute": "email",
"enabled": true
}'
# 200 -> {"id":"samlsp_...","name":"Grafana","entityId":"...","acsUrl":"...","nameIdFormat":"emailAddress","nameIdAttribute":"email","enabled":true}- The call is an upsert keyed on
entityId: posting the same entityID again updates the app in place. acsUrlmust be an absolutehttps://URL. The assertion is a bearer credential posted to that address, so cleartext transport is refused.- Anti-exfiltration: the assertion is only ever posted to the registered ACS. An AuthnRequest that asks for a different
AssertionConsumerServiceURLis refused, so a compromised SP cannot redirect assertions elsewhere. "enabled": falseis a kill switch: a disabled SP is treated as if it did not exist.GET /v1/admin/saml-idpreturns your IdP coordinates (entityId,metadataUrl,ssoUrl) plus the registered apps;DELETE /v1/admin/saml-idp/apps/{id}removes one.
NameID and assertion attributes
nameIdAttribute selects what identifies the user at the SP:
email(default): the NameID is the user's email address. Obexal refuses to assert an unverified email (HTTP 403): a signed assertion is a vouching act, and asserting an unproven address would be an impersonation primitive.subject: the NameID is the stable internal user ID. This mode works even without a verified email.
nameIdFormat sets the declared format: emailAddress (default), persistent or unspecified.
When the email is verified, the assertion also carries it as attributes uid, eduPersonPrincipalName and cn. If the user belongs to groups, their names are emitted as a multi-valued eduPersonAffiliation attribute (omitted when empty).
SP-initiated and IdP-initiated sign-in
SP-initiated: the app redirects the browser to your SSO endpoint with an AuthnRequest. If there is no Obexal session, the user is sent to the sign-in page first and comes back automatically. Obexal then answers with an auto-submitting form that POSTs the signed response to the app's ACS. The asserted session validity is capped at 8 hours.
IdP-initiated: the user clicks the app tile in their Obexal portal, which opens /saml/idp/{tenant}/apps/{appId}/sso. No AuthnRequest is involved; the assertion goes straight to the registered ACS.
In both flows an active Obexal session is required, and the session must belong to the tenant of the IdP: a user from another organization gets a 403, never an assertion.
Errors
| Response | Meaning |
|---|---|
| 302 to the sign-in page | No active session; the user signs in and returns |
| 400 | Malformed AuthnRequest, or SP unknown or disabled (deliberately indistinguishable) |
| 403 | NameID mode email with an unverified address, or session from another tenant |
Honest limits
- No SAML Single Logout: signing out of Obexal ends the Obexal session, but sessions already established at SPs live on until they expire there. See sessions and logout.
- The signing certificate is self-signed only: you cannot upload a CA-issued certificate or bring your own key.
- One ACS URL per SP, HTTP-POST binding only.
- Assertions are signed but not encrypted.