Demo: static OAuth2 clients and identity directory API
Location: contrib/demo/static-clients/
Starts a single ahdapa instance with three OAuth2 clients pre-seeded from a
TOML file. The script verifies that the static clients are present and protected
from modification or deletion, then demonstrates the /api/identity/ directory
API used by SSSD and other machine-identity consumers.
What it shows
- TOML-seeded clients — clients declared in
clients.tomlare upserted into the CRDT at startup. They participate in normal gossip replication and database persistence. - Write protection — the admin API returns
403 ForbiddenforDELETEandPUTrequests targeting a client withsource = "static". kerberos_client_authtemplate client — thesssd-templateclient allows any IPA-enrolled host to authenticate using its Kerberos keytab without a per-machine secret. Thekerberos_principal_patternfield controls which principals match.- Identity directory API for SSSD — the
directory-readerclient holds thedirectory.readscope. The script obtains aclient_credentialstoken for it and exercises the two-phase user and group lookup API (/api/identity/users,/api/identity/groups,/api/identity/users/{id}/groups,/api/identity/groups/{id}/members). - Auth guards — the identity API returns
401 Unauthorizedwithout a token and403 Forbiddenfor a token that lacks thedirectory.readscope.
Prerequisites
ahdapabinary — the script looks in$PATHfirst (resolved to its full path), thentarget/release/ahdapa, thentarget/debug/ahdapa, and falls back tocargo buildif none is found.python3— for JSON parsing.- Port 8080 free.
Running
# Non-interactive: runs all steps and exits.
contrib/demo/static-clients/run.sh
# Interactive: same steps, then keeps the server running until Ctrl-C.
contrib/demo/static-clients/run.sh --interactive
What the script does
- Starts ahdapa with
ahdapa.toml(which sets[clients] file = "clients.toml"). The static clients are seeded into the CRDT before the first request is served. - Logs in as
alice(admin) to obtain a session cookie. - Lists all clients via
GET /api/admin/clientsand verifiessssd-template,dev-console, anddirectory-readerare present. - Attempts
DELETE /api/admin/clients/sssd-template— expects403 Forbidden. - Attempts
PUT /api/admin/clients/sssd-template— expects403 Forbidden. - Obtains a
client_credentialsaccess token fordirectory-readerusing HTTP Basic authentication (client_secret_basic). - Uses the token to call:
GET /api/identity/users?username=alice&exact=true— Phase 1 user lookup.GET /api/identity/users?username=bob&exact=true— Phase 1 user lookup.GET /api/identity/users/<alice-id>/groups— Phase 2 group membership.GET /api/identity/users/bob/groups— Phase 2 using short uid.GET /api/identity/groups?search=corp-staff&exact=true— Phase 1 group lookup.GET /api/identity/groups/corp-staff/members— Phase 2 group member list.
- Verifies auth guards: no token → 401, wrong scope → 403.
Example output (abbreviated)
Starting ahdapa (log: .../ahdapa.log)...
Server ready (pid 12345).
Logging in as alice (admin)...
── Listing clients ──────────────────────────────────────────────────────
[
{"client_id": "sssd-template", ...},
{"client_id": "dev-console", ...},
{"client_id": "directory-reader", ...}
]
✓ sssd-template is present
✓ dev-console is present
✓ directory-reader is present
── Testing delete protection ────────────────────────────────────────────
✓ DELETE /api/admin/clients/sssd-template → 403 (protected)
── Testing update protection ────────────────────────────────────────────
✓ PUT /api/admin/clients/sssd-template → 403 (protected)
── Identity API demo ────────────────────────────────────────────────────
Obtaining token for directory-reader (client_credentials)...
✓ Token obtained (512 chars)
── Phase 1 — user lookup: alice ─────────────────────────────────────────
[{"id": "alice@CORP.LOCAL", "username": "alice", ...}]
✓ alice resolved — id=alice@CORP.LOCAL
── Phase 1 — user lookup: bob ───────────────────────────────────────────
[{"id": "bob@CORP.LOCAL", "username": "bob", ...}]
✓ bob resolved — id=bob@CORP.LOCAL
── Phase 2 — groups for alice (using fully-qualified id) ────────────────
[{"id": "corp-staff", ...}, {"id": "editors", ...}]
✓ alice belongs to 2 group(s)
── Phase 2 — groups for bob (using short uid) ───────────────────────────
[{"id": "corp-staff", ...}]
── Phase 1 — group lookup: corp-staff ───────────────────────────────────
[{"id": "corp-staff", ...}]
✓ corp-staff resolved
── Phase 2 — members of corp-staff ─────────────────────────────────────
[{"username": "alice", ...}, {"username": "bob", ...}]
✓ corp-staff has 2 member(s)
── Auth guard: missing token → 401 ─────────────────────────────────────
✓ No token → 401 Unauthorized
── Auth guard: missing scope → 403 ──────────────────────────────────────
✓ Token without directory.read → 403 Forbidden
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Demo complete.
Static clients: seeded and read-only.
Identity API: two-phase user/group lookup verified.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Interactive exploration
With --interactive, the server stays running after verification. The
following endpoints are available for manual testing:
Token endpoint: http://127.0.0.1:8080/token
Identity API: http://127.0.0.1:8080/api/identity/users?username=alice&exact=true
Admin API: http://127.0.0.1:8080/api/admin/clients
To obtain a token for manual identity API calls:
TOKEN=$(curl -sf -X POST http://127.0.0.1:8080/token \
-u "directory-reader:dir-reader-secret" \
-d "grant_type=client_credentials&scope=directory.read" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
curl -s -H "Authorization: Bearer $TOKEN" \
"http://127.0.0.1:8080/api/identity/users?username=alice&exact=true" \
| python3 -m json.tool
Configuration notes
ahdapa.toml points to the clients file:
[clients]
file = "contrib/demo/static-clients/clients.toml"
Static clients
sssd-template — Kerberos machine authentication template. Any host with a
host/<hostname>@EXAMPLE.COM keytab can authenticate using
kerberos_client_auth. The HBAC service sssd-idp gates which hosts are
allowed (when FreeIPA HBAC is configured).
[[client]]
client_id = "sssd-template"
client_name = "SSSD Machine Template (Kerberos)"
token_endpoint_auth_method = "kerberos_client_auth"
scopes = ["openid"]
grant_types = ["client_credentials"]
kerberos_principal_pattern = "host/*@EXAMPLE.COM"
kerberos_hbac_service = "sssd-idp"
dev-console — Public authorization-code client for a local developer
console. No client secret; uses PKCE.
[[client]]
client_id = "dev-console"
client_name = "Developer Console"
token_endpoint_auth_method = "none"
redirect_uris = ["http://localhost:8081/callback"]
scopes = ["openid", "profile", "email"]
grant_types = ["authorization_code"]
directory-reader — Confidential client for SSSD-style machine identity
lookups. Uses HTTP Basic authentication (client_secret_basic). The
directory.read scope gates access to the /api/identity/ endpoints.
[[client]]
client_id = "directory-reader"
client_name = "Directory Reader (SSSD / machine identity lookup)"
token_endpoint_auth_method = "client_secret_basic"
client_secret = "dir-reader-secret"
scopes = ["directory.read"]
grant_types = ["client_credentials"]
Identity API two-phase lookup
The /api/identity/ API follows the same two-phase protocol as SSSD’s
id_provider = idp lookup:
Phase 1 — search:
GET /api/identity/users?username=<name>&exact=true
GET /api/identity/groups?search=<name>&exact=true
Returns a JSON array with basic attributes. The id field in each entry is
the fully-qualified identity handle (e.g. alice@CORP.LOCAL) used in Phase 2
calls.
Phase 2 — detail:
GET /api/identity/users/<id>/groups
GET /api/identity/groups/<id>/members
<id> may be the fully-qualified handle (URL-encoded) or the short uid. Both
forms are accepted.
All identity API endpoints require a bearer token with the directory.read
scope. Requests without a token return 401 Unauthorized; requests with a
token that lacks the scope return 403 Forbidden.
Users file
users.toml includes POSIX attributes so that the identity API can return
uid_number, gid_number, home_directory, login_shell, and gecos:
[[user]]
username = "alice"
password = "alice123"
uid_number = 10001
gid_number = 10001
home_directory = "/home/alice"
login_shell = "/bin/bash"
gecos = "Alice Atkinson,,,"
groups = ["corp-staff", "editors"]
How static clients work
On startup, AppState::new() reads the file pointed to by [clients] file,
converts each [[client]] entry to a ClientEntry with source = "static",
and calls crdt.clients.upsert() for each one using the current timestamp.
Because LWW (Last-Write-Wins) semantics apply, the file always wins on restart:
any database row for the same client_id from a prior run is overwritten.
After seeding, the CRDT is persisted to the database and gossiped to peers —
static clients participate in normal replication. The admin API rejects DELETE
and PUT requests for clients with source = "static", returning 403 Forbidden.
See also
- Identity API — full
/api/identity/endpoint reference. - Configuration —
[clients]—clients.filekey documentation. - FreeIPA Co-deployment — SSSD — production SSSD deployment with
kerberos_client_auth.