SAML SSO setup
Connect your identity provider to the PAID dashboard over SAML 2.0 for single sign-on and SCIM 2.0 for automated user lifecycle. This guide covers Okta, Auth0, Azure AD (Entra ID), and any generic SAML 2.0 IdP.
/sso/*, /scim/v2/*, and the /v1/account/saml_config / /v1/account/scim_config admin endpoints —
returns HTTP 503 service_unavailable until the platform operator sets SAML_SSO_ENABLED=true (SAML) and SCIM_ENABLED=true (SCIM) on the API.
The one exception is GET /v1/account/saml_config/assertion_log, which is available
regardless of the flags.
A 503 saying "disabled in this environment" means the platform flag is off — contact
support. A 503 saying "not enabled for this account" means the flag is on but your
account's own Enable toggle is off.Before you begin
PAID acts as the SAML Service Provider (SP). Your IdP needs three SP-side values, all issued by PAID; your account needs the IdP-side values in return. Everything lives in Dashboard → Security → SSO:
| Value | Where to find it | Where it goes in your IdP |
|---|---|---|
| SP Entity ID | SSO → SAML (read-only) | Audience URI (Okta) · Identifier (Azure) · audience (Auth0) |
SP ACS URL — /sso/acs/:account_slug | SSO → SAML (read-only) | Single sign-on URL (Okta) · Reply URL (Azure) · Callback URL (Auth0) |
| SP metadata XML | "Download SP Metadata XML" button, or GET /sso/metadata/:account_id | Metadata import, where the IdP supports it |
SCIM Base URL — /scim/v2/:account_id/ | SSO → SCIM (read-only) | SCIM connector base URL (Okta) · Tenant URL (Azure) |
| SCIM bearer token | SSO → SCIM — shown once at generation | HTTP Header auth (Okta) · Secret Token (Azure) |
# SP metadata XML — public once SAML is enabled for your account.
curl https://api.trustfabric.ai/sso/metadata/$ACCOUNT_ID \
-o paid-sp-metadata.xmlHow provisioning works
- Users are JIT-provisioned on their first successful SAML assertion. The
emailattribute is required — an assertion without a resolvable email is refused rather than guessed. default_rolemust bevieweroreditor. The server rejectsadminoutright: SSO can never grant admin, neither as the default role nor throughrole_mappingat JIT time. Admin escalation is a manual operator action.role_mappingmaps PAID roles to IdP group values carried in thegroupsattribute, and is enforced server-side — the IdP cannot inject roles outside the mapping.- Attribute names match exactly (case-sensitive) against the assertion
attribute's
NameorFriendlyName. When the email mapping is left empty it defaults toEmailAddress. - If you set
idp_metadata_url, PAID re-fetches it whenever you re-save the config (PUT) or run the/testdry-run — an automatic 24-hour background re-fetch is on the roadmap but not live yet. A changed IdP signing certificate detected on save is flagged, not trusted — assertions keep verifying against the old certificate until you re-save with"approve_cert_change": true. force_sso+sso_domainreject password login for users whose email matches the domain.force_sso: truewithout a domain is refused (it would fail open).
SCIM 2.0 provisioning
PAID exposes a standard RFC 7644 SCIM server per account at https://api.trustfabric.ai/scim/v2/:account_id/. Copy the exact Base URL from SSO → SCIM, then generate the bearer token there —
or via the API:
# Returns the cleartext bearer token EXACTLY ONCE. PAID stores only an
# Argon2id hash — copy the token into your IdP immediately. Rotating
# invalidates the previous token instantly.
curl -X POST https://api.trustfabric.ai/v1/account/scim_config/rotate_token \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# → { "bearer_token": "…", "token_rotated_at": "…",
# "base_url": "https://api.trustfabric.ai/scim/v2/YOUR_ACCOUNT_ID/" }| Endpoint | Supported operations |
|---|---|
/Users | GET (list), POST (create), GET /:id, PUT, PATCH, DELETE (deactivates — never a hard delete) |
/Groups | GET (list) only on the direct routes — direct group push (POST/PUT/PATCH on /Groups) is not exposed; group create/replace/delete are accepted only inside /Bulk envelopes. Group membership never drives roles — those come from the SAML groups attribute + role mapping |
/Bulk | POST, up to 100 operations per envelope by default |
/ResourceTypes, /Schemas, /ServiceProviderConfig | GET — discovery is also auth-checked, so unauthenticated probes get 401, not a fingerprint |
- The token is compared against an Argon2id hash on every request and is never logged.
- SCIM writes can never grant the admin role — it is clamped server-side.
- A deactivation flood (DELETE or
PATCH active=falsetouching more than 10% of active users within 5 minutes, by default) raises an internal alert.
# Any RFC 7644 client works. Every endpoint — including the discovery
# routes — requires the SCIM bearer token.
curl https://api.trustfabric.ai/scim/v2/$ACCOUNT_ID/ServiceProviderConfig \
-H "Authorization: Bearer $SCIM_BEARER_TOKEN" \
-H "Accept: application/scim+json"
curl https://api.trustfabric.ai/scim/v2/$ACCOUNT_ID/Users \
-H "Authorization: Bearer $SCIM_BEARER_TOKEN" \
-H "Accept: application/scim+json"Okta
Create the app
- Okta Admin Console → Applications → Create App Integration → SAML 2.0.
- Single sign-on URL = your SP ACS URL (tick "Use this for Recipient URL and Destination URL"). Audience URI (SP Entity ID) = your SP Entity ID.
- Name ID format:
EmailAddress. If you forget the email attribute statement below, PAID falls back to an email-format NameID — but set the attribute anyway. - From the app's Sign On tab, copy the Identity Provider metadata URL into PAID's IdP Metadata URL field on SSO → SAML, then save with SAML enabled.
Map attributes
Add attribute statements in Okta and mirror the same names in PAID's Attribute Mapping — matching is exact and case-sensitive:
| Okta attribute statement | Okta value | PAID mapping field |
|---|---|---|
EmailAddress | user.email | email (this is also PAID's default) |
FirstName | user.firstName | first_name |
LastName | user.lastName | last_name |
Groups (group attribute statement) | group filter, e.g. starts with paid- | groups |
Map the Okta group names your filter emits to PAID roles under Role Mapping (editor / viewer). Users start sessions at https://api.trustfabric.ai/sso/login/YOUR_ACCOUNT_SLUG.
SCIM provisioning
- In your Okta app: General → App Settings → Enable SCIM provisioning.
- Provisioning → Integration: SCIM connector base URL = the Base URL from SSO → SCIM; unique identifier field =
email; authentication mode = HTTP Header, with the PAID bearer token. - Enable Create Users, Update User Attributes, and Deactivate Users.
Skip Push Groups — Okta pushes groups via direct
POST /Groups, which PAID does not expose (group writes are accepted only inside/Bulkenvelopes), and group membership never drives roles anyway.
Getting 503s from every Okta test? That is the platform flag, not your config — see the enablement note at the top of this page.
Auth0
Create the app
- Auth0 Dashboard → Applications → Create Application (Regular Web Application), then open Addons → SAML2 Web App.
- Application Callback URL = your SP ACS URL.
- In the addon's Settings JSON set
"audience"to your SP Entity ID and probe the NameID from email:"nameIdentifierProbes": ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]. - The addon's Usage tab lists the IdP metadata URL
(
https://YOUR_TENANT.auth0.com/samlp/metadata/YOUR_CLIENT_ID) — paste it into PAID's IdP Metadata URL on SSO → SAML.
Map attributes
Use the addon's "mappings" setting to control the attribute names Auth0 emits, and
mirror those exact names in PAID's Attribute Mapping. Auth0 does not emit a groups attribute by
default — inject one with a post-login Action (or legacy Rule) that adds a custom SAML
attribute (e.g. Groups) from your authorization data, then point PAID's groups mapping at that name and configure Role Mapping.
SCIM provisioning
Auth0 has no built-in outbound SCIM push to downstream service providers. Two honest options: rely on SAML JIT provisioning alone (users are created on first login, deactivation stays manual), or drive PAID's SCIM API directly from your own tooling — it is a standard RFC 7644 server, so any SCIM client library works with the bearer token from SSO → SCIM.
Reminder: both the SAML and SCIM surfaces are flag-gated platform-side
(SAML_SSO_ENABLED / SCIM_ENABLED) — a blanket 503 is the flag, not
your Auth0 config.
Azure AD (Entra ID)
Create the app
- Entra admin center → Enterprise applications → New application → Create your own application → "Integrate any other application you don't find in the gallery (Non-gallery)".
- Single sign-on → SAML. Identifier (Entity ID) = your SP Entity ID; Reply URL (Assertion Consumer Service URL) = your SP ACS URL.
- Copy the App Federation Metadata URL from the SAML Certificates card into PAID's IdP Metadata URL on SSO → SAML.
Map attributes
Azure emits claims under full URI names, and PAID matches attribute names exactly — so put the full URIs into PAID's Attribute Mapping:
| PAID mapping field | Azure claim name |
|---|---|
email | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress |
first_name | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname |
last_name | http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname |
groups | http://schemas.microsoft.com/ws/2008/06/identity/claims/groups (add via Attributes & Claims → Add a group claim) |
Azure's group claim carries group object IDs (GUIDs) by default — use those GUIDs as the values in PAID's Role Mapping unless you configure Azure to emit names.
SCIM provisioning
- In the enterprise application: Provisioning → Automatic.
- Tenant URL = the Base URL from SSO → SCIM; Secret Token = the PAID bearer token.
- Test Connection — Azure authenticates against PAID's discovery endpoints, so
a failure here with 503 means the platform
SCIM_ENABLEDflag is off (see the note at the top), while 401 means the token is wrong or was rotated.
Generic SAML 2.0
Any spec-compliant SAML 2.0 IdP works the same way — Google Workspace, JumpCloud, OneLogin, Ping Identity, Duo SSO. The contract:
- Give your IdP the SP Entity ID and ACS URL (or import the SP metadata XML from
GET /sso/metadata/:account_id). - Give PAID either the IdP metadata URL (
https://only — plainhttp://and IP-literal URLs are rejected) or the explicit triple: IdP Entity ID, IdP SSO URL, and the IdP signing certificate as PEM. - Configure attribute mapping (email required) and role mapping, either on SSO → SAML or via the API:
# Either idp_metadata_url OR the explicit triple
# (idp_entity_id + idp_sso_url + idp_signing_certificate) is required.
# sp_entity_id must be a fully-qualified https:// URL — no wildcards,
# no IP literals (the server rejects both) — and its FINAL path segment
# must be the same YOUR_ACCOUNT_SLUG used in sp_acs_url and the login
# URL: the server resolves your account by that trailing segment.
curl -X PUT https://api.trustfabric.ai/v1/account/saml_config \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"idp_metadata_url": "https://idp.example.com/app/saml/metadata",
"sp_entity_id": "https://api.trustfabric.ai/sso/YOUR_ACCOUNT_SLUG",
"sp_acs_url": "https://api.trustfabric.ai/sso/acs/YOUR_ACCOUNT_SLUG",
"attribute_mapping": {
"email": "EmailAddress",
"first_name": "FirstName",
"last_name": "LastName",
"groups": "Groups"
},
"role_mapping": {
"editor": ["paid-engineering"],
"viewer": ["paid-support"]
},
"default_role": "viewer",
"force_sso": false,
"sso_domain": ""
}':account_slug in /sso/login/:account_slug and /sso/acs/:account_slug must equal the
final path segment of sp_entity_id — the server resolves your account by that
suffix, so a mismatched sp_entity_id makes every SP-initiated login and ACS post
fail with 404 not_found. Copy both values from SSO → SAML rather than constructing them by hand.Then dry-run the configuration before pointing users at it:
# Dry-run: validates the stored config and builds a SAML AuthnRequest
# without sending traffic to your IdP.
curl -X POST https://api.trustfabric.ai/v1/account/saml_config/test \
-H "Authorization: Bearer $PAID_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# → { "ok": true, "checks": [
# { "name": "config_present", "ok": true },
# { "name": "authn_request_build", "ok": true },
# { "name": "idp_metadata_fetch", "ok": true },
# { "name": "default_role_not_admin","ok": true } ], ... }- SP-initiated login:
GET /sso/login/:account_slug(optional?return_to=deep link, validated same-origin at the ACS). - Logout:
GET /sso/logout/:account_slug. PAID also accepts IdP-initiated Single Logout atPOST /sso/slo/:account_slug— inbound requests are acknowledged and audited, but the signed LogoutResponse round-trip is not implemented yet, so treat IdP-side SLO configuration as optional. - For SCIM, any RFC 7644 client can target the Base URL with the bearer token — see the SCIM section for the endpoint surface and its limits.
- Flag note: every route here 503s until the platform operator enables
SAML_SSO_ENABLED/SCIM_ENABLED, and SAML additionally requires your account's own Enable toggle.
Troubleshooting
Every ACS attempt — success or failure — is recorded. See the last 50 events under SSO → SCIM → Recent Provisioning Events, or pull
them from GET /v1/account/saml_config/assertion_log (this endpoint works even while
the platform flags are off, so it is always available for debugging).
| Status | What it means |
|---|---|
success | Assertion verified; user logged in (and JIT-provisioned if new). |
signature_failed | Signature invalid, missing, wrapped (XSW), or the assertion was addressed to another account. Usually a stale IdP certificate. |
expired | Assertion outside its validity window — NotBefore/NotOnOrAfter are enforced with a small clock-skew allowance. Check IdP clock skew. |
replay | Assertion ID already consumed once. |
missing_attr | No resolvable email. Attribute names match exactly and case-sensitively — compare the assertion's attribute Name against your mapping. |
signature_failed until an admin acts — rotation is not detected automatically
(the 24-hour background metadata re-fetch is roadmap, not live). Re-save the config with "approve_cert_change": true (a PUT /v1/account/saml_config): the save
re-fetches your IdP metadata, detects the new certificate, and promotes it. The swap is refused
entirely if the audit pipeline is down, so every rotation leaves an audit trail.