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

Client Credentials Flow

The client credentials flow (RFC 6749 §4.4) is the machine-to-machine grant type. There is no user involved: the client authenticates itself at the token endpoint and receives an access token scoped to that client’s own permissions.

Public clients (token_endpoint_auth_method = "none") are not permitted to use this grant — a client must prove its identity to receive a token.


When to use

Use the client credentials flow when:

  • A backend service needs to call another service and no user session is involved.
  • A machine (SSSD host, CI runner, monitoring agent) needs to authenticate itself.
  • You need a token that represents the client rather than a specific user.

For user-delegated access, use the authorization code flow or device authorization flow instead.


Token request

POST /token with grant_type=client_credentials. Authenticate the client using one of the methods described below.

The response does not include a refresh_token (RFC 6749 §4.4.3 — the server SHOULD NOT issue one). To renew, simply repeat the token request.

Successful response — 200 OK:

{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImF0K0pXVCIsImtpZCI6IkFCQ0QxMjM0In0...",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "openid"
}

The access token sub is the client_id (RFC 9068 §2.2), except for Kerberos template clients where sub is the authenticated machine principal (see kerberos_client_auth below).


Authentication methods

client_secret_basic

The client ID and secret are encoded as HTTP Basic credentials:

curl -s -X POST https://idp.example.com/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  -d "scope=openid"

client_secret_post

Credentials are sent as form body parameters:

curl -s -X POST https://idp.example.com/token \
  -d "grant_type=client_credentials" \
  -d "client_id=CLIENT_ID" \
  -d "client_secret=CLIENT_SECRET" \
  -d "scope=openid"

client_secret_jwt

A JWT is signed with an HMAC key derived from the client_secret. The JWT must contain iss = sub = client_id, aud pointing to the token endpoint or issuer, a short exp, and a unique jti. The server validates the HMAC signature, the claims, and enforces single-use on jti.

curl -s -X POST https://idp.example.com/token \
  -d "grant_type=client_credentials" \
  -d "client_id=CLIENT_ID" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=HMAC_SIGNED_JWT" \
  -d "scope=openid"

private_key_jwt

A JWT is signed with the client’s private key. The server fetches the corresponding public key from the client’s registered jwks_uri.

# Build and sign the assertion JWT (example using Python's PyJWT):
# {
#   "iss": "CLIENT_ID",
#   "sub": "CLIENT_ID",
#   "aud": "https://idp.example.com/token",
#   "iat": <now>,
#   "exp": <now + 60>,
#   "jti": "<unique-uuid>"
# }

curl -s -X POST https://idp.example.com/token \
  -d "grant_type=client_credentials" \
  -d "client_id=CLIENT_ID" \
  -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
  -d "client_assertion=SIGNED_JWT" \
  -d "scope=openid"

The aud claim must be either the token endpoint URL (https://idp.example.com/token) or the issuer URL (https://idp.example.com). A unique jti is required; replay is detected and rejected.

tls_client_auth and self_signed_tls_client_auth

The client presents its registered TLS certificate during the TLS handshake. The server verifies that the SHA-256 thumbprint of the presented certificate matches the stored thumbprint. The resulting access token contains a cnf.x5t#S256 claim binding it to the certificate.

curl -s -X POST https://idp.example.com/token \
  --cert /path/to/client.pem \
  --key /path/to/client.key \
  -d "grant_type=client_credentials" \
  -d "client_id=CLIENT_ID" \
  -d "scope=openid"

For reverse-proxy deployments, configure tls.client_cert_header in ahdapa.toml. See Configuration.

kerberos_client_auth

An ahdapa-specific extension for SSSD deployments where every enrolled machine holds a Kerberos host keytab (host/hostname@REALM).

The machine presents its Kerberos AP-REQ in the standard HTTP Negotiate header. Multi-round GSSAPI exchanges are not supported on the token endpoint — the AP-REQ must complete in a single round trip (normal for host/ service principals).

# Acquire a service ticket for the IdP's HTTP service principal
kinit -k -t /etc/krb5.keytab host/node1.example.com@EXAMPLE.COM

curl -s -X POST https://idp.example.com/token \
  --negotiate -u: \
  -d "grant_type=client_credentials" \
  -d "client_id=TEMPLATE_CLIENT_ID" \
  -d "scope=openid directory.read"

Include directory.read in the scope when the token will be used with the Identity API (/api/identity/users, /api/identity/groups).

kerberos_client_auth requires:

  • [ipa] gssapi = true in the server configuration.
  • The client registered via the admin API with kerberos_principal or kerberos_principal_pattern (not via DCR — dynamic registration rejects it).

For template clients (those with kerberos_principal_pattern), the access token sub is the authenticated machine principal (e.g. host/node1.example.com@EXAMPLE.COM) rather than the client_id. This makes individual machines distinguishable in audit logs and introspection responses.

See Authentication Methods for registration details and SSSD deployment patterns.


Scopes

The granted scope is the intersection of the requested scope with the client’s registered scope set. If scope is omitted, all of the client’s registered scopes are granted.

# Request only a subset of registered scopes
curl -s -X POST https://idp.example.com/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  -d "scope=openid"

DPoP sender-constrained tokens

Add a DPoP header to the token request to obtain a sender-constrained access token. See Using and Validating Tokens for the full DPoP proof construction.

curl -s -X POST https://idp.example.com/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -H "DPoP: DPOP_PROOF_JWT" \
  -d "grant_type=client_credentials" \
  -d "scope=openid"

When a DPoP proof is accepted, the response includes "token_type": "DPoP" and the access token’s cnf claim contains "jkt" — the JWK thumbprint of the client’s DPoP key. Every subsequent use of the access token must be accompanied by a fresh DPoP proof.


Token renewal

Client credentials tokens do not have refresh tokens. When a token expires, repeat the token request with the same credentials. The short default lifetime (15 minutes) means that leaked access tokens are naturally short-lived, which is why refresh tokens are not needed for machine-to-machine flows.