The OAuth client credentials grant gives a service its own access with no user in the loop, so whoever holds the client credential holds that access, often tenant-wide and long after anyone remembers the client exists. Machine-to-machine (M2M) OAuth is how backend services, scheduled jobs, integrations and increasingly AI agents get access tokens for APIs. It is simple to set up with a client ID and secret, and that simplicity is the problem: shared secrets leak, application permissions are broad, and tokens are usually bearer tokens anyone can replay. This guide covers how the grant works, which client authentication method to choose, scoping and audience restriction, sender-constrained tokens, resource server validation, platform specifics, rollout, detection and the common mistakes. It is for platform, API and identity engineers; for user-facing flows see the OAuth 2.0 and OpenID Connect Guide, and for acting on behalf of a user see the OAuth Token Exchange and Delegation Guide.
Key takeaways
- The client is the identity. With no user, the client credential is the only thing standing between an attacker and every permission the client holds, so treat it as a privileged NHI credential.
- Replace shared secrets with asymmetric authentication. RFC 9700 recommends mTLS or private_key_jwt over client secrets; better still, use workload identity federation so there is no long-lived credential at all.
- Scope and audience every token. Request one resource and the minimum scopes, and make resource servers reject tokens not issued for them.
- Sender-constrain high-value tokens. mTLS certificate binding (RFC 8705) or DPoP (RFC 9449) makes a stolen access token useless without the client's private key.
- Resource servers must enforce, not assume. Some platforms issue app-only tokens with no roles at all; the API has to check issuer, audience, client and permissions itself.
How the client credentials grant works
RFC 6749 section 4.4 defines the client credentials grant: the client authenticates to the authorisation server's token endpoint and receives an access token for resources it controls, or that have been granted to it. The grant MUST only be used by confidential clients, meaning clients that can keep a credential secret, such as server-side services. It must never be used from browsers, mobile apps or desktop software distributed to users.
- The client sends a POST to the token endpoint with
grant_type=client_credentials, an optionalscope(or a resource indicator) and its client authentication. - The authorisation server authenticates the client, checks which permissions the client has been granted and issues an access token, usually with
token_typeBearer and anexpires_invalue. - The client calls the API with the token. When it expires, the client simply authenticates again: no refresh token is needed, and RFC 6749 says one should not be issued for this grant. Microsoft Entra ID, for example, never issues refresh tokens in this flow.
If the server grants fewer scopes than requested it must return the granted scope in the response (RFC 6749 section 3.3), so clients should read it rather than assume. The client must cache tokens until shortly before expiry: fetching a token per request multiplies load, cost (Amazon Cognito bills M2M by active app clients and token request volume) and the number of credential presentations.
Client credentials is not the right grant when a service acts for a specific user; use the authorisation code grant for the user, then token exchange (RFC 8693) for downstream calls, so the user context is kept. See the Human vs Non-Human Identity guide for the distinction.
Choosing a client authentication method
| Method | How the client proves itself | Credential at rest | Assurance | Use when |
|---|---|---|---|---|
client_secret_basic | Client ID and secret in an HTTP Basic header (RFC 6749 section 2.3.1) | Shared secret on client and server | Low | Only if nothing better is supported; rotate and vault it |
client_secret_post | Secret in the request body; RFC 6749 calls this NOT RECOMMENDED | Shared secret | Low | Avoid; never put secrets in the URI |
private_key_jwt (RFC 7523) | JWT signed with the client's private key, sent as client_assertion | Private key on client only; server holds public key or JWKS URI | High | Default choice for services that can hold a key |
tls_client_auth / self_signed_tls_client_auth (RFC 8705) | X.509 client certificate in the TLS handshake | Private key on client only | High | mTLS already in place end to end; FAPI-style APIs |
| Federated assertion (workload identity federation) | Token from the workload's platform (Kubernetes, CI, cloud) presented as client_assertion | None long-lived; the platform issues short-lived tokens | High | Workloads running on platforms with their own identity provider |
RFC 9700 section 2.5 recommends asymmetric methods such as mTLS or private_key_jwt over shared secrets. Microsoft states that certificates and federated credentials give higher assurance than secrets, and that the certificate flow differs from the secret flow only by replacing client_secret with client_assertion_type and client_assertion.
Building a private_key_jwt assertion correctly
- Type:
client_assertion_typeisurn:ietf:params:oauth:client-assertion-type:jwt-bearer. - Issuer and subject: for client authentication,
submust be the client ID;issidentifies the issuer, normally also the client ID. - Audience:
audmust identify the authorisation server (the token endpoint URL may be used), and the server must reject assertions not addressed to it. - Lifetime:
expis required; keep it to a few minutes. Servers may also reject aniatunreasonably far in the past. - Replay: include a unique
jti. RFC 7523 makes jti tracking optional for servers, so check whether yours does; if not, very short lifetimes are your replay control. - Errors: an invalid client assertion returns
invalid_client; log these, as they show broken clients or forged attempts.
Workload identity federation removes the stored key altogether. Microsoft Entra ID, for instance, accepts a JWT issued by another identity provider such as Kubernetes as the client_assertion once it is registered as a federated identity credential. See the Workload Identity Federation Guide and the Kubernetes Service Account Token Guide.
Scopes, audiences and least privilege
RFC 9700 section 2.3 says access tokens should be audience-restricted to a specific resource server and limited to the minimum privileges needed, using scope or authorization_details (RFC 9396 rich authorisation requests). Resource indicators (RFC 8707) let the client name the intended resource in the token request.
- One resource per token: Microsoft Entra requires the
scopeto name a single resource with the/.defaultsuffix (for examplehttps://graph.microsoft.com/.default), which returns all application permissions granted for that resource. That makes the grant itself the control: what an admin consents to is what every token carries. - Application permissions are broad by design: in Entra, client credentials can only use application permissions (app roles), which need administrator consent and can reach data across the whole organisation. Where the API supports it, narrow them, for example by scoping Exchange Online application access to specific mailboxes.
- Custom scopes per action: Amazon Cognito uses resource servers with scopes named
resourceServerIdentifier/scopeName(for exampleorders-api/orders.read). Define read and write scopes separately and grant them per client. - Check platform gaps: Cognito documents that RFC 8707 resource binding is not available for client credentials grants, so audience separation there depends on separate resource servers and scopes.
- One client per workload: never share a client ID across services or environments. Shared clients make attribution impossible and turn one leak into many compromises.
| Platform | Permission model for M2M | Key setting | Gotcha |
|---|---|---|---|
| Microsoft Entra ID | Application permissions (app roles) with admin consent | Assignment required on the resource app | Tokens can be issued without a roles claim; the API must check permissions itself |
| Amazon Cognito | Custom scopes on resource servers, linked to an app client with a secret | ALLOW_CLIENT_TOKEN_AUTH for GetClientToken | Client secret required; up to two active secrets for rotation; no RFC 8707 binding for M2M |
| Generic OAuth server | Scopes, audiences and per-client policy | Registered token_endpoint_auth_method per client | Defaults often allow secrets and long token lifetimes |
Sender-constrained tokens: mTLS and DPoP
A bearer token works for anyone who holds it. RFC 9700 section 2.2.1 says authorisation and resource servers should use sender-constraining mechanisms, naming mTLS (RFC 8705) and DPoP (RFC 9449).
| Aspect | mTLS certificate-bound tokens (RFC 8705) | DPoP (RFC 9449) |
|---|---|---|
| Binding | cnf claim with x5t#S256 certificate thumbprint | cnf claim with jkt JWK thumbprint |
| Proof at the API | Same client certificate in the TLS handshake | DPoP header: JWT with typ dpop+jwt, public jwk, claims jti, htm, htu, iat, ath |
| Token type | Bearer token checked against the certificate | Authorization: DPoP with token_type DPoP |
| Layer | Transport | HTTP application layer |
| Works through TLS-terminating proxies | Only if the proxy forwards the verified certificate securely | Yes |
| Replay controls | Inherent in the handshake | Short proof window, jti tracking, server nonces via DPoP-Nonce |
| Discovery | tls_client_certificate_bound_access_tokens | dpop_signing_alg_values_supported; clients can register dpop_bound_access_tokens |
DPoP is not a client authentication method and combines with any of them, including private_key_jwt. Resource servers must reject a DPoP-bound token presented as a plain Bearer token, otherwise the binding is decorative. See the mTLS Guide for the transport side and the OAuth 2.1, GNAP and FAPI Guide for profiles that require these mechanisms.
Validating M2M tokens at the resource server
RFC 9068 defines a JWT profile for access tokens with the header typ at+jwt and required claims iss, exp, aud, sub, client_id, iat and jti. For grants with no resource owner, such as client credentials, sub identifies the client application.
- Reject any token whose
typis notat+jwt(orapplication/at+jwt); this stops OpenID Connect ID tokens being accepted as access tokens. - Check
issexactly matches the expected issuer and fetch keys only from that issuer's metadata. - Check
audcontains this API's own identifier. - Verify the signature with the expected algorithm, never
none, and do not let the token's header choose keys from arbitrary locations. - Check
expwith no more than a few minutes of clock skew. - Authorise: check scopes or roles for the operation and, where the platform allows role-less app tokens, check
client_id(or Entra'sappid) against an allow list. - If the token is sender-constrained, verify the certificate thumbprint or DPoP proof; failures return
invalid_token.
RFC 9068 also warns that authorisation servers should stop clients from influencing sub, for example by registering a client ID that collides with a privileged user identifier, so APIs should never treat sub alone as proof of a human. For opaque tokens, use token introspection (RFC 7662) and cache results briefly.
Implementation and rollout
- Inventory clients: list every client registration with grant types enabled, authentication method, granted permissions, owner, last token request and credential expiry. Include test tenants and old integrations.
- Remove what is unused: disable clients with no token requests in 90 days (after confirming with owners), and remove unused permissions.
- Split shared clients: one client per service per environment, with its own permissions.
- Migrate authentication: add a certificate, JWKS or federated credential alongside the existing secret, switch the client, monitor
invalid_clienterrors, then delete the secret. Keeping the old secret registered during the change is the rollback path; deleting it on a fixed date ends it. - Tighten tokens: shorten access token lifetimes where the platform allows, enforce audience and scopes, and enable assignment required or equivalent so unassigned clients cannot get tokens.
- Add sender constraint: pilot mTLS binding or DPoP on one high-value API, with the resource server accepting both bound and bearer tokens for a short, logged transition, then require binding.
- Harden resource servers: roll out the validation steps above in log-only mode first, review rejections, then enforce.
- Govern new clients: require an owner, a purpose, a permission review and an expiry date for every new M2M client, and review application permissions quarterly.
Decision guidance
- Client credentials versus API keys: prefer client credentials when you need central revocation, scopes, short-lived tokens and audit by client. A static API key is a bearer credential with none of these. See the API Key Management Guide.
- Client credentials versus token exchange: if the downstream call should be limited to what a particular user may do, client credentials is the wrong tool; use delegation.
- private_key_jwt versus mTLS: private_key_jwt is easier when TLS is terminated by gateways or load balancers; mTLS fits estates that already run a mesh or private PKI.
- Federation versus stored keys: where the workload runs on a platform with an identity provider (cloud, Kubernetes, CI), federation is the strongest option because nothing long-lived exists to steal.
- Small organisations: start with one client per integration, secrets in a vault with rotation, short token lifetimes and a quarterly permission review, then move the most privileged clients to certificates.
- Large organisations: enforce asymmetric or federated authentication by policy, automate client inventory, and require sender-constrained tokens for financial, data export and administrative APIs.
- AI agents and commerce: agents calling APIs on their own account fit client credentials; agents acting for a person or paying on their behalf need delegation and per-transaction limits. See the Agentic AI Identity Guide and the Agentic Commerce Identity Guide.
Detection and measurement
- Credential changes: alert on new secrets, certificates or federated credentials added to existing clients, which ATT&CK tracks as T1098.001 Additional Cloud Credentials.
- Permission grants: alert on new application permissions and admin consent, especially for mail, files, directory and key vault APIs.
- Token use anomalies: token requests from new IP ranges or networks for a client, sudden volume increases, or a client calling APIs outside its normal set. Stolen tokens used elsewhere map to T1550.001 Application Access Token and their theft to T1528 Steal Application Access Token.
- Authentication failures:
invalid_clientandinvalid_grantspikes at the token endpoint; rejected tokens at resource servers grouped by reason.
| Metric | Target | Why it matters |
|---|---|---|
| M2M clients using asymmetric or federated authentication | All privileged clients; rising overall | Shared secrets are the main leak path |
| Clients with a named owner and last-used date | 100% | Orphaned clients keep working after everyone forgets them |
| Clients unused for 90 days still enabled | Zero | Dormant credentials are pure risk |
| Clients holding tenant-wide application permissions | Falling, each with an approved exception | Broad permissions turn one leak into a tenant compromise |
| High-value APIs requiring sender-constrained tokens | All in scope | Blocks replay of stolen tokens |
Resource servers enforcing aud, iss and typ checks | 100% | Prevents token reuse across APIs |
Common failure patterns
- Forgotten privileged clients: in the Microsoft Midnight Blizzard breach, attackers compromised a legacy test OAuth application with elevated access, created further OAuth apps and used the app-only Exchange Online
full_access_as_approle to read mailboxes, access that survives user password resets. Inventory test tenants and alert on new credentials and consents. - Secrets in code and pipelines: client secrets committed to repositories, baked into images or pasted into tickets, with expiry set to the maximum. Move to certificates or federation, and scan for leaked secrets.
- Using the grant from public clients: mobile, desktop or browser apps shipped with a client secret, which RFC 6749 forbids because the secret cannot be kept.
- APIs that trust any valid token: a resource server checks only the signature, so tokens for another API, ID tokens or role-less app tokens are accepted.
- One client for everything: a single client ID shared by many services and environments, so revocation breaks production and logs cannot say who did what.
- Token per request and no caching: clients request a new token on every call, inflating cost and load and hiding real anomalies in noise.
Framework mapping
| Framework | Reference | How M2M OAuth applies |
|---|---|---|
| NIST SP 800-53 Rev. 5 | IA-9 Service Identification and Authentication | Services authenticate before access, ideally with asymmetric credentials |
| NIST SP 800-53 Rev. 5 | IA-5 Authenticator Management; IA-5(2) Public Key-based Authentication | Secret rotation, key protection, certificate and JWKS management |
| NIST SP 800-53 Rev. 5 | AC-6 Least Privilege | Minimum scopes and application permissions per client |
| NIST CSF 2.0 | PR.AA-01; PR.AA-03; PR.AA-05 | Service identities managed, authenticated and given least-privilege access |
| NIST CSF 2.0 | DE.CM-03 | Monitoring of client and token activity |
| ISO/IEC 27001:2022 Annex A | A.5.17 Authentication information; A.8.5 Secure authentication | Handling of client secrets and keys, strong authentication methods |
| MITRE ATT&CK v20 | T1528; T1550.001; T1098.001; T1552.004 Private Keys | Token theft and reuse, credential persistence on apps, theft of signing keys |
| MITRE D3FEND | D3-TBA Token-based Authentication; D3-TB Token Binding | Defensive techniques implemented by scoped, sender-constrained tokens |
Practitioner checklist
- Use client credentials only from confidential, server-side clients.
- Inventory every M2M client with owner, permissions, authentication method and last use.
- Give each workload and environment its own client ID.
- Replace client secrets with private_key_jwt, mTLS or workload identity federation, starting with the most privileged clients.
- Request one resource and minimum scopes; review application permissions quarterly.
- Cache tokens until shortly before expiry and keep lifetimes short.
- Validate
typ,iss,aud, signature, expiry and permissions at every resource server. - Require sender-constrained tokens (RFC 8705 or DPoP) for high-value APIs.
- Alert on new client credentials, new application permissions and unusual token use.
- Disable clients unused for 90 days after confirming with owners.
Standards and references
- IETF: RFC 6749 The OAuth 2.0 Authorization Framework, Client Credentials Grant
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
- IETF: RFC 7523 JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants
- IETF: RFC 8705 OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
- IETF: RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)
- IETF: RFC 9068 JWT Profile for OAuth 2.0 Access Tokens
- Microsoft Learn: Microsoft identity platform and the OAuth 2.0 client credentials flow
- AWS: Scopes, M2M and resource servers in Amazon Cognito
Related NHI Mgmt Group resources: NHI Authentication Guide · OAuth 2.0 and OpenID Connect Guide · OAuth Token Exchange and Delegation Guide · Workload Identity Federation Guide · mTLS Guide