Salta ai contenuti

Enterprise identity: single sign-on and SCIM

Questi contenuti non sono ancora disponibili nella tua lingua.

An organization signs its people in through its own identity provider (OIDC or SAML 2.0) and provisions them with SCIM 2.0. Decisions and their reasons are in ADR 0055; account security, which SSO sessions are judged by, is in ADR 0053 and its guide.

Everything below is administered by the organization’s owners and administrators, from the web app (Organization → Single sign-on) or the public API (enterprise_identity tag, browser sessions only; see the API reference).

  1. Add the domain: a registrable domain such as example.com (not a subdomain, not a public or shared-hosting suffix). It covers its subdomains: ada@rome.example.com is on example.com.
  2. Publish the TXT record GuestCraft shows, for example _guestcraft-verification.example.com. TXT "guestcraft-verification=3f9c…".
  3. Verify. A domain belongs to one organization at a time: if another organization proves it, the answer is domain.unavailable; it must release the domain first, or let it lapse.

Keep the record published. GuestCraft looks it up again every 24 hours (an hour later when the lookup itself failed). If it is missing, the domain is failing: the page shows when it will lapse, and SSO keeps working for 7 days. Then the domain lapses: SSO for its addresses stops and the sessions it began end. Publish the record and verify again to restore it. Releasing a domain has the same effect at once.

On Vercel the daily check runs from the project’s cron (/api/internal/domains/check, with CRON_SECRET); every runtime also checks at most once an hour while it serves requests.

OIDC SAML 2.0
You enter Issuer, client ID, client secret IdP entity ID, SSO URL (HTTP-Redirect), signing certificate(s) in PEM
You give the identity provider Redirect URI: https://<instance>/api/auth/sso/callback/<sso_id> Entity ID and metadata: https://<instance>/api/auth/sso/saml2/sp/metadata?providerId=<sso_id>; ACS: https://<instance>/api/auth/sso/saml2/sp/acs/<sso_id> (HTTP-POST)
GuestCraft always uses Authorization code with PKCE (S256), a nonce, scopes openid email profile, client_secret_basic or client_secret_post Signed assertions, InResponseTo, audience, recipient and validity checks
  • The issuer must publish its discovery document at <issuer>/.well-known/openid-configuration over HTTPS on a public host. An identity provider on a private network must be listed in GUESTCRAFT_TRUSTED_ORIGINS.
  • The issuer or entity ID of a connection never changes; create a new connection to move to another identity provider. The client secret is never shown again; leave it empty when editing to keep it.
  • The client secret is stored sealed with the instance’s ENCRYPTION_KEYS, for this organization and connection, and is opened only for the code exchange of a sign-in. Without ENCRYPTION_KEYS, OIDC connections cannot be created or changed (server.unavailable). When you rotate the keys, the credential maintenance (reencrypt) rewrites these secrets too; it also seals any secret an earlier pre-release version stored in plaintext.
  • Domains served: the proved domains whose addresses sign in through this connection. A connection is active while it is enabled and serves at least one proved domain.
  • Default workspace roles: the roles a person gets at their first sign-in, besides becoming a member of the organization, each at all the workspace’s properties or at the properties you select, as a member’s (roles and property access). SSO never makes anyone an administrator or owner.
  • Trust the identity provider’s second factor (off by default): list the values that prove one: OIDC amr values (mfa, otp, hwk, swk, …) or acr values; SAML AuthnContextClassRef values. A sign-in carrying one of them meets this organization’s “second factor” requirement; it never meets a “passkey” requirement, and never another organization’s.

On the sign-in page, Continue with single sign-on asks for the e-mail address. An address on a proved domain served by an active connection goes to the identity provider; any other address gets the same answer as an unknown one, so nobody can list your organizations or providers. Every answer takes at least 300 ms. An address without single sign-on also starts a decoy sign-in that runs a real one’s statements, OIDC or SAML, and removes what it stored before answering. It is never sent and nobody can use it. An overloaded server that stores slowly, or fails to store, treats both alike. ADR 0055 says how, and what is measured.

  • First sign-in: the person’s account is created and they become a member with the default roles. If an account with that address already exists with a password, its owner is asked to sign in with that password (and second factor) and confirm the link; nothing is linked otherwise.
  • Linking: the confirmation page names the organization and connection that ask before the person signs in. The link is checked again when confirmed: a connection disabled, a domain released or a person deprovisioned meanwhile refuses it.
  • Refused sign-ins show a reason: the address is not on the connection’s domains, SCIM deactivated the person, an administrator removed a person from the organization (invite them again; SSO never restores a membership an administrator ended), or the address belongs to an account that cannot be linked.
  • Applications: a person an OAuth application or MCP client sends to the sign-in page can sign in with single sign-on too. The server verifies the application’s signed request first and returns the browser to it at this instance’s authorization endpoint after the identity provider signs them in; the consent page then asks as always. A request that expired meanwhile is refused with “This request from an application has expired”: start again from the application.
  • Identity-provider-initiated SAML (an unsolicited response) is not supported.
  • A SAML sign-in completes only in the browser that started it (GuestCraft sets a short-lived __Host-guestcraft.saml_flow cookie when it starts): the identity provider’s answer posted from another browser is refused with “This sign-in was not started in this browser”. The answer must name this connection’s ACS URL as its Destination, and be within its validity window on GuestCraft’s clock, with 60 seconds of tolerance: keep the identity provider’s clock accurate.
  1. Create a SCIM token (Organization → Single sign-on → Provisioning): it is shown once, lives 1–730 days and can be revoked. Up to 5 tokens are live at a time. Choose its permissions: read users, create/change/deactivate users, read groups, create/change/delete groups. Microsoft Entra ID and Okta provision users and groups, so they need all four (the form proposes them); a narrower token gets 403 on every request its permissions do not cover. They never change: create another token for another choice.
  2. Give the identity provider the SCIM base URL https://<instance>/api/auth/scim/v2 and the token as a bearer token (Authorization: Bearer gc_scim_…).
  3. Map the groups it provisions to workspace roles (the page lists them a hundred at a time), each at all the workspace’s properties or at selected ones. A person in several groups gets, per workspace, the role with the most permissions, at the properties of every group that gives that role (all properties if one of them does).
SCIM GuestCraft
POST /Users The person becomes a member; an account of the same address that belongs only to this organization is linked, any other is refused (409 uniqueness); an address on a domain another organization proves is refused (400 invalidValue), also when changed later
Group membership The mapped workspace roles at their properties; changing a mapping’s properties changes the memberships it granted; roles an administrator granted by hand are never changed; every change applies at once, also in an archived workspace, so its restore finds the memberships as the identity provider grants them
active: false Deprovisioned: membership and workspace roles, every browser and device session, pending device codes, remembered browsers, the API keys and organization tokens the person created in the organization, and the OAuth applications they authorized there with every token of them end, in the same transaction; a person reactivated later must authorize an application again
active: true A member again, with their groups’ roles
DELETE /Users/{id} Deprovisioned like active: false; the account is kept, never erased, and the link’s tombstone lets the same externalId come back
Owner Never deactivated or deleted through SCIM (400 mutability): transfer the ownership first. SCIM still updates the name and address of an owner it created

Filters accept eq only, pages hold at most 100 resources, and requests are limited to 600 a minute per client address, 600 a minute per token and 600 a minute per organization, each counted on its own (429 with Retry-After).

The organization’s owner always keeps their own sign-in: SCIM cannot deactivate them, a lapsed domain or a broken connection does not touch password sign-in, and an instance administrator can issue them a reset link from the command line (instance administrators). The same reset gives a person SSO created a password of their own.

domain.added, domain.verified, domain.verification_failed, domain.verification_failing, domain.verification_recovered, domain.lapsed, domain.released, sso.connection_created, sso.connection_updated, sso.connection_deleted, sso.signed_in (with the assurance and its claim), sso.member_provisioned, sso.account_linked, scim.token_created, scim.token_revoked, scim.token_refused, scim.group_access_updated, scim.access_reconciled, scim.user_deprovisioned (with what ended). None holds a token, secret or assertion.

Enforcing SSO for every member, encrypted SAML assertions, signed AuthnRequests, Single Logout, IdP metadata upload and private-key client authentication.