Join our Newsletter — 33% off our NHI Course
Home› Guides› Authorisation Code vs Client Credentials Flow: Choosing the…
Guide Authorisation

Authorisation Code vs Client Credentials Flow: Choosing the Right OAuth Grant per Caller

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 14 November 2026 16 min read
On this page

The most damaging OAuth design mistake is not a weak secret, it is picking the wrong grant, so that a service token silently does work that should have been bound to a user. The authorisation code grant issues a token that carries a user's delegated authority, after that user has authenticated and consented. The client credentials grant issues a token that carries only the application's own authority, with no user in the picture at all. The two flows look similar at the token endpoint, which is exactly why teams mix them up. This guide is a decision guide for architects, identity engineers and API owners: how each grant works in words, what each one's threat model looks like, how to choose by caller type (a user-facing app, a background service, an AI agent acting for a person), and where PKCE and token exchange change the answer. Mechanics of client credentials in depth are in the Machine-to-Machine OAuth (Client Credentials) Guide, and PKCE detail is in PKCE Implementation.

Key takeaways

  • The question is whose authority the token carries. Authorisation code produces a token for a user, via a client; client credentials produces a token for the client alone. Choose by the subject of the action, not by the convenience of the integration.
  • Client credentials for user actions is the classic failure. A service token that can act on any user's data turns every downstream authorisation check into "is this the trusted app?", which removes per-user limits and per-user audit.
  • Authorisation code now always means PKCE. RFC 9700 says public clients must use PKCE and confidential clients should, and the OAuth 2.1 draft requires it by default. PKCE does not apply to client credentials, because there is no code.
  • Client credentials is for confidential clients only. RFC 6749 section 4.4 says the grant must only be used by confidential clients, and section 4.4.3 says a refresh token should not be included in the response.
  • Agents usually need both, joined by token exchange. An agent acting for a user starts from a user-delegated token and should exchange it (RFC 8693) for a narrow, audience-bound token that names the agent as actor, rather than reusing its own service credential.

The short answer

If a human is the reason the call is happening and the data belongs to, or is scoped by, that human, use the authorisation code grant with PKCE, so the access token represents the user's delegated consent and the resource server can enforce that user's permissions. If no human is involved, the caller is a backend workload acting on its own behalf (a nightly sync, a monitoring probe, a service calling another service it owns), use the client credentials grant with strong client authentication, ideally private_key_jwt or mutual TLS, and a narrow audience. If a workload acts for a specific user after the user has left, such as an agent completing a task, do not fall back to client credentials: carry the user context forward with a refresh token held by a confidential client, or exchange the user's token for a delegated one. Implicit and resource owner password credentials grants are not options: RFC 9700 says clients should not use the implicit grant and the password grant must not be used, and neither appears in the OAuth 2.1 draft.

Side-by-side comparison

DimensionAuthorisation code (with PKCE)Client credentials
Subject of the tokenThe resource owner (user), acting through the clientThe client application itself
Defined inRFC 6749 section 4.1, PKCE in RFC 7636RFC 6749 section 4.4
User interactionYes: browser redirect, authentication, consentNone
Client types allowedConfidential and public (public clients rely on PKCE)Confidential only
Proof at token endpointAuthorisation code plus code_verifier, plus client authentication if confidentialClient authentication only
Refresh tokenCommonly issued; public clients need rotation or sender constraint (RFC 9700 section 2.2.2)Should not be included (RFC 6749 section 4.4.3); the client simply asks again
Scope meaningThe ceiling of what the user consented to, intersected with what the user may doThe full authority granted to the application, for every record it can reach
Audit trailToken carries the user's sub; actions attribute to a personToken carries the client's identity; actions attribute to a workload
Main credential to protectRedirect URIs, authorisation codes, refresh tokensThe client secret or private key
Typical blast radius if stolenOne user's delegated access, for the token or refresh lifetimeEverything the application can touch, for every tenant or user it serves

Authorisation code grant: architecture and threat model

Architecture in words

The client sends the user's browser to the authorisation endpoint with its client_id, requested scope, a state value, its registered redirect_uri and a PKCE code_challenge derived with S256. The authorisation server authenticates the user, records consent and redirects back with a short-lived, single-use code. The client then calls the token endpoint with the code, the original redirect URI and the code_verifier, authenticating itself if it is confidential. RFC 7636 defines the verifier as a random string of 43 to 128 characters and makes S256 mandatory for servers to implement; the plain method exists only for compatibility.

What goes wrong

  • Code interception or injection: RFC 7636 was written for malicious apps that register the same custom URI scheme and capture the code. RFC 9700 adds code injection into the legitimate client's session, which PKCE stops and client authentication alone does not. See Client Secret vs PKCE.
  • Redirect URI abuse: loose matching lets an attacker steer codes to a URI they control. RFC 9700 requires exact string matching, with a port exception only for localhost in native apps, and OWASP ASVS 5.0 V10.4.1 asks for the same check.
  • Refresh token theft: long-lived refresh tokens held by an integration are the real prize. In the Salesloft Drift breach stolen OAuth access and refresh tokens for a chatbot integration let the actor call Salesforce APIs as the trusted app and bulk export records across many tenants.
  • Over-broad consent: a single "full access" scope means a user-delegated token is still far more powerful than the task needs.

Controls that matter

PKCE S256 on every request, exact redirect URI matching, state or PKCE for CSRF, short access token lifetimes, refresh token rotation or sender constraint for public clients (see OAuth Refresh Token Rotation and Expiry Guide), and audience restriction so a token for one API is rejected by another.

Client credentials grant: architecture and threat model

Architecture in words

A backend workload calls the token endpoint directly with grant_type=client_credentials, an optional scope, and its client authentication: a shared secret (client_secret_basic or client_secret_post), a signed JWT assertion (private_key_jwt, RFC 7523) or a client certificate (mutual TLS, RFC 8705). The authorisation server returns an access token whose subject is the client. There is no browser, no code and no consent screen, so nothing in the flow can tell the authorisation server which user, if any, the work is for.

What goes wrong

  • Secret sprawl: shared secrets end up in environment variables, CI variables, config repositories and chat. Anyone who copies the secret is the application.
  • App-wide authority: the token is valid for every record the app's scopes reach, so a leak is a tenant-wide or platform-wide event, not a single-user one.
  • Use for user actions: the service receives a user's request, then calls a downstream API with its own client credentials token, passing a user_id parameter. The downstream API now trusts a parameter, not a token, which is the same shape as broken object level authorisation. See BOLA vs BFLA.
  • No natural expiry pressure: because the client can mint a new token at will, teams set long token lifetimes or never rotate the underlying credential.

Controls that matter

RFC 9700 section 2.5 recommends asymmetric client authentication (mutual TLS or private key JWTs). Bind tokens to the client with mTLS or DPoP (RFC 9449) as RFC 9700 section 2.2.1 recommends, restrict each token to one audience, keep scopes per downstream API and per operation, and store keys in a KMS, HSM or workload identity system rather than files. Where the platform supports it, replace stored secrets with workload identity federation. Deep implementation is in the Machine-to-Machine OAuth (Client Credentials) Guide and DPoP Guide.

Choosing by caller type

CallerWho is the subject?GrantNotes
Server-side web app acting for a signed-in userUserAuthorisation code + PKCE + client authenticationConfidential client; prefer private_key_jwt over a shared secret
Single-page app or mobile appUserAuthorisation code + PKCEPublic client; never ship a secret. Consider a backend for frontend so tokens stay server side
Nightly batch job, metrics collector, service calling its own platform APIThe workloadClient credentialsAsymmetric client auth, audience-bound token, minimal scopes
SaaS integration that reads a customer user's dataUser (or the customer tenant admin who consented)Authorisation code; refresh token held by the integrationThe refresh token is a high-value NHI secret: rotate, vault and monitor it
Device or CLI without a browserUserDevice authorisation grant (RFC 8628) or CIBASee CIBA vs OAuth Device Flow
Microservice receiving a user's token and calling another APIUser, with the service as actorToken exchange (RFC 8693)Do not forward the inbound token; do not swap to client credentials
AI agent acting for a userUser, with the agent as actorAuthorisation code for initial consent, then token exchange or a narrowly scoped refresh tokenRecord the agent in the act claim; keep per-task scopes
Autonomous agent doing its own housekeepingThe agent workloadClient credentialsTreat the agent as an NHI with its own lifecycle and owner

Small organisations: the usual first win is moving user-facing integrations off shared service accounts and onto authorisation code with PKCE, and moving client credentials secrets into a secrets manager. Large organisations: the harder problem is east-west traffic, where dozens of services use client credentials tokens with broad scopes to act for users. That needs token exchange at a central authorisation server and resource servers that read the user subject, not just the client ID.

PKCE and token exchange edge cases

  • PKCE with confidential clients: still required in practice. RFC 9700 recommends it, and the OAuth 2.1 draft (draft-ietf-oauth-v2-1-17, dated 31 October 2026, still an Internet-Draft) requires it by default. Client authentication proves which app is calling; PKCE proves the code belongs to this transaction.
  • PKCE with client credentials: does not apply. If a team asks "PKCE or client credentials?", the real question is whether a user is involved.
  • Token exchange for delegation: RFC 8693 uses grant_type=urn:ietf:params:oauth:grant-type:token-exchange with a subject_token (the user's token) and optional actor_token (the calling service or agent). With delegation the actor keeps its own identity and the issued JWT carries an act claim naming it; with impersonation the actor becomes indistinguishable from the subject. Prefer delegation so audit shows both parties. The may_act claim lets the authorisation server check which actors a subject's token may be exchanged for. See OAuth Token Exchange and Delegation Guide.
  • Audience on exchange: request the downstream API with audience or resource, so the exchanged token is useless anywhere else.
  • Token passthrough: the Model Context Protocol authorisation specification (2025-06-18) says an MCP server must not pass through the token it received from the client to upstream APIs, and must only accept tokens issued for itself. The same rule is sound for any service in a call chain. See OAuth Token Exchange vs Token Passthrough.
  • MCP and agents: the MCP specification builds on the authorisation code flow: clients must use PKCE and must send RFC 8707 resource indicators naming the MCP server in authorisation and token requests. An agent connecting to a user's tools is a user-delegated client, not a service.

Worked scenario: an expense assistant

A company deploys an AI expense assistant. Employees chat with it, and it files claims in the finance API and reads receipts from the employee's mailbox. A first design gave the assistant a client credentials token with expenses:write and mail:read for the whole tenant, and the assistant passed employee_id in each call. A prompt injection in a forwarded receipt could have told it to read another employee's mail, and the finance API would have had no way to object.

The redesign: each employee connects once through the authorisation code flow with PKCE, consenting to mail.read.receipts and expenses:submit. The assistant's backend is a confidential client using private_key_jwt and stores the refresh token in a vault, keyed by user. For each task it exchanges the user's access token for a token with audience set to the finance API, a single scope, a five-minute lifetime and an act claim naming the assistant. The finance API authorises on the user subject, so the assistant can only file that employee's claims, and logs show the employee and the agent together. The assistant keeps a separate client credentials registration, with no user data scopes, for its own health checks.

Implementation and migration

  1. Inventory clients by grant: export every registered client from the authorisation server with its allowed grant types, client authentication method, scopes and owner. Flag clients allowed both authorization_code and client_credentials; they are often the ones mixing user and service work.
  2. Find client credentials tokens acting on user data: at the resource servers, look for app-only tokens (no user sub, or sub equal to the client ID) calling endpoints that take a user or tenant identifier in the request. Each is a candidate for migration.
  3. Enable PKCE everywhere for authorisation code: require code_challenge_method=S256 on the server, reject plain, and block downgrade by rejecting a code_verifier when no challenge was sent.
  4. Harden client credentials clients: move to private_key_jwt or mTLS, set an audience per token, split broad scopes, and set a credential rotation schedule with an owner.
  5. Introduce token exchange for service chains: stand up the exchange endpoint, define may_act or policy rules for which services may act for users, and migrate one call chain at a time.
  6. Test in shadow mode: have resource servers log what they would decide using the user subject while still accepting the old tokens, compare, then enforce. Roll back by re-enabling the legacy client grant, which you should disable rather than delete until the cut-over is proven.
  7. Retire the old path: remove the client_credentials grant from clients that only need user flows, and revoke their secrets.

Detection and measurement

What to log: at the authorisation server, every token issuance with grant type, client ID, client authentication method, scopes, audience and, for exchange, subject and actor. At resource servers, the token's client ID, subject, scopes and the object or tenant acted on. Alert on client credentials issuance from new IP ranges or ASNs, sudden bursts of token requests, refresh token use from a new client location, and app-only tokens touching many users' records in a short window. MITRE ATT&CK describes the attacker side as T1528 Steal Application Access Token and T1550.001 Use Alternate Authentication Material: Application Access Token.

MetricTarget
Share of authorisation code requests using PKCE S256100%
Client credentials clients using asymmetric client authenticationRising each quarter, towards 100%
App-only tokens observed calling user-scoped endpointsFalling towards zero, each one with a documented exception
Client credentials clients with a named owner and rotation date100%
Access tokens accepted without an audience checkZero
Median client secret ageWithin the rotation policy

Framework mapping

FrameworkReferenceRelevance
NIST SP 800-53 Rev 5AC-3 Access Enforcement; AC-6 Least PrivilegeEnforce the user's permissions, not the service's, for user actions; narrow scopes
NIST SP 800-53 Rev 5IA-9 Service Identification and Authentication; IA-5 Authenticator ManagementClient authentication and secret or key management for client credentials
NIST CSF 2.0PR.AA-01; PR.AA-05Manage identities and credentials for services; least privilege in authorisations
OWASP ASVS 5.0V8.3.3; V10.4.1Access based on the originating subject, not an intermediary; exact redirect URI matching
MITRE ATT&CK v20T1528; T1550.001Theft and reuse of application access tokens

Common failure patterns

  • Service token plus user ID parameter: the downstream API trusts a parameter for the user, so any caller holding the service token can act for anyone.
  • Client credentials in a public client: a mobile or browser app ships a client secret to call an API "as the app". The secret is extractable, and RFC 6749 limits the grant to confidential clients.
  • Forwarding the user's token downstream: the same token works at every API in the chain because no audience is checked, so one leak opens all of them.
  • Agents on shared service credentials: an agent acting for many users with one client credentials token cannot be limited to the current user and leaves no per-user audit trail.
  • Refresh tokens treated as low value: integration refresh tokens stored in plaintext databases and never rotated, giving long-term user-delegated access to whoever steals them.

Practitioner checklist

  • For each client, write down whose authority its tokens carry, and confirm the grant matches.
  • Require PKCE S256 for every authorisation code client, public or confidential, and reject plain.
  • Allow the client credentials grant only for confidential clients with no user-scoped work.
  • Move client credentials clients to private_key_jwt or mTLS and give each an owner and rotation date.
  • Restrict every access token to one audience and validate aud at every resource server.
  • Replace token forwarding and service-token-plus-user-ID patterns with RFC 8693 token exchange.
  • Give AI agents user-delegated tokens for user work, with the agent named as actor, and separate client credentials for their own housekeeping.
  • Vault, rotate and monitor refresh tokens held by integrations.
  • Log grant type, client, subject, actor, scopes and audience on every issuance and alert on anomalies.

Standards and references

Related NHI Mgmt Group resources: Machine-to-Machine OAuth (Client Credentials) Guide, Access Tokens vs Refresh Tokens, CIBA vs OAuth Device Flow, AI Agent Authorisation Guide, OAuth Scope Design Guide.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 14 November 2026. Spotted an error? Use our contact page.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM and Agentic AI security. nhimg.org