Configuration Reference
All options are set under config :ex_scim.
Core
| Key | Default | Description |
|---|---|---|
:base_url | "http://localhost:4000" | Base URL for SCIM endpoints. Resource URLs are built as <base_url>/scim/v2/..., so the routes are expected under /scim/v2. Falls back to SCIM_BASE_URL env var. |
:storage_strategy | ExScim.Storage.EtsStorage | Module implementing ExScim.Storage.Adapter |
:auth_provider_adapter | required | Module implementing ExScim.Auth.AuthProvider.Adapter |
:authorization_adapter | ExScim.Authorization.DefaultPolicy | Module implementing ExScim.Authorization.Adapter |
Resource Mapping
| Key | Default | Description |
|---|---|---|
:user_resource_mapper | ExScim.Users.Mapper.DefaultMapper | Module implementing ExScim.Users.Mapper.Adapter |
:group_resource_mapper | ExScim.Groups.Mapper.DefaultMapper | Module implementing ExScim.Groups.Mapper.Adapter |
Ecto Storage (ex_scim_ecto)
| Key | Default | Description |
|---|---|---|
:storage_repo | required | Your Ecto Repo module |
:user_model | required | Ecto schema module or {Schema, opts} tuple |
:group_model | required | Same format as :user_model |
When using the {Schema, opts} tuple form, available sub-options are:
:preload- list of associations to preload (default[]):lookup_key- primary key field (default:id):filter_mapping- map of SCIM attribute paths to DB columns (default%{}):tenant_key- column used for multi-tenant scoping (defaultnil):field_mapping- map of domain fields to{db_field, to_storage_fn, from_storage_fn}tuples for value transformation (default%{})
config :ex_scim,
storage_strategy: ExScimEcto.StorageAdapter,
storage_repo: MyApp.Repo,
user_model:
{MyApp.Accounts.User,
preload: [:roles],
lookup_key: :uuid,
filter_mapping: %{"emails.value" => :email},
tenant_key: :organization_id,
field_mapping: %{
active: {:status,
fn true -> "active"; false -> "inactive" end,
fn "active" -> true; _ -> false end}
}},
group_model:
{MyApp.Groups.Group,
preload: [:members],
tenant_key: :organization_id}Capabilities
Reported in the ServiceProviderConfig discovery endpoint.
| Key | Default |
|---|---|
:patch_supported | false |
:bulk_supported | true |
:bulk_max_operations | 1000 |
:bulk_max_payload_size | 1_048_576 |
:filter_supported | false |
:filter_max_results | 200 |
:sort_supported | false |
:change_password_supported | false |
:etag_supported | false |
Discovery
| Key | Default | Description |
|---|---|---|
:documentation_uri | nil | URI included in ServiceProviderConfig |
:authentication_schemes | [] | List of scheme maps per RFC 7643 |
:resource_types | User + Group | List of resource type maps |
:schema_modules | Built-in User, EnterpriseUser, Group | Schema definition modules |
Lifecycle & Tenancy
| Key | Default | Description |
|---|---|---|
:lifecycle_adapter | nil | Module implementing ExScim.Lifecycle.Adapter |
:tenant_resolver | nil | Module implementing ExScim.Tenant.Resolver |
Authorization Scopes
Scopes are strings in the ExScim.Scope struct's :scopes list, populated by your AuthProvider.Adapter when validating a token or credentials. Every endpoint and every bulk operation asks the configured authorization policy whether the scope may perform an action (:read, :create, :update, :delete) on a resource (:users, :groups, :me, :schemas, :resource_types, :service_provider_config).
The default policy, ExScim.Authorization.DefaultPolicy, expects the scim:* scopes below. To use the scopes your identity platform issues instead, see Custom policies.
Standard scopes
| Scope | Endpoints | Actions |
|---|---|---|
scim:read | /Users, /Groups, /Schemas, /ServiceProviderConfig | GET (list, show, search) |
scim:create | /Users, /Groups, /Bulk (POST operations) | POST |
scim:update | /Users, /Groups, /Bulk (PUT/PATCH operations) | PUT, PATCH |
scim:delete | /Users, /Groups, /Bulk (DELETE operations) | DELETE |
/Me scopes
| Scope | Action |
|---|---|
scim:me:read | GET /Me |
scim:me:create | POST /Me |
scim:me:update | PUT /Me, PATCH /Me |
scim:me:delete | DELETE /Me |
Bulk operations
For /Bulk, scope is enforced per individual operation rather than on the request as a whole. A caller with only scim:create may submit a bulk request containing POST operations; any PUT, PATCH, or DELETE operations in the same payload will return a 403 operation result (and count toward failOnErrors).
Example scope lists
# Read-only client
scopes: ["scim:read"]
# Provisioning client - can create and update users, but not delete them
scopes: ["scim:read", "scim:create", "scim:update"]
# Full-access admin client
scopes: ["scim:read", "scim:create", "scim:update", "scim:delete"]
# Self-service user (Me endpoint only)
scopes: ["scim:me:read", "scim:me:update"]/ResourceTypes requires no scope.
Custom policies
Implement ExScim.Authorization.Adapter to authorize against other scope names, authorize Users and Groups separately, or decide based on token claims in scope.metadata:
defmodule MyApp.ScimPolicy do
@behaviour ExScim.Authorization.Adapter
alias ExScim.Authorization.DefaultPolicy
alias ExScim.Scope
@impl true
def authorize(scope, resource, :read) when resource in [:users, :groups],
do: require_scope(scope, "directory.#{resource}.read")
def authorize(scope, resource, _action) when resource in [:users, :groups],
do: require_scope(scope, "directory.#{resource}.write")
def authorize(scope, resource, action), do: DefaultPolicy.authorize(scope, resource, action)
defp require_scope(scope, required) do
if Scope.has_scope?(scope, required),
do: :ok,
else: {:error, {:missing_scopes, [required]}}
end
endconfig :ex_scim, authorization_adapter: MyApp.ScimPolicyDenials return 403. {:error, {:missing_scopes, scopes}} lists the missing scopes in the error detail; any other reason yields a generic detail. Cross-resource search (POST /.search) requires :read on both :users and :groups.
Multi-Tenancy
Multi-tenancy is opt-in. When no tenant_resolver is configured (or scope.tenant_id is nil), the system operates in single-tenant mode with no isolation applied.
To enable it, wire up three pieces:
- Tenant resolver - implement
ExScim.Tenant.Resolverto extract a tenant identifier from the request (header, subdomain, path, etc.). - Phoenix plug - add
ExScimPhoenix.Plugs.ScimTenantto your SCIM pipeline afterScimAuth. - Ecto tenant key - set
:tenant_keyon your model tuples so queries are scoped and creates inject the tenant ID.
# 1. Resolver
defmodule MyApp.TenantResolver do
@behaviour ExScim.Tenant.Resolver
@impl true
def resolve_tenant(conn, _scope) do
case Plug.Conn.get_req_header(conn, "x-tenant-id") do
[tenant_id] -> {:ok, tenant_id}
_ -> {:error, :missing_tenant}
end
end
end
# 2. Config
config :ex_scim,
tenant_resolver: MyApp.TenantResolver,
user_model: {MyApp.Accounts.User, tenant_key: :organization_id},
group_model: {MyApp.Groups.Group, tenant_key: :organization_id}
# 3. Pipeline
# Add: plug ExScimPhoenix.Plugs.ScimTenantThe resolver can optionally implement tenant_scim_base_url/1 to generate tenant-specific resource location URLs (e.g., https://acme.example.com/scim/v2).
Custom Adapters
Storage Adapter
Implement the ExScim.Storage.Adapter behaviour to use a custom data store:
defmodule MyApp.CustomStorage do
@behaviour ExScim.Storage.Adapter
def get_user(id), do: # your implementation
def create_user(user_data), do: # your implementation
# ... other callbacks
endResource Mapper
Implement ExScim.Users.Mapper.Adapter or ExScim.Groups.Mapper.Adapter to control how domain structs map to SCIM JSON:
defmodule MyApp.UserMapper do
use ExScim.Users.Mapper.Adapter
@impl true
def from_scim(scim_data, _caller) do
{:ok,
%MyApp.User{
username: scim_data["userName"],
email: get_primary_email(scim_data["emails"])
}}
end
@impl true
def to_scim(%MyApp.User{} = user, _caller, opts \\ []) do
{:ok,
%{
"schemas" => ["urn:ietf:params:scim:schemas:core:2.0:User"],
"id" => user.id,
"userName" => user.username,
"emails" => format_emails(user.email),
"meta" => format_meta(user, opts)
}}
end
endopts carries :location, the resource URL. Passing opts to format_meta/2 sets meta.location, which is also returned as the Location header.
Endpoints
All endpoints are served under the scope you configure (typically /scim/v2).
Users
| Method | Path | Description | RFC |
|---|---|---|---|
GET | /Users | List with filtering, sorting, pagination | RFC 7644 Section 3.4.2 |
POST | /Users | Create | RFC 7644 Section 3.3 |
GET | /Users/{id} | Fetch by ID | RFC 7644 Section 3.4.1 |
PUT | /Users/{id} | Replace | RFC 7644 Section 3.5.1 |
PATCH | /Users/{id} | Partial update (JSON Patch) | RFC 7644 Section 3.5.2 |
DELETE | /Users/{id} | Delete | RFC 7644 Section 3.6 |
Groups and Me follow the same pattern. See RFC 7644 Section 3.11 for Me endpoint details.
Other
| Method | Path | Description | RFC |
|---|---|---|---|
POST | /.search | Cross-resource search | RFC 7644 Section 3.4.3 |
POST | /Bulk | Bulk operations | RFC 7644 Section 3.7 |
GET | /ServiceProviderConfig | Server capabilities | RFC 7644 Section 4 |
GET | /ResourceTypes | Supported resource types | RFC 7644 Section 4 |
GET | /Schemas | Schema definitions | RFC 7644 Section 4 |