A modern, Kotlin-first SCIM 2.0 (RFC 7643/7644) SDK for the JVM with full Java interop.
- Complete RFC compliance: All SCIM 2.0 operations (CRUD, search, bulk, patch, discovery)
- Kotlin-first with Java interop: Kotlin DSLs, extension functions, coroutines, plus @Jvm annotations for Java users
- Hexagonal architecture: Framework-agnostic core, pluggable persistence and identity
- Spring Boot starter: Auto-configuration with sensible defaults
- Works without Spring: Use with any JVM HTTP framework
- OOTB persistence: JPA adapter with reference schemas for PostgreSQL, MySQL, Oracle, MSSQL, H2
- Observability: Metrics (Micrometer), tracing (OpenTelemetry), structured logging (MDC), event system
- Type-safe client: Fluent API with Kotlin DSLs for filters, patches, searches
- RFC 9457 ProblemDetail: Content-negotiated error responses
- Extensible: SPI for serialization, HTTP transport, identity, authorization, events
For detailed architecture diagrams (module dependencies, request flow, authentication, outbox pattern), see docs/architecture.md.
graph LR
CORE[scim2-sdk-core] --> SERVER[scim2-sdk-server]
CORE --> CLIENT[scim2-sdk-client]
SERVER --> SPRING[Spring Boot<br/>Auto-Configuration]
CLIENT --> HTTPCLIENT[Java HttpClient]
CLIENT --> OKHTTP[OkHttp]
style CORE fill:#4CAF50,color:#fff
style SERVER fill:#2196F3,color:#fff
style CLIENT fill:#FF9800,color:#fff
style SPRING fill:#9C27B0,color:#fff
Add the starter dependency:
<dependency>
<groupId>com.marcosbarbero</groupId>
<artifactId>scim2-sdk-spring-boot-starter</artifactId>
<version>${scim2-sdk.version}</version>
</dependency>Configure in application.yml:
scim:
base-path: /scim/v2
persistence:
enabled: true # enables JPA-backed storage
bulk:
enabled: true
filter:
enabled: trueThat's it! The starter auto-configures:
- SCIM endpoints at
/scim/v2/*(Users, Groups, Schemas, ResourceTypes, ServiceProviderConfig, Bulk) - JPA persistence with H2/PostgreSQL/MySQL/Oracle/MSSQL
- Jackson serialization with SCIM module
- Micrometer metrics (when on classpath)
- RFC 9457 ProblemDetail error responses
To provide your own ResourceHandler:
@Component
class CustomUserHandler(private val userService: UserService) : ResourceHandler<User> {
override val resourceType = User::class.java
override val endpoint = "/Users"
override fun create(resource: User, context: ScimRequestContext) = userService.create(resource)
override fun get(id: ResourceId, context: ScimRequestContext) = userService.findById(id.value)
// ... other methods
}@Component
public class CustomUserHandler implements ResourceHandler<User> {
private final UserService userService;
public CustomUserHandler(UserService userService) {
this.userService = userService;
}
@Override public Class<User> getResourceType() { return User.class; }
@Override public String getEndpoint() { return "/Users"; }
@Override
public User create(User resource, ScimRequestContext context) {
return userService.create(resource);
}
@Override
public User get(ResourceId id, ScimRequestContext context) {
return userService.findById(id.getValue());
}
// ... other methods
}See the Spring Boot full-stack sample for a production-like example with Keycloak, PostgreSQL, and React.
The SDK works with any JVM HTTP framework. See the plain Java full-stack sample for a production-like example using only the JDK HTTP server with PostgreSQL (plain JDBC) — no Spring Boot.
// Create client
val client = ScimClientBuilder()
.baseUrl("https://scim.example.com/scim/v2")
.transport(HttpClientTransport())
.serializer(JacksonScimSerializer())
.authentication(BearerTokenAuthentication("your-token"))
.build()
// Type-safe operations (recommended)
val response = client.createUser(user)
val user = client.getUser(id).value
val results = client.searchUsers("userName sw \"john\"")
client.patchUser(id, patch)
client.deleteUser(id)
// Group operations
val group = client.createGroup(Group(displayName = "Admins"))
val groups = client.searchGroups()
// Generic typed operations (reads endpoint from @ScimResource annotation)
val created = client.createResource(user) // detects /Users from annotation
val fetched = client.getResource<User>(id)
// Low-level operations (explicit endpoint and type)
val response = client.create("/Users", user, User::class)
val results = client.search("/Users", searchRequest, User::class)ScimClient client = new ScimClientBuilder()
.baseUrl("https://scim.example.com/scim/v2")
.transport(new HttpClientTransport())
.serializer(new JacksonScimSerializer())
.authentication(new BearerTokenAuthentication("your-token"))
.build();
// Type-safe operations via ScimClients utility class
ScimResponse<User> response = ScimClients.createUser(client, user);
User user = ScimClients.getUser(client, id).getValue();
ScimClients.deleteUser(client, id);
ScimClients.searchUsers(client, new SearchRequest());
// Group operations
ScimClients.createGroup(client, group);
ScimClients.getGroup(client, id);The SDK supports automatic outbound provisioning — when resources are created, updated, or deleted on your SCIM server, changes can be pushed to a target SCIM endpoint.
Set scim.client.base-url to enable automatic outbound provisioning:
scim:
client:
base-url: https://target-scim-server.example.com/scim/v2The SDK auto-configures:
SpringScimEventPublisher— bridges SCIM events to SpringApplicationEventScimOutboundProvisioningListener— reacts to events and pushes viaScimClient
Authentication to the target is handled by providing an AuthenticationStrategy bean (e.g., BearerTokenAuthentication, OAuth2 client credentials).
For reliable delivery, implement a custom ScimEventPublisher using the transactional outbox pattern with namastack-outbox.
Use ScimOutboundEventPublisher with a ScimOutboundTarget — no Spring needed:
ScimOutboundTarget target = new ScimClientOutboundTarget(scimClient);
ScimEventPublisher publisher = new ScimOutboundEventPublisher(target, handlers);
var dispatcher = new ScimEndpointDispatcher(
handlers, ..., eventPublisher = publisher, ...
);Chain multiple publishers with CompositeEventPublisher:
var publisher = new CompositeEventPublisher(outboundPublisher, auditPublisher);The SDK provides built-in observability:
- Metrics — Micrometer auto-configured with Prometheus, Grafana dashboard included
- Tracing — OpenTelemetry auto-configured when on classpath (spans, trace IDs, exception recording)
- Structured logging — SLF4J MDC with
scim.correlationId,scim.operation,scim.resourceType - Event correlation — all events carry the tracer's correlation ID
See the Observability Guide for configuration, metrics reference, and custom implementation examples.
management:
endpoints:
web:
exposure:
include: health,prometheus,metricsMetrics and tracing are automatically recorded for all SCIM operations. See the Spring Boot full-stack sample for a complete example with Prometheus + Grafana.
| Sample | Description |
|---|---|
| sample-fullstack-spring | Production-like: Spring Boot + PostgreSQL + Keycloak + React + bidirectional SCIM sync. docker compose up -d |
| sample-fullstack-plain | Production-like: JDK HttpServer + plain JDBC + PostgreSQL + Keycloak + React + outbound provisioning. No Spring. docker compose up -d |
| sample-server-spring | Minimal Spring Boot server (hosts E2E and contract tests) |
| Module | Description | Details |
|---|---|---|
scim2-sdk-core |
Domain model, filter/path parsing, PATCH engine, serialization SPI | README |
scim2-sdk-server |
SCIM Service Provider framework (ports + adapters) | README |
scim2-sdk-client |
Fluent client API with Kotlin DSLs | README |
scim2-sdk-client-httpclient |
Java HttpClient transport adapter | README |
scim2-sdk-client-okhttp |
OkHttp transport adapter | README |
scim2-sdk-spring-boot-autoconfigure |
Spring Boot auto-configuration | README |
scim2-sdk-spring-boot-starter |
Spring Boot starter (aggregates dependencies) | README |
scim2-sdk-test |
Test fixtures, contract tests, in-memory server | README |
scim2-sdk-bom |
Bill of Materials for version management | README |
All properties are optional with sensible defaults:
| Property | Default | Description |
|---|---|---|
scim.base-path |
/scim/v2 |
Base URL path for all SCIM endpoints |
scim.base-url |
(none) | Base URL for absolute meta.location URIs. When set, all responses include meta.location. Example: http://localhost:8080 |
scim.bulk.enabled |
true |
Enable Bulk Operations (RFC 7644 §3.7) — allows clients to batch multiple create/update/delete operations into a single HTTP request, reducing round-trips for large provisioning jobs |
scim.bulk.max-operations |
1000 |
Maximum number of individual operations allowed in a single bulk request |
scim.bulk.max-payload-size |
1048576 |
Maximum payload size (bytes) for bulk requests (default: 1 MB) |
scim.filter.enabled |
true |
Enable Filtering (RFC 7644 §3.4.2.2) — allows clients to query resources using expressions like userName eq "john" or emails[type eq "work"] |
scim.filter.max-results |
200 |
Maximum number of resources returned by a filtered query |
scim.etag.enabled |
true |
Enable ETags (RFC 7644 §3.14) — optimistic concurrency control using If-Match / If-None-Match headers to prevent lost updates when multiple clients modify the same resource |
scim.patch.enabled |
true |
Enable PATCH Operations (RFC 7644 §3.5.2) — partial resource updates (add/remove/replace individual attributes) without replacing the entire resource |
scim.sort.enabled |
false |
Enable Sorting (RFC 7644 §3.4.2.3) — allows clients to sort search results using sortBy and sortOrder query parameters |
scim.change-password.enabled |
false |
Enable Change Password (RFC 7644 §3.5.2.1) — indicates the service provider supports password changes via PATCH |
scim.pagination.default-page-size |
100 |
Default number of resources returned per page when the client doesn't specify count |
scim.pagination.max-page-size |
1000 |
Maximum allowed page size — the server caps the client's count parameter to this value |
scim.persistence.enabled |
false |
Enable the OOTB JPA persistence adapter — stores SCIM resources in a relational database |
scim.persistence.table-name |
scim_resources |
Database table name for SCIM resource storage |
scim.persistence.schema-name |
(none) | Database schema name (e.g., scim) — if set, the table is qualified as schema.table |
scim.persistence.auto-migrate |
false |
Run Flyway migration on startup to create the scim_resources table automatically |
scim.tracing.enabled |
true |
Enable OpenTelemetry tracing auto-configuration. Set to false to disable even when OpenTelemetry is on the classpath |
scim.client.base-url |
(none) | Base URL of the remote SCIM Service Provider — when set, auto-configures a ScimClient bean |
scim.client.connect-timeout |
10s |
TCP connection timeout for the SCIM client |
scim.client.read-timeout |
30s |
Read timeout for the SCIM client |
scim.idp.provider |
(none) | Identity Provider type: keycloak, okta, azure-ad, ping-federate, auth0 — auto-configures the corresponding IdentityResolver |
scim.idp.client-id |
(none) | Client ID for Keycloak client-role extraction |
scim.idp.namespace |
(none) | Auth0 custom namespace for role claims |
scim.idp.claims.subject |
sub |
JWT claim for subject |
scim.idp.claims.email |
email |
JWT claim for email |
scim.idp.claims.name |
name |
JWT claim for name |
scim.idp.claims.roles |
roles |
JWT claim for roles |
scim.idp.claims.groups |
groups |
JWT claim for groups |
scim.idp.claims.custom |
{} |
Additional custom claim mappings (key-value pairs) |
When scim.persistence.enabled=true, the JPA adapter stores SCIM resources as JSON in a single table:
CREATE TABLE scim_resources (
id VARCHAR(255) NOT NULL PRIMARY KEY,
resource_type VARCHAR(100) NOT NULL, -- "User", "Group", etc.
external_id VARCHAR(255),
display_name VARCHAR(500),
resource_json TEXT NOT NULL, -- Full SCIM resource as JSON
version BIGINT NOT NULL DEFAULT 1, -- ETag version
created TIMESTAMP NOT NULL,
last_modified TIMESTAMP NOT NULL
);This design stores any SCIM resource type in one table, discriminated by resource_type. The full resource is preserved as JSON in resource_json, enabling schema-less flexibility while maintaining queryable metadata columns.
Reference schemas for specific databases:
Opt-in auto-migration via Flyway: set scim.persistence.auto-migrate=true.
Three options for managing the SCIM schema:
-
Hibernate DDL (development): Set
spring.jpa.hibernate.ddl-auto=create-drop— used in samples. -
Manual migration: Copy the reference schema from
db/scim/schema-{database}.sqlinto your own Flyway/Liquibase migrations. -
Auto-migration (opt-in): Enable the built-in Flyway migration:
scim: persistence: enabled: true auto-migrate: true # runs Flyway migration for SCIM tables schema-name: my_schema # optional, defaults to the datasource default schema
This uses a separate Flyway history table (
scim_flyway_history) so it does not conflict with your application's migrations. Requiresorg.flywaydb:flyway-coreon the classpath.
- OpenAPI 3.1 Specification — machine-readable API spec, usable with Swagger UI, Redoc, or any OpenAPI tooling
- HTTP Examples — curl command examples for quick reference
- Java 25+
- Maven 3.9+
- Spring Boot 4.0+ (for starter, optional)