Configuration & Guides

Copy Markdown View Source

Configuration Reference

All options are set under config :ex_scim.

Core

KeyDefaultDescription
: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_strategyExScim.Storage.EtsStorageModule implementing ExScim.Storage.Adapter
:auth_provider_adapterrequiredModule implementing ExScim.Auth.AuthProvider.Adapter
:authorization_adapterExScim.Authorization.DefaultPolicyModule implementing ExScim.Authorization.Adapter

Resource Mapping

KeyDefaultDescription
:user_resource_mapperExScim.Users.Mapper.DefaultMapperModule implementing ExScim.Users.Mapper.Adapter
:group_resource_mapperExScim.Groups.Mapper.DefaultMapperModule implementing ExScim.Groups.Mapper.Adapter

Ecto Storage (ex_scim_ecto)

KeyDefaultDescription
:storage_reporequiredYour Ecto Repo module
:user_modelrequiredEcto schema module or {Schema, opts} tuple
:group_modelrequiredSame 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 (default nil)
  • :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.

KeyDefault
:patch_supportedfalse
:bulk_supportedtrue
:bulk_max_operations1000
:bulk_max_payload_size1_048_576
:filter_supportedfalse
:filter_max_results200
:sort_supportedfalse
:change_password_supportedfalse
:etag_supportedfalse

Discovery

KeyDefaultDescription
:documentation_urinilURI included in ServiceProviderConfig
:authentication_schemes[]List of scheme maps per RFC 7643
:resource_typesUser + GroupList of resource type maps
:schema_modulesBuilt-in User, EnterpriseUser, GroupSchema definition modules

Lifecycle & Tenancy

KeyDefaultDescription
:lifecycle_adapternilModule implementing ExScim.Lifecycle.Adapter
:tenant_resolvernilModule 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

ScopeEndpointsActions
scim:read/Users, /Groups, /Schemas, /ServiceProviderConfigGET (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

ScopeAction
scim:me:readGET /Me
scim:me:createPOST /Me
scim:me:updatePUT /Me, PATCH /Me
scim:me:deleteDELETE /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
end
config :ex_scim, authorization_adapter: MyApp.ScimPolicy

Denials 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:

  1. Tenant resolver - implement ExScim.Tenant.Resolver to extract a tenant identifier from the request (header, subdomain, path, etc.).
  2. Phoenix plug - add ExScimPhoenix.Plugs.ScimTenant to your SCIM pipeline after ScimAuth.
  3. Ecto tenant key - set :tenant_key on 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.ScimTenant

The 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
end

Resource 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
end

opts 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

MethodPathDescriptionRFC
GET/UsersList with filtering, sorting, paginationRFC 7644 Section 3.4.2
POST/UsersCreateRFC 7644 Section 3.3
GET/Users/{id}Fetch by IDRFC 7644 Section 3.4.1
PUT/Users/{id}ReplaceRFC 7644 Section 3.5.1
PATCH/Users/{id}Partial update (JSON Patch)RFC 7644 Section 3.5.2
DELETE/Users/{id}DeleteRFC 7644 Section 3.6

Groups and Me follow the same pattern. See RFC 7644 Section 3.11 for Me endpoint details.

Other

MethodPathDescriptionRFC
POST/.searchCross-resource searchRFC 7644 Section 3.4.3
POST/BulkBulk operationsRFC 7644 Section 3.7
GET/ServiceProviderConfigServer capabilitiesRFC 7644 Section 4
GET/ResourceTypesSupported resource typesRFC 7644 Section 4
GET/SchemasSchema definitionsRFC 7644 Section 4