rustauth-scim 0.3.0

SCIM support for RustAuth.
Documentation

rustauth-scim

Server-side SCIM 2.0 provisioning plugin for RustAuth.

What It Is

rustauth-scim lets external identity providers provision users and groups into RustAuth through SCIM 2.0. It is server-side only and intentionally omits browser SDKs, dashboard UI, and Better Auth Infrastructure self-service features.

What It Provides

  • Provider connection management and bearer-token authentication.
  • SCIM Users, Groups, Bulk, search, metadata, schema, and resource type routes.
  • Filtering, sorting, pagination, projections, weak ETags, and SCIM error responses.
  • Organization-scoped group provisioning backed by RustAuth organization teams.
  • Token storage modes: hashed (default), plain, encrypted, and custom transforms.
  • Schema contributions for SCIM providers, user profiles, and group profiles.

Quick Start

Enable the scim feature on the umbrella rustauth crate (or depend on rustauth-scim directly):

[dependencies]
rustauth = { version = "0.2.0", features = ["scim"] }
use rustauth::RustAuth;
use rustauth::scim::{scim, ScimOptions, ScimTokenStorage};

let auth = RustAuth::builder()
    .secret("secret-a-at-least-32-chars-long!!")
    .base_url("https://app.example.com/api/auth")
    .plugin(
        scim(
            ScimOptions::default()
                .token_storage(ScimTokenStorage::Hashed),
        ),
    )
    .build()?;
# let _ = auth;
# Ok::<(), Box<dyn std::error::Error>>(())

Run your adapter migration flow after adding the plugin. SCIM clients call routes under your auth base URL:

https://app.example.com/api/auth/scim/v2

Your HTTP integration must allow GET, POST, PUT, PATCH, and DELETE.

Provider connections (providerId)

RustAuth follows the same storage model as Better Auth: providerId is globally unique. One id equals one SCIM connection (one bearer token row), not “one organization.” Organization scope is optional metadata on that connection:

  • Omit organizationId to provision users linked to the provider account only.
  • Set organizationId so list/create/update/delete only touch members of that org.

If you need separate tokens for the same vendor (different orgs, environments, or apps), assign different provider ids (okta-prod, okta-eu, entra-hr). Regenerating a token for the same providerId replaces the stored secret on the existing row (upsert) instead of deleting the connection first.

We keep global uniqueness rather than a composite (providerId, organizationId) key because upstream does the same: it is a deliberate “one integration id” design, not a missing composite index. A composite key would be an RustAuth-only extension and would complicate management APIs that key off providerId alone.

Provider Tokens

Create or rotate a provider token with an authenticated RustAuth session:

POST /scim/generate-token

Example body:

{
  "providerId": "okta",
  "organizationId": "org_123"
}

organizationId is optional. When present, the organization plugin must be installed and the session user must have an allowed organization role.

When omitted, set provider_ownership.enabled to true in ScimOptions so only the owning session user can create, list, rotate, or delete that global connection.

Route Summary

Management routes use regular RustAuth JSON errors:

  • POST /scim/generate-token
  • GET /scim/list-provider-connections
  • GET /scim/get-provider-connection?providerId=...
  • POST /scim/delete-provider-connection

SCIM protocol routes use RFC 7644-compatible SCIM errors and application/scim+json responses:

  • /scim/v2/Users
  • /scim/v2/Users/:userId
  • /scim/v2/Users/.search
  • /scim/v2/Groups
  • /scim/v2/Groups/:groupId
  • /scim/v2/Groups/.search
  • /scim/v2/.search
  • /scim/v2/Bulk
  • /scim/v2/ServiceProviderConfig
  • /scim/v2/Schemas
  • /scim/v2/ResourceTypes

GET /scim/v2/Me returns SCIM 501; provider-scoped tokens are not end-user aliases.

Identity validation

User provisioning resolves a canonical email from userName and optional emails. Both must produce a valid email address before create, replace, bulk, or patch mutations persist changes. Empty userName values are rejected.

Filters and metadata

Surface Filter handling
GET /Users?filter=userName eq "a@b.com" Pushed to SQL on users.email (Better Auth parity).
GET /Users?filter=... (anything else) Parsed with the RFC-style parser, evaluated in memory on each provider-scoped User resource (includes extension profile fields).
POST /Users/.search, POST /Groups/.search Same parser; Groups always filter in memory.
Invalid syntax 400 with scimType: invalidFilter.

Use rustauth_scim::filters::list_user_filter_uses_database_pushdown in integrator code to detect the SQL-backed form.

Better Auth upstream only implements userName eq for user list. RustAuth keeps that path for compatibility and adds in-memory filtering so enterprise attributes (urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:department, title, etc.) and operators like co work without a second query language. Large directories should prefer userName eq or pagination (startIndex / count, capped by ServiceProviderConfig.filter.maxResults).

ServiceProviderConfig advertises bulk, sort, weak etag, and extended filter support that this crate implements server-side.

Bulk operations

Better Auth 1.6.9 does not implement SCIM Bulk (bulk.supported: false in upstream metadata). RustAuth implements POST /scim/v2/Bulk with two modes:

Mode ScimOptions Behavior
Independent (default) bulk_mode: Independent RFC-style sequential ops; each mutation runs in its own DB transaction; failOnErrors stops the batch.
Atomic bulk_mode: Atomic All mutating ops share one adapter transaction; the first error rolls back earlier ops in the same request (prior successes are reported as 412 in the bulk response).

Atomic requires a database adapter that advertises native transactions (AdapterCapabilities::supports_transactions). The in-memory test adapter does not qualify; use SQLite/Postgres/MySQL adapters in production.

Bulk operations do not evaluate per-operation If-Match headers (unlike direct PUT/PATCH/DELETE routes).

Deprovision mode

ScimDeprovisionMode::UnlinkAccount (default) removes only the current provider account and SCIM profile (and org membership when the provider is org-scoped). The user row remains while other linked accounts exist (password credentials, other IdPs, or additional SCIM providers).

ScimDeprovisionMode::DeleteUser removes the RustAuth user when they have no other linked accounts besides the current SCIM provider; otherwise it unlinks the current provider only, so email-linked identities are not destroyed by one IdP.

Token storage

ScimOptions::default() stores generated SCIM base tokens as SHA-256 hashes. Use ScimTokenStorage::Plain only for local development or when you manage storage security yourself. Provider token rotation updates the existing scim_providers row instead of deleting and recreating it.

Migrating from Better Auth plain tokens

If you previously stored SCIM tokens in plain text (Better Auth default or RustAuth ScimTokenStorage::Plain), switching to hashed default storage invalidates existing bearer secrets. Regenerate every provider connection via POST /scim/generate-token (or re-seed default_scim with new secrets) after upgrade. There is no in-place migration of raw tokens to hashes.

Audit hooks

Optional audit_event: ScimAuditEventResolver mirrors the SSO plugin pattern: structured log lines plus an async callback for token generation, user provision/deprovision, bulk failures, and atomic bulk rollbacks.

Storage Notes

The plugin contributes:

  • scim_providers
  • scim_user_profiles
  • scim_group_profiles

The in-memory adapter works for tests and local runtime usage, but durable SCIM deployments should use a database adapter. Redis and Valkey crates in this workspace are rate-limit/secondary-storage integrations, not SCIM identity stores.

MongoDB (future)

RustAuth does not ship a MongoDB DbAdapter today. SCIM is tested against SQLite, PostgreSQL, and MySQL via rustauth-sqlx / rustauth-tokio-postgres / rustauth-deadpool-postgres (see tests/scim/db_adapters.rs). The root docker-compose.yml includes a mongodb service for local infra experiments only. Upstream Better Auth has @better-auth/mongo-adapter as a separate npm package; a future RustAuth Mongo adapter would live outside rustauth-scim and would need its own SCIM contract tests before this crate claims support. Telemetry may label connections as mongodb when detected, but that is not storage for SCIM tables.

Status

Experimental beta. The server provisioning surface is implemented and covered, but API details, schema shape, and parity choices may change before stable release.

Better Auth compatibility

Server-side SCIM provisioning is aligned with Better Auth 1.6.9 where it matters; RustAuth is not a line-by-line port. For route-level parity, test counts, differences, and gaps, see UPSTREAM.md.

Links