- RFC 6749, section 4.4: Client Credentials Grantthe normative text; 2.3.1 owns the authentication methods and the Basic encoding
- RFC 9700, section 2.5: Client Authenticationthe recommendation toward asymmetric methods (mTLS, private_key_jwt)
- RFC 8705: Mutual-TLS Client Authentication and Certificate-Bound TokensCampbell et al. (IETF, February 2020) - the certificate route
- RFC 7523: JWT Profile for OAuth 2.0 Client Authenticationthe private_key_jwt assertion format
- RFC 9068: JWT Profile for OAuth 2.0 Access Tokenswhat machine tokens look like when the AS issues JWTs -
subnames the client
The simplest grant in the framework, and the only one with no human in it: a service authenticates as itself and gets a token for its own scopes. Cron jobs hitting your API, microservice-to-microservice calls, CI pipelines, daemons, batch jobs - anywhere the client and the resource owner are the same party. Section 4.4 is direct about eligibility: "the client credentials grant type MUST only be used by confidential clients". A public client has nothing to authenticate with; a machine flow built on one is role-play.
Because "the client authentication is used as the authorization grant" (4.4.1), there is no authorization endpoint, no browser, no consent screen. The whole grant is one POST. That economy is also the security story: the token is exactly as strong as the client authentication behind it - which is why this page spends more time on authentication methods than on the flow.
Two hops before the API call. Everything is back channel.
All arrows are back channel (direct, over TLS). There is no front channel in this flow - nothing ever crosses a browser.
POST /token HTTP/1.1 Host: accounts.example.com Authorization: Basic PGNsaWVudC1pZD46PGNsaWVudC1zZWNyZXQ+ (base64 of the words client-id and client-secret) grant_type=client_credentials scope=invoices:issue
Two parameters, one of them optional. grant_type is REQUIRED and MUST be client_credentials; scope is OPTIONAL (4.4.2). Client authentication is a MUST on both sides - "the client MUST authenticate with the authorization server", "the authorization server MUST authenticate the client". No redirect_uri, no code, no verifier: those exist to carry a user's grant, and there is no user here.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "eyJhbGciOiJQUzI1NiIsImtpZCI6IjcxIn0.<payload>.<signature>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "invoices:issue"
} Note what is missing: "a refresh token SHOULD NOT be included" (4.4.3). There is nothing to keep alive - when the token expires, the service simply authenticates again, which costs it nothing. A refresh token would add a second long-lived credential to protect for zero benefit.
Failures answer in the framework's shared grammar: a rejected secret, certificate, or assertion comes back as invalid_client(a 401 when the client used the Authorization header); a client not registered for this grant type gets unauthorized_client; overscoped requests get invalid_scope. What each code means, with typical causes: theerror-code table on the framework page.
The grant is only as strong as its client authentication, and the options are not equivalent. A shared secret is a string your operations pipeline leaks; asymmetric methods put the proof in something that never leaves the client. RFC 9700 §2.5 settles the direction: "it is RECOMMENDED to use asymmetric cryptography for client authentication, such as mutual TLS for OAuth 2.0 [RFC 8705] or signed JWTs ('Private Key JWT')".
| Method | Credential | Where | Notes |
|---|---|---|---|
client_secret_basic | Shared secret | HTTP Basic header | The 6749 MUST-support baseline (2.3.1). Gotcha: id and secret are form-urlencoded before becoming Basic username/password. OAuth 2.1 demotes Basic to MAY. |
client_secret_post | Shared secret | Body parameters | NOT RECOMMENDED in 6749 (2.3.1), "SHOULD be limited to clients unable to directly utilize the HTTP Basic authentication scheme"; body only, "MUST NOT be included in the request URI". OAuth 2.1 flips it to the MUST-support method. |
private_key_jwt | Registered public key + signed JWT assertion | Body client_assertion | Per RFC 7523; the secret never exists server-side. The assertion MUST carry a short exp (RFC 7523 §3); replay protection via a jti claim is optional - rotation and revocation are key operations, not secret scrambles. |
tls_client_auth | Client certificate | mTLS handshake | RFC 8705 §2. The certificate IS the credential; also enables certificate-bound tokens (§3) so the access token is useless without the private key. |
Whichever you pick, remember what section 3.2.1 says the authentication is for: "enforcing the binding of refresh tokens and authorization codes to the client they were issued to", breach recovery, and credential rotation. A secret hardcoded into a container image answers none of those.
Cache it. The classic production mistake is requesting a new token per API call: your authorization server becomes a hot dependency of every request. Hold the token until just before expires_in runs out (with a safety margin for clock skew) and reuse it across calls and threads. The grant is cheap precisely so you can re-run it when you must.
Read it correctly. If the token comes back as a JWT (theRFC 9068 profile), its sub should be the client's identifier, not a user's (RFC 9068 §2.2, a SHOULD the major servers follow) - machine tokens routinely break user-centric authorization code for exactly this reason. Check aud before accepting the token across service boundaries; the audience rule exists to stop one service accepting a token minted for another (RFC 9700 §2.3).
Scope it. Request the narrowest scope the job needs - section 10.3's "minimal scope necessary" advice applies double when the token's lifetime is measured in cron schedules rather than browser sessions.
| Failure | What happens | The fix |
|---|---|---|
| Secret leakage | Shared secrets ride CI logs, env dumps, and container images; any reader now authenticates as the service | Asymmetric methods: private_key_jwt or mTLS - the proof never leaves the client (RFC 9700 §2.5) |
| Per-request token fetch | Every API call pays a token-endpoint round trip; the AS falls over first | Cache until near-expiry with clock-skew margin |
| Treated as a user | Authorization code keyed on user identity breaks: a machine token's sub should be the client (RFC 9068 §2.2, SHOULD) | Model machine identities as first-class principals |
| No audience discipline | A token minted for service A is accepted by service B - one leak becomes lateral movement (RFC 9700 §2.3, §4.9) | Audience-restricted tokens; resource servers verify aud on every request |
| Public-client machine flows | A device app "authenticates" with an extractable secret and calls itself confidential | The grant is confidential-only by definition (6749 §4.4); rethink who the client is |