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 = truein the server configuration.- The client registered via the admin API with
kerberos_principalorkerberos_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.