Registering an OAuth2 Client
Before a client application can request tokens from ahdapa it must be registered. ahdapa provides three registration paths:
- Static clients file — a TOML file declared via
[clients] file = ...inahdapa.toml. Clients are seeded into the CRDT at startup, gossiped to peers, and protected from API modification. Suitable for pre-provisioned infrastructure clients (e.g. SSSD machine templates, CI pipelines). See Configuration §[clients] for the file format. - Admin API (
POST /api/admin/clients) — operator-controlled registration with full access to every client field. - Dynamic Client Registration (
POST /register, RFC 7591) — automated or self-service registration for applications that manage their own credentials.
All client state lives in the database and is replicated across cluster nodes via CRDT gossip regardless of how the client was registered.
Admin API registration
The admin API gives an operator full control over every client field, including fields that dynamic registration does not expose (Kerberos authentication, grant-type restrictions, mTLS certificate binding, and pairwise subject types).
The admin API requires an authenticated admin session. See
Authentication Methods and the RBAC configuration
in Configuration for how
to grant the clients:write permission.
Create a client
curl -s -X POST https://idp.example.com/api/admin/clients \
-b session.jar \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Web App",
"redirect_uris": ["https://app.example.com/callback"],
"scopes": ["openid", "profile", "email", "offline_access"],
"token_endpoint_auth_method": "client_secret_basic",
"client_secret": "change-me-to-a-random-secret"
}'
Successful response — 201 Created:
{
"client_id": "3f8a2c1e-7d4b-4e9f-a0c1-2b3d4e5f6a7b",
"client_name": "My Web App",
"redirect_uris": ["https://app.example.com/callback"],
"scopes": ["openid", "profile", "email", "offline_access"],
"token_endpoint_auth_method": "client_secret_basic",
"source": "dynamic"
}
The client_id is a UUID generated by the server. Store it alongside
the client_secret you supplied (the server does not echo secrets on
subsequent GET calls).
Request body fields
| Field | Type | Required | Description |
|---|---|---|---|
client_name | string | yes | Human-readable display name. |
redirect_uris | array of strings | yes (for interactive flows) | Allowed redirect URIs. Must be https:// except for loopback addresses (127.0.0.1, [::1]). |
scopes | array of strings | no | Scopes the client is permitted to request. Defaults to []. |
token_endpoint_auth_method | string | no | Authentication method at the token endpoint. Default: private_key_jwt. See table below. |
client_secret | string | if client_secret_basic or client_secret_post | Shared secret. Stored in the database; choose a high-entropy random value. |
jwks_uri | string | if private_key_jwt | URL of the client’s JWKS endpoint. The server fetches the public key from here to verify signed assertions. |
tls_client_certificate | string (PEM) | if tls_client_auth or self_signed_tls_client_auth | PEM-encoded client certificate. The server extracts and stores the SHA-256 thumbprint; the full PEM is not persisted. |
tls_client_auth_subject_dn | string | no | Expected Subject DN for tls_client_auth. Not enforced by the server today; stored for informational purposes. |
grant_types | array of strings | no | If present, restricts which grant types the client may use. When absent, all grant types are permitted. Valid values: authorization_code, refresh_token, client_credentials, urn:ietf:params:oauth:grant-type:device_code, urn:ietf:params:oauth:grant-type:token-exchange, urn:ietf:params:oauth:grant-type:jwt-bearer. |
subject_type | string | no | "public" (default) or "pairwise". Pairwise derives a per-client pseudonymous sub from the underlying identity. |
kerberos_principal | string | if kerberos_client_auth (single-machine) | Exact Kerberos service principal. Format: service/host@REALM. |
kerberos_principal_pattern | string | if kerberos_client_auth (template) | Glob pattern matching multiple Kerberos principals. The * wildcard matches any sequence of non-@ characters. At most three * wildcards. Format: service/*@REALM. |
kerberos_hbac_service | string | no | FreeIPA HBAC service name. When set, the server enforces HBAC rules before issuing a token to a kerberos_client_auth client. |
id_token_signed_response_alg | string | no | JWS algorithm to use for signing both the ID token and the access token for this client (OIDC Dynamic Registration §3.1). Overrides the server-wide jwt_signing_algorithm. Allowed values: RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512, EdDSA, ML-DSA-44, ML-DSA-65, ML-DSA-87. When absent, the server default is used. |
Supported token endpoint authentication methods
token_endpoint_auth_method | Required fields | Notes |
|---|---|---|
private_key_jwt | jwks_uri | Default. RFC 7523 §2.2 signed JWT assertion. Requires no shared secret. |
client_secret_basic | client_secret | HTTP Basic authentication header. |
client_secret_post | client_secret | client_id + client_secret in the POST body. |
client_secret_jwt | client_secret | HMAC-signed JWT assertion (HS256/HS384/HS512). |
tls_client_auth | tls_client_certificate | RFC 8705 mutual TLS with a CA-issued certificate. |
self_signed_tls_client_auth | tls_client_certificate | RFC 8705 mutual TLS with a self-signed certificate. |
kerberos_client_auth | kerberos_principal or kerberos_principal_pattern | Ahdapa extension. Requires [ipa] gssapi = true. Admin API only — not available via DCR. |
none | (none) | Public client. No credential check. PKCE is required for all flows. |
Update a client
PUT /api/admin/clients/{client_id} replaces the registration with
the supplied body. The same field constraints apply. Fields that are
omitted revert to their defaults; the exception is client_secret and
tls_client_certificate_thumbprint, which are preserved from the
existing record when the update body omits them.
curl -s -X PUT https://idp.example.com/api/admin/clients/3f8a2c1e-7d4b-4e9f-a0c1-2b3d4e5f6a7b \
-b session.jar \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Web App (v2)",
"redirect_uris": [
"https://app.example.com/callback",
"https://app.example.com/callback2"
],
"scopes": ["openid", "profile", "email", "offline_access"],
"token_endpoint_auth_method": "client_secret_basic"
}'
Response: 200 OK with the full updated client object.
Delete a client
curl -s -X DELETE https://idp.example.com/api/admin/clients/3f8a2c1e-7d4b-4e9f-a0c1-2b3d4e5f6a7b \
-b session.jar
Response: 204 No Content. The client is tombstoned in the CRDT and
propagated to all cluster nodes. Any existing tokens for this client
remain valid until they expire (access tokens are self-contained JWTs);
revoke outstanding refresh token families separately via
DELETE /api/admin/refresh-families/{family_id} if needed.
List and inspect clients
# List all clients
curl -s https://idp.example.com/api/admin/clients -b session.jar
# Get a specific client
curl -s https://idp.example.com/api/admin/clients/3f8a2c1e-7d4b-4e9f-a0c1-2b3d4e5f6a7b \
-b session.jar
Dynamic Client Registration (RFC 7591)
POST /register allows a client to register itself without operator
involvement. It is available via two authorization paths:
Path 1 — Pre-shared initial access token
Set server.registration_token in ahdapa.toml:
[server]
registration_token = "a-random-high-entropy-token"
Then register:
curl -s -X POST https://idp.example.com/register \
-H "Authorization: Bearer a-random-high-entropy-token" \
-H "Content-Type: application/json" \
-d '{
"redirect_uris": ["https://app.example.com/callback"],
"client_name": "Self-registered App",
"token_endpoint_auth_method": "client_secret_basic",
"scope": "openid profile email"
}'
Path 2 — Kerberos service-principal session
A session cookie whose sub is a service principal in the server’s
own Kerberos realm (format service/host@REALM) may register without
a pre-shared token. See Authentication Methods
for how to obtain such a session via SPNEGO at /authorize.
DCR request body
| Field | Type | Notes |
|---|---|---|
redirect_uris | array of strings | Required. |
client_name | string | Optional. Defaults to the generated client_id. |
token_endpoint_auth_method | string | client_secret_basic (default), client_secret_post, private_key_jwt, or none. kerberos_client_auth is not available via DCR. |
scope | string | Space-separated. Defaults to "openid". |
jwks_uri | string | Required when token_endpoint_auth_method = "private_key_jwt". |
client_secret | string | Optional. When omitted for client_secret_* methods, the server generates a random 32-byte secret. |
subject_type | string | "public" or "pairwise". |
DCR response — 201 Created
{
"client_id": "9b2e4f1a-3c7d-4a0e-b8f2-1d5a6c7e8f9g",
"client_name": "Self-registered App",
"redirect_uris": ["https://app.example.com/callback"],
"token_endpoint_auth_method": "client_secret_basic",
"scope": "openid profile email",
"client_secret": "server-generated-secret-here",
"registration_client_uri": "https://idp.example.com/api/admin/clients/9b2e4f1a-3c7d-4a0e-b8f2-1d5a6c7e8f9g"
}
The registration_client_uri points to the admin API resource for
this client. Subsequent management (updates, deletion) requires admin
RBAC permissions via the admin session — RFC 7592 management tokens
are not issued.
Registration for Kerberos clients
kerberos_client_auth clients must be registered via the admin API.
Dynamic registration rejects them with invalid_client_metadata.
See Authentication Methods
for the full workflow including HBAC enforcement and SSSD deployment patterns.
curl -s -X POST https://idp.example.com/api/admin/clients \
-b session.jar \
-H "Content-Type: application/json" \
-d '{
"client_name": "SSSD template client",
"token_endpoint_auth_method": "kerberos_client_auth",
"kerberos_principal_pattern": "host/*@EXAMPLE.COM",
"kerberos_hbac_service": "sssd-idp",
"scopes": ["openid"]
}'
The * wildcard in kerberos_principal_pattern matches any hostname in
the realm. Each matched machine presents its own Kerberos AP-REQ at the
token endpoint; the server verifies it against the pattern and, if
kerberos_hbac_service is set, against the replicated HBAC rule set.