Skip to content
Anlyon
Esc
↑↓navigate↵open⌘Jpreview
On this page

Single sign-on and SCIM

Sign people in to an Anlyon workspace through your OIDC or SAML identity provider, require it for everyone, and let your directory add, re-role and remove members over SCIM 2.0.

A workspace can connect one identity provider, over OpenID Connect (OIDC) or SAML, and one directory over SCIM 2.0. Single sign-on decides how people sign in. SCIM decides who is a member and with which role.

The people who can approve what your agents do in production should be the people your company says they are. With single sign-on, someone who leaves loses access when your identity provider disables them, and with SCIM their membership, their API keys and their apps’ grants go too, without anyone remembering to clean up in Anlyon.

Who can set this up

What you do Who can do it
Read the identity provider and the directory settings An admin
Connect, verify or remove an identity provider, change its default role, create the SCIM token, map groups to roles An admin
Turn the requirement on or off, or remove a provider while it is required An admin with emergency access

Everything is managed in the console under Settings → Single sign-on and Settings → Directory sync. The routes on this page are console routes: they take a signed-in console session (the better-auth.session_token cookie), and an API key cannot call them. They are not in the API reference. The SCIM endpoints your directory calls take the SCIM token instead.

Single sign-on and SCIM are Team features: they are on the Team and Enterprise plans, and on every plan while paid plans are not on sale. Creating or changing one on another plan answers 403 PLAN_FEATURE_REQUIRED. A setup made earlier keeps working on any plan, and you can always turn the requirement off or delete the setup. See which plans have the Team features.

Connect an identity provider

Register the provider with the email domains it speaks for: 1 to 20 of them. A domain another workspace has verified cannot be claimed again (409). defaultRole is admin, member, approver or viewer, and member when you leave it out.

curl -X POST https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/sso \
  -H "Cookie: better-auth.session_token=$SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "protocol": "oidc",
    "domains": ["example.com"],
    "defaultRole": "member",
    "oidc": {
      "issuer": "https://login.example-idp.com",
      "clientId": "YOUR_CLIENT_ID",
      "clientSecret": "YOUR_CLIENT_SECRET"
    }
  }'
curl -X POST https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/sso \
  -H "Cookie: better-auth.session_token=$SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "protocol": "saml",
    "domains": ["example.com"],
    "defaultRole": "member",
    "saml": {
      "entityId": "https://idp.example.com/metadata",
      "ssoUrl": "https://idp.example.com/sso/saml",
      "certificate": "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----",
      "emailAttribute": "email",
      "nameAttribute": "displayName"
    }
  }'
  • OIDC. Anlyon reads the issuer’s discovery document (/.well-known/openid-configuration), which must be served over HTTPS from a public address. Sign-in uses the authorization code flow with PKCE and the scopes openid, email and profile. The client secret is encrypted at rest and never returned.
  • SAML. entityId is the identity provider’s entity ID and must be a URL. ssoUrl is its single sign-on URL (HTTP-Redirect binding). certificate is its signing certificate, PEM or bare base64. Assertions must be signed. emailAttribute and nameAttribute name the assertion attributes that carry the email and the display name, email and displayName unless you say otherwise. Sign-in started by the identity provider is off: people start at Anlyon’s sign-in page.

The response, and the console, give what to enter in the identity provider, under serviceProvider:

Protocol Field What it is
OIDC oidcRedirectUri The redirect URI to allow in the OIDC app: https://api.anlyon.com/api/auth/sso/callback/<providerId>
SAML samlAcsUrl The Assertion Consumer Service URL. Okta calls it the Single sign-on URL. Entra ID calls it the Reply URL
SAML samlEntityId Anlyon’s entity ID, which is also its metadata URL. Okta calls it the Audience URI (SP Entity ID). Entra ID calls it the Identifier (Entity ID)
SAML samlMetadataUrl Anlyon’s SAML metadata, for a provider that imports it

Copy them from the response rather than building them by hand: <providerId> is generated when you register. The response, which GET /api/v2/workspaces/{workspaceId}/sso also returns (or null when there is none), looks like this for OIDC:

{
  "success": true,
  "data": {
    "workspaceId": "2f8a1f72-7cf9-4fef-95e8-df9de5f2f5d0",
    "providerId": "anlyon-4f9c2a7b1e6d3c80",
    "protocol": "oidc",
    "issuer": "https://login.example-idp.com",
    "domains": ["example.com"],
    "domainsVerified": false,
    "domainsVerifiedAt": null,
    "verificationRecords": [
      { "domain": "example.com", "name": "_anlyon-sso.example.com", "type": "TXT", "value": "anlyon-sso-verification=q8ZtVb3nK1xR7mWp2LcJ9fYd4HsE6aGu" }
    ],
    "enforced": false,
    "defaultRole": "member",
    "serviceProvider": {
      "oidcRedirectUri": "https://api.anlyon.com/api/auth/sso/callback/anlyon-4f9c2a7b1e6d3c80"
    },
    "createdAt": "2026-10-27T10:00:00.000Z",
    "updatedAt": "2026-10-27T10:00:00.000Z"
  }
}

A workspace has one identity provider. To replace it, remove it (DELETE /api/v2/workspaces/{workspaceId}/sso) and register the new one.

Verify your domains

Sign-in through the provider is refused until every domain is verified. For each domain, publish the TXT record listed in verificationRecords:

_anlyon-sso.example.com.  TXT  "anlyon-sso-verification=<token>"

Then check them:

curl -X POST https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/sso/verify-domains \
  -H "Cookie: better-auth.session_token=$SESSION_TOKEN"

Until every record is found, the request answers 422 with DOMAIN_NOT_VERIFIED, and the message names the domains still missing. DNS changes can take a while to appear, so try again later. Once they are found, domainsVerified is true.

How people sign in

A person chooses single sign-on on the sign-in page and enters their work email. Anlyon sends them to the provider that owns that email’s domain.

  • A new person joins with the default role. The first sign-in through the provider by someone with an email on a verified domain adds them to the workspace with the connection’s defaultRole: admin, member, approver or viewer. It never changes the role of someone already a member. Change the default with PATCH /api/v2/workspaces/{workspaceId}/sso and { "defaultRole": "viewer" }. When the workspace has a SCIM directory, the directory alone decides membership, and a sign-in adds nobody.
  • Accounts are created only on verified domains. The provider can create an Anlyon account only for an email on the workspace’s verified domains. Any other email answers 403 with SSO_DOMAIN_NOT_VERIFIED. An existing account is linked only when its email is on a verified domain.

Require single sign-on

An admin with emergency access can require the provider for everyone else in the workspace:

curl -X PATCH https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/sso \
  -H "Cookie: better-auth.session_token=$SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "enforced": true }'

The domains must be verified first, or the request answers 422. Once it is on:

  • A password, social, email-link or two-factor sign-in by a member or admin without emergency access is refused with SSO_REQUIRED.
  • Every console request to this workspace must come from a session the workspace’s provider created. Turning the requirement on ends no session, because a session can also serve other workspaces. The next request from a session that did not come from the provider is refused at this workspace only, and an open live view closes on its next heartbeat.
  • Connecting an MCP client or an OAuth app to the workspace needs a session signed in through the provider. Otherwise the consent answers 403 with SSO_REQUIRED.
  • Deciding an approval from Slack needs a current, unexpired sign-in through the provider.
  • API keys and OAuth grants already issued keep working. They are workspace credentials governed by their scopes, not sign-ins.

Admins with emergency access are exempt. The one or two admins with emergency access keep password and two-factor sign-in and are never refused by these checks. If the identity provider is down or misconfigured, they sign in and turn the requirement off or replace the provider. Give these accounts strong two-factor authentication.

SCIM directory sync

SCIM lets your identity provider add people to the workspace, change their role through groups, and remove them. Anlyon implements SCIM 2.0 at:

https://api.anlyon.com/api/v2/scim

The console shows the same URL as baseUrl under Settings → Directory sync.

Create the SCIM token

Create the workspace’s SCIM token in the console, or:

curl -X POST https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/scim/token \
  -H "Cookie: better-auth.session_token=$SESSION_TOKEN"

It answers 201 with the token and the directory settings:

{
  "success": true,
  "data": {
    "token": "anlyon_scim_8Jq2Xw4Lr7Tz1Vb9Nc3Mk6Hf5Gd0Sa2PqR7uY1eW3oI",
    "settings": {
      "connected": true,
      "baseUrl": "https://api.anlyon.com/api/v2/scim",
      "tokenPrefix": "anlyon_scim_8Jq2Xw",
      "defaultRole": "member",
      "createdAt": "2026-10-27T10:00:00.000Z",
      "rotatedAt": null,
      "lastUsedAt": null,
      "roleMappings": []
    }
  }
}

GET /api/v2/workspaces/{workspaceId}/scim returns the same settings without the token. The token starts with anlyon_scim_ and is shown once: Anlyon keeps only its hash. The directory sends it as Authorization: Bearer <token>, and the token also names the workspace. Calling the same route again rotates it, and the old token stops working at once. DELETE /api/v2/workspaces/{workspaceId}/scim/token disconnects the directory: the token stops working, and the members it provisioned stay, with the role they had.

The directory adds only people whose email is on a domain verified for this workspace’s single sign-on, so verify your domains before you connect it. Until a domain is verified, the directory can update and deactivate existing members but cannot add anyone.

What the directory can call

Resource Methods
/Users GET (list, with an eq filter on userName, externalId or id), POST
/Users/{id} GET, PUT, PATCH, DELETE
/Groups GET (list, with an eq filter on displayName, externalId or id), POST
/Groups/{id} GET, PUT, PATCH, DELETE
/ServiceProviderConfig, /ResourceTypes, /Schemas GET

Responses use application/scim+json and the SCIM error shape. PATCH accepts the PatchOp shapes Okta and Entra ID send: add, replace and remove, in any letter case, a path-less replace whose value is an object of attributes, booleans sent as the strings "True" and "False", and group member removal by a members[value eq "..."] path or by a value list. Bulk operations, sorting and ETags are not supported. A filter other than one eq comparison answers 400 with invalidFilter. Pages hold at most 200 resources.

A User needs an email: the primary entry of emails, or a userName that is an email address. A SCIM User is a membership of this workspace, not an account. Creating one creates an Anlyon account with no password, or links the existing account for that email. Either way the address must be on a domain verified for this workspace’s single sign-on, unless the person is a member already; any other address answers 400 with invalidValue, whether or not an Anlyon account exists for it. A userName, externalId or displayName longer than 512 characters answers 400.

Map directory groups to roles

Map directory groups to workspace roles in the console, or:

curl -X PUT https://api.anlyon.com/api/v2/workspaces/$WORKSPACE_ID/scim/role-mappings \
  -H "Cookie: better-auth.session_token=$SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mappings": [
      { "groupName": "anlyon-admins", "role": "admin", "priority": 10 },
      { "groupName": "anlyon-approvers", "role": "approver", "priority": 5 }
    ]
  }'

A person gets the role of the highest-priority mapping among the groups they are in, matched by group name ignoring case. A tie goes to the mapping listed first. Someone in no mapped group gets the directory’s default role, member unless you change it (PATCH /api/v2/workspaces/{workspaceId}/scim with { "defaultRole": "viewer" }). Any role can be mapped. The call replaces every mapping: send up to 100, each with a groupName of up to 256 characters and an optional whole-number priority from -1000 to 1000 (0 when left out). Changing the mappings or the default re-applies every provisioned person’s role.

What happens when the directory removes someone

When the directory sets a user’s active to false, or deletes the user, that person leaves this workspace only:

  • their membership is removed;
  • the API keys they created in this workspace are revoked (kept, not deleted, so their history stays readable);
  • their apps’ OAuth grants to this workspace end.

Their account, their other workspaces and their sessions elsewhere are untouched. A console session they hold is refused at this workspace from its next request. Setting active back to true restores the membership with the role their groups give. Removing a member in the console revokes their keys and grants the same way.

Admins with emergency access are out of the directory’s reach. SCIM cannot deactivate, delete or re-role them, and group mappings skip them: the request answers 400 with mutability. Nor can SCIM remove or demote the workspace’s last admin. Remove their emergency access in the console first.

Set up the directory in Okta or Entra ID

Every provider asks for the same two values:

  • The base URL above. Okta calls it the SCIM connector base URL. Entra ID calls it the Tenant URL.
  • The token, sent as a bearer token in the Authorization header. Entra ID calls it the Secret Token.

Okta and Entra ID look a user up by userName or externalId, and a group by displayName, before they create one. Anlyon answers those lookups.

Find these changes in the audit log

Every change on this page is an audit record in the access category, written with the change: sso.connection_created, sso.connection_deleted, sso.domains_verified, sso.settings_updated, scim.token_created, scim.token_revoked, scim.role_mappings_replaced, scim.settings_updated, and for people member.provisioned, member.deactivated, member.reactivated, member.deprovisioned, member.profile_updated and member.role_changed. A key revoked because its creator lost access to the workspace is an api_key.revoked record whose data.reason is member_deprovisioned (the directory), member_removed (an admin) or member_left (they left). See Audit records.

Errors

Status and code When What to do
401 No console session, or an API key sent to these routes. Sign in to the console as an admin.
403 Your role is not admin, or the change needs emergency access and you do not hold it. Ask an admin with emergency access.
403 PLAN_FEATURE_REQUIRED Creating or changing single sign-on or SCIM on a plan without the Team features. See which plans have them.
409 A domain is already verified by another workspace. Use domains only this workspace owns.
422 DOMAIN_NOT_VERIFIED Verifying before the TXT records are visible, or requiring single sign-on before the domains are verified. Publish the records, wait for DNS, and try again.
403 SSO_DOMAIN_NOT_VERIFIED A sign-in through the provider with an email outside the verified domains. Add and verify that domain, or sign in with a work email.
SSO_REQUIRED A password or other sign-in, or an OAuth consent, by someone without emergency access while single sign-on is required. Sign in through the provider.
SCIM 400 invalidFilter, invalidValue, mutability An unsupported filter, an email outside the verified domains, or a change to an admin with emergency access or the last admin. Fix the request in the directory, or change the member in the console.

Was this page helpful?