Configure SAML SSO for a Cydarm instance
End-to-end Cydarm SAML SSO setup guide: IdP coordination, metadata staging, DB and docker-compose wiring, plus troubleshooting.
This guide walks an operator through configuring a Cydarm stack to delegate authentication to a customer's Identity Provider via SAML 2.0. It assumes you have shell + DB access on the host running the Cydarm stack, and a contact at the customer's IdP team who can issue you a metadata XML and either accept an Audience ID from us or tell us the one they've already chosen.
Cydarm currently supports one SAML auth source per stack. Multiple users can use it simultaneously, and an internal-password user can coexist with SAML users, but you cannot configure two SAML IdPs against the same stack.
1. Coordinate with the IdP team
Before touching the Cydarm host, you need three things from the customer's IdP team and one or two things to give them.
What to request from the IdP team
-
IdP metadata XML file. A single XML document describing the IdP - sign-on URL, the IdP entity ID, and the X.509 certificate(s) Cydarm will use to verify SAML assertion signatures. Most IdPs expose this at a stable URL (Okta:
https://<tenant>.okta.com/app/<app>/sso/saml/metadata; Microsoft Entra ID / Azure AD: "Federation Metadata XML" download in the Enterprise Application;
JumpCloud: download from the SSO app config page). Either get the URL or get the file directly. -
Confirmation of the NameID format the IdP will assert (typically emailAddress or persistent). Cydarm uses the asserted NameID as the user's auth source handle unless an explicit attribute mapping overrides it.
-
The list of SAML attributes the IdP will release, with their attribute names - at minimum a stable identifier and the attribute the IdP will use for username (often the UPN).
What to give the IdP team
-
Audience URI / SP Entity ID (Cydarm's "App ID"). This is sometimes generated on the Cydarm side and given to the IdP team to record, and sometimes set on the IdP side and given to us. Either way, the value must match exactly between Cydarm's cy_auth_source.source_config and the IdP's app definition. If you're generating it, a UUID is fine. A mismatch produces a NotInAudience rejection (see Troubleshooting).
-
Assertion Consumer Service (ACS) URL. Where the IdP POSTs the SAML response after authenticating. Fixed by Cydarm's backend route:
https://<your-cydarm-fqdn>/cydarm-api/auth/saml, where <your-cydarm-fqdn> is the external FQDN your stack resolves at. It must be HTTPS and publicly reachable from the user's browser (not server-to-server). No port number is needed in the URL - port 443 is the default for HTTPS and IdPs reject explicit :443 in some configurations. -
Binding: HTTP-POST. Cydarm parses SAMLResponse and RelayState fields out of a posted form.
2. Stage the IdP metadata file on the host
Drop the metadata XML you received from the IdP team into the local config directory:
/data/cydarm/config/local/case-management/idp-metadata.xml
Permissions: readable by the user the api-proxy container runs as.
If the IdP only exposes metadata via a URL (and you'd rather not snapshot it), Cydarm also supports https://... and data:text/xml;base64,... schemes for the metadata source - see step 3. The file:// path is the most common and the easiest to control change-management on.
-
Add the SAML auth source to the database
/data/cydarm/bin/cydarm-admin.sh psql
In the psql session, insert a row into cy_auth_source:
INSERT INTO cy_auth_source (name, source_type, source_config) VALUES (
'okta', -- short, lowercase IdP nickname; used in logs and as cy_user.auth_source_id
'saml',
'{
"app_id": "6f3a8c40-9f12-4b8a-9e60-c8e1d4a6f2a7",
"idp_metadata_url": "file:///config/idp-metadata.xml"
}'
);Field notes:
-
name - short identifier used internally and shown in logs as the IdP nickname. okta, entra, jumpcloud, gsuite are all reasonable. Keep it lowercase and short. Existing users get tied to this name via cy_user.auth_source_id.
-
app_id - the Audience URI / SP Entity ID, whether you generated it or the IdP did. Must match the IdP-side configuration character-for-character.
-
idp_metadata_url -
file:///config/idp-metadata.xmlresolves inside the api-proxy container after step 4's bind-mount lands. If you'd rather fetch live, usehttps://<idp-metadata-url>instead, ordata:text/xml;base64,<b64-of-xml>to embed the metadata directly.
You can also set name_id_format in the JSON if the IdP's NameID format isn't the default — but this is rarely necessary if the UPN has been configured correctly on the IdP. Leave it out unless you have a specific reason.
Confirm the row landed:
SELECT id, name, source_type FROM cy_auth_source WHERE source_type = 'saml';For each user who should authenticate via this IdP, update cy_user. Use a nested SELECT to derive auth_source_id so you don't have to hard-code the integer key:
-- find the user
SELECT id, username, auth_source_id, auth_source_handle FROM cy_user WHERE username = 'awhatmore';
-- attach to the SAML source by name
UPDATE cy_user
SET auth_source_id = (SELECT id FROM cy_auth_source WHERE name = 'okta'),
auth_source_handle = 'awhatmore@acme.example' -- the value the IdP will assert as NameID
WHERE id = <user-id>;auth_source_handle is the user's identity as seen by the IdP. If the IdP asserts NameID = awhatmore@acme.example, that's the value that goes here — character-for-character. A handle mismatch produces an Auth source handle not found 403 (see Troubleshooting).
4. Wire the metadata XML into the api-proxy container
Update /data/cydarm/config/local/docker-compose.yaml so the metadata XML you staged in step 2 is bind-mounted into the api-proxy container at the path you referenced in idp_metadata_url:
services:
api-proxy:
configs:
- source: sso-config
target: /config/idp-metadata.xml
configs:
sso-config:
file: /data/cydarm/config/local/case-management/idp-metadata.xmlNote: the configs: block at the bottom is the top-level docker-compose configs section, not a nested one inside the services: block. Double-check the indentation.
5. Apply config and restart the stack
/data/cydarm/bin/cydarm-admin.sh merge-config
/data/cydarm/bin/cydarm-admin.sh stop
/data/cydarm/bin/cydarm-admin.sh startmerge-config regenerates the _runtime directory from the templates and the contents of local. stop then start brings the api-proxy back up with the new bind mount and the new auth source visible to the backend on startup.
6. Verify the configuration
In a new browser session (no cached cookies):
-
Hit
https://<your-cydarm-fqdn>/. -
Click the SSO login button.
-
You should be redirected to the IdP, authenticate, and land back on the Cydarm dashboard with an Access-Token cookie set.
In the backend logs you should see:
Registering SAML provider "okta"
read SAML config from file /config/idp-metadata.xml
Build auth URL with relayState=...
Created new session from SAML request: <uuid>If you see those four lines and the user lands on the dashboard, you're done.
Troubleshooting
The Cydarm SAML authenticator emits explicit reason strings on every rejection. The pattern to grep for is “SAML authn rejected:” - the suffix tells you exactly which check failed.
SAML authn rejected: InvalidTime
Cause: clock skew between the IdP and the Cydarm host pushes the SAML assertion's NotBefore/NotOnOrAfter window outside what the host considers "now."
Fix: synchronise the Cydarm host's clock (timedatectl, chrony, NTP) with the IdP's. SAML's tolerance is small; you want sub-second skew.
SAML authn rejected: NotInAudience
Cause: the app_id in cy_auth_source.source_config does not match the Audience URI configured at the IdP for this app.
Fix: pull the app_id value from the DB:
SELECT source_config->>'app_id' FROM cy_auth_source WHERE source_type = 'saml';Compare character-for-character with the IdP-side Audience URI / SP Entity ID. UUIDs are case-insensitive in some IdPs and case-sensitive in others — match the case the IdP uses.
This is the most common mismatch on a fresh setup.
SAML authn rejected: no assertion ID found
Cause: the SAML response from the IdP is structurally valid but contains no assertion (or an assertion with no ID).
Fix: usually an IdP-side configuration problem. Get the IdP team to capture the SAMLResponse they're sending and confirm it carries a populated <saml:Assertion ID="..."> element.
SAML authn rejected: session '...' with SSO ID '...' already exists
Cause: the IdP is replaying a SAML response Cydarm has already consumed. Each SAML assertion has a unique ID and Cydarm refuses to honour the same ID twice.
Fix: in normal use, this means the user double-submitted (back button, refresh on the post-SSO landing page). Have them re-initiate the login from the start. If you see this for every login attempt, the IdP may be configured to reuse assertion IDs — escalate to the IdP team.
Auth source handle not found / 403 with no other detail
Cause: the SAML response was valid, but the user's auth_source_handle (the asserted NameID) doesn't match any cy_user row, and SAML user auto-creation is not enabled.
Fix: in psql:
SELECT auth_source_id, auth_source_handle FROM cy_user WHERE username = '<username>';Compare auth_source_handle against what the IdP is asserting. The backend logs the asserted value at debug level — turn debug logging on if needed. If they look identical but still don't match, check for trailing whitespace, case differences, or a domain in the asserted value that isn't in the stored handle.
User with handle ... has auth source id internal
Cause: the user exists, but their cy_user.auth_source_id is internal (i.e. they're set up for password auth, not SAML). The backend refuses to upgrade an internal user to SAML implicitly.
Fix: re-run the UPDATE cy_user from step 3 to set auth_source_id to the SAML source's id and auth_source_handle to the right value.
User with handle ... has external auth source id internal (or any other mismatch)
Cause: the user is configured for an external auth source whose name doesn't match the SAML source that just authenticated them. Usually a leftover from a prior IdP config that wasn't cleaned up when the new one was added.
Fix: update the user's auth_source_id to the current SAML source's id.
unable to find EntityDescriptor or EntitiesDescriptor
Cause: the IdP metadata XML at idp_metadata_url is malformed, or you've accidentally pointed at a non-metadata document (e.g. an HTML login page from the IdP's web UI).
Fix: open the file at /data/cydarm/config/local/case-management/idp-metadata.xml and confirm the root element is <EntityDescriptor> or <EntitiesDescriptor>. If you fetched it from a URL, re-fetch with curl -L. If the IdP signs metadata, make sure the file you got is the unwrapped XML, not a multipart-mime envelope.
unsupported IdP metadata URL
Cause: idp_metadata_url doesn't start with file://, https://, or data:text/xml;base64,. Other schemes (including http:// without TLS) are rejected for security.
Fix: convert the URL to one of the supported schemes. For air-gapped stacks, file:// is the right answer.
Login redirects to IdP, IdP authenticates successfully, but the user lands on a Cydarm error page
Cause: the ACS URL the IdP is posting to doesn't match the Cydarm-side route.
Fix: confirm the IdP-side ACS URL is exactly https://<your-cydarm-fqdn>/cydarm-api/auth/saml - no trailing slash, no explicit :443, scheme is https.
"It works in dev but not in prod"
Most common causes, in order:
-
ACS URL still pointing at the dev FQDN in the IdP-side config.
-
app_id mismatch (re-issued in dev but not re-given to the IdP for prod, or vice versa).
-
Stale idp-metadata.xml on disk that was never re-pulled when the IdP rotated certificates.
-
Metadata file accidentally placed in _runtime and silently overwritten by merge-config - confirm it's in local.
Capture a SAMLResponse from the prod browser session (browser dev tools → Network → POST to /cydarm-api/auth/saml → SAMLResponse form field), base64-decode it, and compare the <Audience> element to the app_id in the DB. They should match exactly.
Inspecting a SAML response
When a rejection isn't obvious from the logs, decode the actual SAMLResponse the IdP is sending and look at it:
-
In the browser, open dev tools → Network → enable Preserve log (otherwise the entry vanishes when the page navigates after auth).
-
Click the SSO login button. Find the POST to /cydarm-api/auth/saml.
-
Under Form Data / Payload, copy the SAMLResponse value.
-
Paste it into https://gchq.github.io/CyberChef/ with the recipe From Base64 → XML Beautify. CyberChef runs entirely client-side, so the response never leaves the browser — safe for production SAML responses that may carry PII.
The decoded XML contains:
-
saml:Audience— compare against app_id in cy_auth_source.source_config. Mismatch → NotInAudience. -
saml:NameID— compare against cy_user.auth_source_handle. Mismatch → Auth source handle not found. -
<saml:Assertion ID="…"> — must be populated. Empty → no assertion ID found.
-
<saml:Conditions NotBefore="…" NotOnOrAfter="…"> — compare against the host clock. Outside the window → InvalidTime.
If the response contains saml:EncryptedAssertion instead of saml:Assertion, the IdP is configured to encrypt assertions. You can't read the contents without the SP's private key - escalate to whoever holds the Cydarm SAML signing keypair, or ask the IdP team to disable assertion encryption for the diagnostic.
Where to look in logs
/data/cydarm/bin/cydarm-admin.sh logs api-proxy | grep -E "SAML|Saml"Useful greps:
-
Registering SAML provider - confirms the auth source loaded on backend startup
-
read SAML config from - confirms where the metadata came from (file vs URL)
-
Build auth URL - the redirect-to-IdP step worked
-
SAML response values - the attributes the IdP actually asserted (debug level)
-
SAML authn rejected: - every failure pinned with its specific reason
-
Created new session from SAML request - successful login