A production-ready SCIM 2.0 server for testing and developing Azure Entra ID provisioning integrations.
This server implements the System for Cross-domain Identity Management (SCIM) 2.0 specification, enabling secure user and group provisioning from Azure Entra ID to your applications.
Features:
- Full SCIM 2.0 compliance (users, groups, resource types, service provider config)
- PostgreSQL persistence with async SQLAlchemy
- Dual authentication: Bearer Token (testing) + OAuth2 OIDC (production)
- Azure Entra ID compatible endpoints and error responses
- Docker Compose for local testing
- Comprehensive audit logging with structlog
- Enterprise layered architecture (services, repositories, domain models)
src/itl_scim/
├── core/ # Base classes, protocols, exceptions, config
├── domain/ # Pure domain objects (User, Group)
├── schemas/ # Pydantic request/response models
├── models/ # SQLAlchemy ORM models (XRow naming)
├── services/ # Business logic (UserService, GroupService)
├── repositories/ # Data access (UserRepository, GroupRepository)
├── middleware/ # Auth, logging, error handling
├── api/v1/ # FastAPI routes (thin wrappers)
└── infrastructure/ # External adapters (DB client, auth)
- Python 3.12+
- PostgreSQL 14+
- Docker & Docker Compose (optional, for local testing)
cd d:\repos\ITL.Scim.Server
# Start PostgreSQL and run migrations
docker compose up -d
# Install dependencies
pip install -e ".[dev]"
# Run migrations
alembic upgrade head
# Start server
uvicorn itl_scim.main:app --reload --port 8000Server will be available at http://localhost:8000
Create .env in the project root:
# Database
DATABASE_URL=postgresql+asyncpg://scim:scim@localhost:5432/scim
# Auth
SCIM_BEARER_TOKEN=test-token-12345
OAUTH2_ISSUER_URL=https://login.microsoftonline.com/{tenant-id}/v2.0
# Server
DEBUG=true
LOG_LEVEL=INFOGET /scim/v2/Users— List all users (with filtering, sorting, pagination)GET /scim/v2/Users/{id}— Get single userPOST /scim/v2/Users— Create userPUT /scim/v2/Users/{id}— Replace userPATCH /scim/v2/Users/{id}— Partial update userDELETE /scim/v2/Users/{id}— Delete user
GET /scim/v2/Groups— List all groupsGET /scim/v2/Groups/{id}— Get single groupPOST /scim/v2/Groups— Create groupPUT /scim/v2/Groups/{id}— Replace groupPATCH /scim/v2/Groups/{id}— Partial update groupDELETE /scim/v2/Groups/{id}— Delete group
GET /scim/v2/ServiceProviderConfig— SCIM compliance infoGET /scim/v2/ResourceTypes— Available resource typesGET /scim/v2/Schemas— SCIM schemas
- Open Azure Portal
- Go to Azure Entra ID → App registrations → New registration
- Register your app:
- Name:
SCIM Server Test - Supported account types: Single tenant
- Redirect URI: (leave blank for now)
- Name:
- In your app registration, go to Certificates & secrets
- New client secret → Copy the
Value(this is your secret)
- Go to Expose an API
- Click Set next to Application ID URI
- Accept the default URI (e.g.,
api://12345678-...) - Save
Your tenant admin must grant consent. Construct this URL (replace placeholders):
https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
?client_id={application-id}
&scope=api://{application-id}/.default
&response_type=code
&redirect_uri=https://localhost:8000/auth/callback
Or use Azure Portal API permissions → Grant admin consent for [org].
- Go to Enterprise applications → New application → Create your own application
- Name it
SCIM Test, choose Integrate any other application - Go to the new app → Provisioning
- Provisioning Mode:
Automatic - Tenant URL:
http://localhost:8000/scim/v2(or your server URL) - Secret Token: Your Bearer token from
.env(SCIM_BEARER_TOKEN) - Click Test Connection
- Once connected, configure Attribute Mappings and Save
- In the Entra ID app provisioning config, go to Users and groups
- Assign users or groups you want to provision
- Go back to Provisioning and click Start provisioning
# Set variables
TOKEN="test-token-12345"
BASE_URL="http://localhost:8000/scim/v2"
# List users
curl -H "Authorization: Bearer $TOKEN" "$BASE_URL/Users"
# Create user
curl -X POST "$BASE_URL/Users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/scim+json" \
-d '{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "alice.smith@contoso.com",
"name": {
"givenName": "Alice",
"familyName": "Smith"
},
"emails": [{"value": "alice.smith@contoso.com", "primary": true}],
"active": true
}'
# Get user by ID
curl -H "Authorization: Bearer $TOKEN" "$BASE_URL/Users/123e4567-e89b-12d3-a456-426614174000"
# Delete user
curl -X DELETE "$BASE_URL/Users/123e4567-e89b-12d3-a456-426614174000" \
-H "Authorization: Bearer $TOKEN"- Import SCIM 2.0 Postman collection
- Set environment variables:
base_url=http://localhost:8000token= Your Bearer token
- Run requests against
/scim/v2endpoints
For development and testing, Bearer token authentication is simple:
curl -H "Authorization: Bearer test-token-12345" http://localhost:8000/scim/v2/UsersFor production with Azure Entra ID:
- Set
OAUTH2_ISSUER_URLin.env - Server validates ID token signature and claims
- Uses tenant-scoped OIDC discovery to fetch public keys
alembic revision --autogenerate -m "describe what changed"alembic upgrade headalembic downgrade -1pytest # All tests
pytest tests/test_users.py -v # Specific file
pytest -k "test_create" --cov # Specific test with coveragedocker build -t itl-scim:latest .
docker run -p 8000:8000 \
-e DATABASE_URL="postgresql+asyncpg://user:pass@db:5432/scim" \
-e SCIM_BEARER_TOKEN="your-secret-token" \
itl-scim:latestSee docs/KUBERNETES.md for Helm chart and deployment instructions.
Logs are structured using structlog and JSON-formatted for easy parsing:
# Watch logs
docker compose logs -f scim-server
# Sample log output:
{
"event": "user_created",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"user_name": "alice@contoso.com",
"timestamp": "2026-08-11T14:30:45.123Z",
"level": "info"
}error="invalid syntax for unique constraint in definition" or
"Connection refused"
Solution:
- Verify PostgreSQL is running:
docker compose ps - Check DATABASE_URL in
.envis correct - Check logs:
docker compose logs db
error="Unauthorized - Invalid authentication credentials"
Solution:
- Ensure
Authorization: Bearer <TOKEN>header is present - Token must match
SCIM_BEARER_TOKENfrom.env - Check server logs:
docker compose logs scim-server
error="No resource with ID found" or HTTP 404
Solution:
- Verify the resource UUID exists:
GET /scim/v2/Users - Check exact UUID spelling and case (UUIDs are case-insensitive but Postgres is strict)
- Use valid UUID format:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- Enable detailed logging:
LOG_LEVEL=DEBUGin.env - Watch server logs:
docker compose logs -f scim-server - Check audit events in Azure Entra ID → Provisioning logs
- Test single user creation via API before enabling bulk sync
- Increase connection pool:
DATABASE_POOL_SIZE=20in.env - Enable query caching:
CACHE_TTL_SECONDS=300 - Batch operations: Use
/scim/v2/Users?count=100for pagination
# Connect to PostgreSQL
docker compose exec db psql -U scim -d scim
# Common queries
SELECT id, user_name, created_at FROM users LIMIT 10;
SELECT * FROM groups WHERE name LIKE 'Sales%';ITL.Scim.Server/
├── src/itl_scim/ # Main package
│ ├── __init__.py
│ ├── main.py # FastAPI app initialization
│ ├── core/
│ │ ├── config.py # Settings (Pydantic)
│ │ ├── exceptions.py # Custom exceptions
│ │ ├── base.py # Base classes & ABCs
│ │ └── __init__.py
│ ├── domain/
│ │ ├── user.py # User domain entity
│ │ ├── group.py # Group domain entity
│ │ └── __init__.py
│ ├── schemas/
│ │ ├── user_requests.py # Pydantic input models
│ │ ├── user_responses.py # Pydantic output models
│ │ ├── group_requests.py
│ │ ├── group_responses.py
│ │ └── __init__.py
│ ├── models/
│ │ ├── user_row.py # SQLAlchemy User ORM model
│ │ ├── group_row.py # SQLAlchemy Group ORM model
│ │ └── __init__.py
│ ├── services/
│ │ ├── user_service.py # User business logic
│ │ ├── group_service.py # Group business logic
│ │ └── __init__.py
│ ├── repositories/
│ │ ├── user_repository.py # User data access
│ │ ├── group_repository.py # Group data access
│ │ └── __init__.py
│ ├── api/v1/
│ │ ├── users.py # FastAPI user routes
│ │ ├── groups.py # FastAPI group routes
│ │ ├── service_provider.py # SCIM config endpoints
│ │ └── __init__.py
│ ├── middleware/
│ │ ├── auth.py # Authentication
│ │ ├── logging.py # Request/response logging
│ │ └── __init__.py
│ ├── infrastructure/
│ │ ├── db_client.py # Database session
│ │ └── __init__.py
│ └── __init__.py
├── alembic/ # Database migrations
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
├── tests/ # Pytest test suite
│ ├── test_users.py
│ ├── test_groups.py
│ ├── test_auth.py
│ └── __init__.py
├── docs/ # Documentation
│ ├── KUBERNETES.md
│ ├── API.md
│ └── ARCHITECTURE.md
├── docker-compose.yml # Local dev stack
├── Dockerfile # Container image
├── .env.example # Environment template
├── pyproject.toml # Package config
├── README.md # This file
└── .gitignore
MIT — See LICENSE file
For issues or questions, please open a GitHub issue or contact platform@itlusions.com.