Skip to content
Docs

Single Sign-On & Directory Sync

Identity is where a governed AI platform starts: every budget, policy and audit record hangs off a person. AgentWatch integrates with the identity provider you already run rather than asking you to maintain a second user list.

Protocol Support
SAML 2.0 Native service provider — metadata exchange, signed assertions, certificate upload, IdP-initiated and SP-initiated flows
OIDC / OAuth 2.0 Standard authorization-code flow against any compliant provider
LDAP / Active Directory Direct bind with LDAPS and CA certificate upload
SCIM v2 Per-organization provisioning and de-provisioning with managed tokens

A unified hub-and-spoke setup wizard covers OIDC, SAML and LDAP, so the configuration path is the same shape whichever protocol you use. Providers are configured per organization, so each tenant in a multi-tenant deployment brings its own identity provider.

That is your choice, per provider, and it is the single most consequential setting on this page.

  • Auto-create on — the first successful login provisions the user, mints their gateway identity, and applies group-to-team mapping. Suitable when your identity provider is already the authority on who should have access.
  • Auto-create off — the user lands in a pending state and no gateway key is issued until a tenant administrator approves them. Suitable when access must be granted deliberately.

An optional domain restriction narrows auto-creation to addresses on domains you own. Leaving it empty means anyone who can authenticate through that provider is provisioned, which is exactly right for some deployments and wrong for others — the field exists so that it is your decision rather than a default.

Group-to-team mapping is unified across SAML, OIDC and LDAP, with three modes:

Mode Behavior
Off Team is assigned by the default-team rule; groups are ignored
Automatic A parse rule derives the team name from the group claim, so a new group creates a team without configuration
Mapped An explicit table maps each group to a team, for when directory names and team names differ

Team membership matters because team drives budget roll-ups, model-access scoping and every team filter in the console. A user with no team has no budget parent, so AgentWatch resolves a default team rather than leaving the field empty.

Roles can be assigned from the directory too, so an administrator in your IdP arrives as an administrator here.

What exactly does my IdP put in the group claim?

Section titled “What exactly does my IdP put in the group claim?”

This is the question that decides whether mapping works, and there is no single answer — which is why AgentWatch will tell you rather than make you guess:

Terminal window
curl -s "https://<your-agentwatch-host>/admin/sso/observed-groups?providerId=<provider id>" \
-H "Authorization: Bearer <admin JWT>"

It returns the exact values your tenant has actually received, each flagged as opaque or not, ready to paste into a mapping. A sign-in whose groups match nothing is recorded in the log together with this URL.

What the major directories send by default:

Directory Default groups claim Notes
Microsoft Entra ID Group object IDs (GUIDs) — e.g. 8f4c…-…-… Display names only when the app registration is configured to emit them; sAMAccountName for on-premises synced groups
Okta Group names Configurable per app
Google Workspace Group email addresses
LDAP / AD Full distinguished names (CN=Engineering,OU=…)

Entra emitting GUIDs is the cause of most group-mapping trouble, and it produced two distinct symptoms before this was handled:

  • Automatic mode derived team names with a rule that leaves a GUID untouched, creating teams named after GUIDs. It now skips opaque identifiers instead of naming a team after one, and records why.
  • Mapped mode required an exact, case-sensitive match against the raw claim, so typing Engineering against a GUID claim matched nothing, silently.

Which spellings of a group name will match?

Section titled “Which spellings of a group name will match?”

Matching is shape-tolerant and case-insensitive, so you can enter whichever form you have in front of you. All of these map the same directory group:

You enter Matches a claim of
Engineering Engineering, CN=Engineering,OU=Groups,DC=corp, CORP\Engineering
CN=Engineering,OU=Groups,DC=corp the same, in any of those spellings
CORP\Engineering the same
8f4c1e2a-… (object ID) that group’s GUID claim

It is deliberately not a substring match: Engineering does not match Engineering-Contractors. A mapping that matches too much puts people in teams they do not belong to, which is worse than one that matches nothing.

If you would rather map by name than by GUID, configure group claims in the Entra app registration to emit names instead — either approach works.

How do I verify group mapping before rolling it out?

Section titled “How do I verify group mapping before rolling it out?”
  1. Sign one pilot user in through the IdP.
  2. Read back what the claim actually contained: GET /admin/sso/observed-groups?providerId=<id>
  3. Add a mapping for each group you care about (Settings → Single Sign-On → your provider → Group mapping), pasting a value from step 2.
  4. Sign the pilot user in again and confirm their team in Employee Management.

You should now see the user in the mapped team, and their spend rolling up to that team’s budget.

Symptom Cause Fix
Teams appear named after GUIDs Automatic mode against an Entra groups claim Upgrade to 2.0.283+; opaque ids are skipped. Delete the GUID-named teams and use Mapped mode, or emit names from Entra
A mapping is configured and nothing matches The claim carries GUIDs while the mapping holds display names (or vice versa) GET /admin/sso/observed-groups and paste a real value
Everyone lands in one team Mapping mode is Off, or the IdP sends no group claim at all Set the mode, and confirm the claim is actually emitted
A user is in too many teams A mapping value is a prefix of several group names Use the full distinguished name or the object ID
PUT /scim/v2/Groups/:id returned 404 Replace-style group updates were unimplemented before 2.0.283 Upgrade
SCIM creates duplicate users userName/externalId matched case-sensitively before 2.0.283 Upgrade; both are caseExact:false per RFC 7643 §7

All three protocols follow the same path: Settings → Single Sign-On → Add provider, choose the protocol, paste the values from your identity provider, save as a draft, run the built-in test, then enable it. Nothing takes effect until you enable it, so a misconfigured provider cannot lock anyone out.

What your identity provider needs from AgentWatch

Field Value
ACS / Reply URL (SAML) https://<your-agentwatch-host>/api/auth/saml/callback/<provider id>
Redirect URI (OIDC) https://<your-agentwatch-host>/api/auth/oauth2/callback/<provider id>
Entity ID / Audience the Entity ID you entered on the provider (SAML), shown on the provider row once saved

SAML: what the “SAML Issuer (Entity ID)” field is

Section titled “SAML: what the “SAML Issuer (Entity ID)” field is”

The SAML Issuer (Entity ID) you enter on a SAML provider is your identity provider’s entity ID — the value Entra ID, Okta or ADFS uses to identify itself (for Entra, https://sts.windows.net/{tenant}/). If you upload your IdP’s metadata XML, AgentWatch fills it in for you from the document’s entityID attribute.

It is not AgentWatch’s own entity ID. AgentWatch’s entity ID — the one your IdP needs in its SP configuration — is always:

https://<your-agentwatch-host>/api/auth/saml/metadata/<provider id>

which is exactly what the metadata endpoint at that URL publishes. Nothing to set, and nothing to change on upgrade.

My SAML metadata says http:// — how do I fix it?

Section titled “My SAML metadata says http:// — how do I fix it?”

If the Entity ID and ACS URL AgentWatch publishes begin with http:// while your users reach it over HTTPS, your reverse proxy is terminating TLS without telling AgentWatch. The connection AgentWatch sees is plain HTTP, so that is what it advertises — and most identity providers refuse to register a non-HTTPS Assertion Consumer Service URL, because SAML 2.0 expects assertions to be protected in transit.

Check what your deployment publishes:

Terminal window
curl -s https://<your-agentwatch-host>/api/auth/saml/metadata/<provider id> | grep entityID

There are two fixes. Prefer the first — it is one line of proxy configuration and needs nothing per-deployment.

1. Have the proxy declare the scheme. From 2.0.301 AgentWatch honours both the standards-track RFC 7239 header and the de-facto one. Set it on the frontend that terminates TLS, not on a port-80 frontend that only redirects — a redirect-only frontend never reaches the application, so the header there has no effect:

frontend https-in
bind *:443 ssl crt /etc/pki/tls/certs/your.pem
mode http
http-request set-header X-Forwarded-Proto https
location / {
proxy_pass http://agentwatch:8787;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}

Use set-header rather than add-header: it replaces any value the caller sent, so a client cannot spoof the scheme. Reload the proxy and re-run the curl above — the entity ID should now begin https://.

2. Or pin it in AgentWatch. If you cannot change the proxy, set AW_PUBLIC_BASE_URL (below). It takes precedence over every header, so no request can influence it.

Symptom Cause Fix
Metadata shows http://, users reach HTTPS Proxy sends neither Forwarded nor X-Forwarded-Proto Set the header on the TLS frontend, or set AW_PUBLIC_BASE_URL
Header set but nothing changed It is on a redirect-only (port 80) frontend Move it to the frontend with bind *:443 ssl
IdP rejects the ACS URL It is not HTTPS As above, then re-upload or re-enter the metadata at the IdP
Entity ID changed after fixing this The published entity ID now carries https Update the Entity ID registered at your IdP to match the metadata

Set AW_PUBLIC_BASE_URL to the externally reachable URL of your deployment, without a trailing slash:

Terminal window
AW_PUBLIC_BASE_URL=https://agentwatch.example.com

When it is unset, the SP metadata document and the ACS URL are built from the Host header of whatever request asked for them. That header is set by the caller, so metadata fetched through an unexpected hostname advertises that hostname — and SP metadata is a document administrators hand directly to their IdP. Setting this pins both values to your real address regardless of how the request arrived. It is not mandatory, because requiring it would break existing deployments on the one path where a wrong answer locks everybody out; AgentWatch logs a one-time saml-public-base-unset warning instead.

Verify it took effect:

Terminal window
curl -s https://<your-agentwatch-host>/api/auth/saml/metadata/<provider id> | grep entityID

The entityID and the Location of the AssertionConsumerService should both name your public URL.

Over the API, for infrastructure-as-code:

Terminal window
curl -s -X POST https://<your-agentwatch-host>/api/admin/oauth2/providers \
-H "Authorization: Bearer <tenant-admin JWT>" \
-H 'Content-Type: application/json' \
-d '{
"name": "Okta",
"protocol": "saml",
"samlIssuer": "http://www.okta.com/exk…",
"samlEntryPoint": "https://acme.okta.com/app/…/sso/saml",
"samlCertificate": "-----BEGIN CERTIFICATE-----\nMIID…",
"autoCreateUsers": true,
"domainRestriction": "acme.com",
"groupMappingMode": "mapped",
"defaultRole": "user",
"enabled": true
}'

For OIDC, send "protocol": "oidc" with issuerUrl, clientId, clientSecret and scopes. For LDAP, use the LDAP provider form — host, bind DN, base DN, LDAPS and an uploaded CA certificate.

  1. Save the provider as a draft (leave enabled false).
  2. Use Test connection on the provider row — it performs a real bind or metadata fetch and reports the actual error, not a generic failure.
  3. Sign in from a private browser window with one pilot account.
  4. Check Employee Management: the user should exist, with the expected team and role, and a gateway key if auto-create is on.
  5. Enable the provider for everyone.
  1. Settings → Single Sign-On → SCIM, generate a token for the organization.
  2. In your identity provider, set the SCIM base URL to https://<your-agentwatch-host>/scim/v2 and paste the token as the bearer credential.
  3. Push a test user and confirm they appear; then push a group and confirm the team is created or mapped.
  4. De-provision that test user in your directory and confirm their key stops authenticating — this is the half most rollouts forget to verify.

Tokens are per organization and revocable from the same screen, so a leaked token is contained to one tenant.

Symptom Cause Fix
Login loops back to the sign-in page ACS/redirect URL mismatch Make the value in your IdP match the provider row exactly, including the provider id
Invalid signature on SAML Wrong or stale certificate Re-upload the current IdP signing certificate
User signs in but has no access autoCreateUsers is off and they are pending Approve them in Employee Management, or enable auto-create
User signs in but API calls return 401 They have a login but no gateway identity Provision them, or see delegated identity
Everyone lands in one team Group mapping is off, or the claim is missing Set the mapping mode and confirm your IdP actually sends the group claim
Mapping is configured but nothing matches The claim carries object IDs (GUIDs) while the mapping holds display names — Entra’s default GET /admin/sso/observed-groups and paste a value your IdP actually sent
Teams appear named after GUIDs Automatic mode against an Entra group claim Upgrade to 2.0.283+, then map explicitly or emit names from Entra
Nobody can sign in after enabling Domain restriction excludes their address Widen or clear the domain restriction
No account matching <address> exists in this organization The address in your IdP token is not the one the employee was provisioned under Compare it with the Contact column in Employee Management — see below
Email not verified by your IdP… assert email_verified=true A release earlier than 2.0.293 Upgrade. Entra omits the email claim for accounts without a mailbox, and before 2.0.293 that skipped the provisioned-user check entirely
SCIM employees show — in the Team column Created before 2.0.293, which fixed only new employees Upgrade to 2.0.304+ — a one-time sweep at startup fills them in
Provisioning fails with Unsupported PATCH path: <attribute> An attribute in your IdP mapping that AgentWatch has no field for (job title, department, employee ID) Upgrade to 2.0.306+; before that, remove the attribute from the mapping
A user’s details changed in the IdP but not in AgentWatch The same thing — one unsupported attribute rejected the whole update, so the changes beside it never applied Upgrade to 2.0.306+; Entra retries failed users on the next cycle (~40 min). If provisioning is quarantined it will not resume on its own — use Restart provisioning

Why does one account sign in and another not?

Section titled “Why does one account sign in and another not?”

Almost always because the two accounts carry different email claims, not because one of them is configured differently in AgentWatch.

Microsoft Entra fills the email claim from the account’s mail attribute. A normal user has a mailbox, so the claim is present. A privileged or administrative account — the a- prefixed accounts many organisations use — often has no mailbox, so Entra sends no email claim and falls back to the UPN. If the UPN is spelled differently from the address SCIM provisioned (a-mark.deckert@example.com versus a-mdeckert@example.com), sign-in is refused even though the employee is plainly listed in Employee Management.

The refusal message names the address we searched for. Compare it against the Contact column for that employee:

  • They match — the account is not in this organization. Add it, or check you are signing in to the right tenant.
  • They differ — this is the cause. Either correct the employee’s email to the UPN the IdP actually sends, or have the IdP send the provisioned address (in Entra, add email as an optional claim under App registration → Token configuration, or populate the account’s mail attribute).

email_verified is not the cause, despite what releases before 2.0.304 said. Entra work accounts do not send that claim at all, and AgentWatch does not require it for an account that already exists.

SCIM v2 handles the full lifecycle without a login: users created, updated, suspended and de-provisioned from your identity provider, and group membership synchronized to teams. Each organization gets its own SCIM token, managed and revocable from the console, so a token leak is contained to one tenant.

De-provisioning is the half that matters most: a user disabled in your directory loses gateway access, and their API key stops authenticating.

Can employees get gateway access without ever logging in?

Section titled “Can employees get gateway access without ever logging in?”

Yes, and for most deployments that is the point. An employee’s gateway identity — the ak_ key their applications use — is independent of console sign-in. Employees can be created by SSO, SCIM, CSV import, the employee wizard, or automatically by a service key (see delegated identity), and a person can hold a governed gateway identity with no usable console password at all.

Local accounts work, with password policy, lockout after repeated failures, forced password change, and administrator-driven resets. Organizations commonly start local and add SSO later; existing users are matched by email address when a provider is introduced, so adopting SSO does not duplicate anyone.