Skip to main content

SSO and user provisioning

Two questions that are easy to conflate and have different answers: how a person signs in, and how their user record comes to exist. Sign-in is federation into SpendOne's identity provider. Provisioning is one of three paths you choose from.

Neither applies to an integration itself: a machine caller authenticates with a bearer token and never signs in interactively. See API Basics.

How sign-in works​

SpendOne authenticates through OpenID Connect, brokered by Zitadel. Your own identity provider federates into Zitadel, not into SpendOne, so there is no SSO configuration screen inside the product and no SAML endpoint on the API.

Entra ID, Google Workspace, Okta, Ping, OneLogin and ADFS are all connected the same way: as an upstream provider on the Zitadel organization that represents your tenant, alongside generic OIDC (a Shibboleth bridge, for example). Ask API support at tech@spendone.com to set that up; it is not self-service today.

Two identities exist for every person, and keeping them apart is what makes the rest of this page make sense:

  • The Zitadel user is the authentication identity: the federated IdP account, or a local password or passkey.
  • The SpendOne user is the authorization record in your tenant: roles, permissions, org unit, cost centers, everything in Core Concepts.

SpendOne is the source of truth for who is allowed and what they may do. Zitadel is a thin authentication broker.

The two tenant modes​

A tenant runs in one of two modes, set by SpendOne rather than in the dashboard, with a fail-secure default of closed:

ssoclosed
Creating a user in the portalWrites the SpendOne record only. No account is minted, no mail is sentCreates the Zitadel user and sends the invite/setup mail
First login, no matching recordThe record is created on the spot, so no valid IdP user is locked outRejected with 403 and an audit entry
First login, record existsThe IdP identity is linked to it once, by email, and the link is auditedNot applicable; the record already carries its identity
Deactivated SpendOne userBlocked, even when the IdP login still succeedsSame

Three consequences an integration should build for:

  • Users are matched on a permanent identifier, never on email. The token's subject is stored on the record at first link, and everything afterwards keys on that. An email change mutates the same record; it can neither break the link nor hijack another one.
  • Roles assigned in SpendOne stay authoritative. A login never reconciles roles from token claims, so it cannot downgrade someone your integration promoted.
  • Deactivation is the offboarding switch, and it holds regardless of what the IdP still allows.

The employee_id claim​

Where the Zitadel SSO connection carries an employee_id custom claim, it is written to the user's external_id on every login. The IdP wins when the claim is present, and an absent or empty claim never clears a stored value.

That matters if your integration also writes external_id (the CSV import does): the two writers converge on the IdP's value once the person logs in. A value that already belongs to a different user is skipped rather than applied, because a login never fails over an identifier collision, and the skip is audited.

How user records get created​

Three paths, in descending order of how much you should prefer them.

1. SCIM 2.0​

The right path when your IdP is the system of record for joiners, movers and leavers. Your IdP pushes changes and SpendOne applies them: no file exchange, no scheduled job.

The endpoints are the standard ones, under a per-tenant base URL:

/scim/v2/{tenant}/ServiceProviderConfig
/scim/v2/{tenant}/ResourceTypes
/scim/v2/{tenant}/Schemas
/scim/v2/{tenant}/Users GET, POST
/scim/v2/{tenant}/Users/{id} GET, PUT, PATCH, DELETE

What the service supports, as ServiceProviderConfig reports it: PATCH yes, filtering yes (capped at 100 results), bulk no, sort no, ETag no, change-password no. Filtering is deliberately narrow: userName eq "…" and externalId eq "…", the two filters an IdP connector actually sends. Anything else is rejected rather than silently widened.

Tokens are managed separately from user sessions and separately from the REST bearer token:

GET /api/v1/scim-tokens
POST /api/v1/scim-tokens
DELETE /api/v1/scim-tokens/{id}

A SCIM token carries the scim_ prefix and is returned once, at creation. Point your IdP's SCIM connector at the base URL above with one of them.

Attribute mapping. userName or the primary email becomes the user's email, lowercased (Entra sends the UPN in userName, which is handled). externalId becomes external_id. name.givenName and name.familyName become the names. From the enterprise extension:

SCIM attributeResolves to
departmentAn organizational unit, matched by name. No match, or an ambiguous one, is flagged rather than guessed
costCenterA cost center
manager.valueThe reporting manager, looked up by email. A cycle is refused
organizationStored verbatim, not resolved

Resolution failures flag the user rather than failing the sync, so a department your org tree does not have yet does not block the person from being provisioned. Where a resolved manager differs from the matched unit's leader or deputy, that is recorded as an advisory: the two are different relationships, and SCIM only speaks to the first.

2. CSV import​

Best for the initial load, and for a directory you export from rather than one that pushes. See Data import via blob storage, or POST /api/v1/admin/dataimport/kinds/users/upload for a one-off file.

3. Direct API calls​

For corrections and small changes: POST /api/v1/users, PUT /api/v1/users/{id}, PATCH /api/v1/users/{id}/deactivate. See Push users and permissions.

Choosing​

Your situationUse
IdP owns joiners, movers and leavers, and can pushSCIM
HR system can export on a schedule, but not pushCSV via blob storage
Neither; a person or a script maintains the directoryREST API
Going live from nothingCSV for the load, then SCIM or REST for the steady state
  • Customer Onboarding for where identity sits in the go-live sequence. It is the first blocking milestone, because email matching is what links imported users to their IdP identity.
  • Roles and Permissions for what to provision alongside the person.
  • API Basics for bearer tokens and the error model.