Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.toml are upserted into the CRDT at startup. They participate in normal gossip replication and database persistence.
  • Write protection — the admin API returns 403 Forbidden for DELETE and PUT requests targeting a client with source = "static".
  • kerberos_client_auth template client — the sssd-template client allows any IPA-enrolled host to authenticate using its Kerberos keytab without a per-machine secret. The kerberos_principal_pattern field controls which principals match.
  • Identity directory API for SSSD — the directory-reader client holds the directory.read scope. The script obtains a client_credentials token 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 Unauthorized without a token and 403 Forbidden for a token that lacks the directory.read scope.

Prerequisites

  • ahdapa binary — the script looks in $PATH first (resolved to its full path), then target/release/ahdapa, then target/debug/ahdapa, and falls back to cargo build if 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

  1. 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.
  2. Logs in as alice (admin) to obtain a session cookie.
  3. Lists all clients via GET /api/admin/clients and verifies sssd-template, dev-console, and directory-reader are present.
  4. Attempts DELETE /api/admin/clients/sssd-template — expects 403 Forbidden.
  5. Attempts PUT /api/admin/clients/sssd-template — expects 403 Forbidden.
  6. Obtains a client_credentials access token for directory-reader using HTTP Basic authentication (client_secret_basic).
  7. 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.
  8. 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