A small, local SCIM 2.0 server built with Python and Flask. It simulates an HRIS "worker" directory so an Identity Provider (IdP, e.g. Okta) can provision and import users. The IdP reaches your machine through an ngrok tunnel and authenticates with a Bearer token in the Authorization header.
For development and testing only. Data is held in memory and lost on restart, and the token is hardcoded.
| File | Purpose |
|---|---|
app.py |
Flask SCIM server (port 8000) with Bearer token check |
seed_db.py |
Posts users from scim_sample_users.json to the running server |
scim_sample_users.json |
Sample SCIM users used for seeding |
hr_import_workers.csv |
Sample HR worker data (CSV) for reference/imports |
workers_db.py |
Standalone sample worker dataset (not imported by app.py) |
- Python 3.9+
- ngrok with an authtoken configured (
ngrok config add-authtoken <TOKEN>) - An IdP tenant (e.g. Okta) with a SCIM 2.0 app integration
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install flaskOn macOS/Linux: source .venv/bin/activate.
-
Start the server (listens on
http://localhost:8000):python app.py
-
In a second terminal, start ngrok:
ngrok http 8000Copy the HTTPS forwarding URL, e.g.
https://abcd-1234.ngrok-free.app. -
(Optional) Seed sample users:
python seed_db.py
The server expects header-based authentication on every request:
Authorization: Bearer test-token-12345
The token is defined as BEARER_TOKEN in app.py. Change it before sharing the tunnel URL, and use the same value in the IdP.
In your SCIM app's Provisioning → Integration settings:
| Field | Value |
|---|---|
| SCIM connector base URL | https://<your-ngrok-host>/scim/v2 |
| Unique identifier field for users | userName |
| Authentication Mode | HTTP Header |
| Authorization (Bearer token) | test-token-12345 (or your own token) |
Supported actions: Import New Users and Profile Updates, Push New Users, Push Profile Updates. Use Test Connector Configuration to verify.
Base path: /scim/v2
| Method | Path | Description |
|---|---|---|
| GET | /Users |
List users (SCIM ListResponse) |
| GET | /Users/<id> |
Get a user by id |
| POST | /Users |
Create a user (id generated as hr-worker-<timestamp>) |
| PUT | /Users/<id> |
Replace a user |
| PATCH | /Users/<id> |
Update active via replace operations |
| GET | /Groups |
Returns an empty list |
Missing/invalid token returns 401.
curl -H "Authorization: Bearer test-token-12345" http://localhost:8000/scim/v2/Users
curl -H "Authorization: Bearer test-token-12345" https://<your-ngrok-host>/scim/v2/UsersThe Flask console prints each inbound IdP request, which is useful for debugging.
- ngrok free URLs change on each restart; update the IdP base URL when that happens.
debug=Trueandhost=0.0.0.0are set inapp.py; do not expose this beyond local testing.