Join our Newsletter — 33% off our NHI Course
Home› Guides› Client Secret vs PKCE: Why Confidential Clients Need…
Guide Authentication

Client Secret vs PKCE: Why Confidential Clients Need Both Controls

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

Teams with a server-side OAuth client often assume a client secret makes PKCE unnecessary, while teams moving to PKCE sometimes assume it lets them drop client authentication, and both assumptions leave a gap. A client secret, or better an asymmetric credential, proves which application is calling the token endpoint. PKCE proves that the party redeeming an authorisation code is the same client instance, in the same transaction, that asked for it. They answer different questions, and RFC 9700 shows an attack, authorisation code injection, that client authentication alone does not stop. This guide is about confidential clients: what each control protects, why the OAuth Security Best Current Practice and the OAuth 2.1 draft expect PKCE on top of client authentication, where private_key_jwt fits, and how to roll both out. It is for identity engineers, application architects and assessors reviewing web applications, backends for frontends and agent backends. Public client PKCE mechanics are in the PKCE Implementation guide.

Key takeaways

  • Client authentication and PKCE protect different things. Client authentication says "this is application X". PKCE says "this code belongs to this transaction". Neither substitutes for the other.
  • Client authentication does not stop code injection. RFC 9700 section 4.5 describes an attacker injecting a stolen code into their own session with the legitimate client, which then authenticates normally; it says asymmetric client authentication does not stop this, while PKCE does.
  • PKCE is recommended for confidential clients now and required by default in OAuth 2.1. RFC 9700 says confidential clients should use PKCE. The OAuth 2.1 draft (draft-ietf-oauth-v2-1-17) requires it unless the client is confidential and the server has reasonable assurance it validates the OpenID Connect nonce, and still recommends PKCE then.
  • PKCE never makes a public client confidential. A secret shipped in a mobile, desktop or browser app can be extracted; OAuth 2.1 tells servers not to accept such a secret as proof of identity.
  • The strongest pattern is PKCE plus asymmetric client authentication. RFC 9700 and OAuth 2.1 recommend mTLS or private_key_jwt, and FAPI 2.0 requires one of them together with PKCE S256, which also removes the shared secret that sprawls into config files.

The short answer

No, PKCE does not replace client authentication, and client authentication does not replace PKCE. For a confidential client using the authorisation code grant, do both: send a fresh S256 code challenge on every authorisation request, and authenticate at the token endpoint, preferably with private_key_jwt or mTLS rather than a shared secret. For a public client, PKCE is mandatory and is the only binding you have; do not pretend an embedded secret adds anything. For machine-to-machine calls with no user and no authorisation code (the client credentials grant), PKCE does not apply at all, and client authentication is the whole story.

What each control proves

QuestionClient authentication (secret, private_key_jwt, mTLS)PKCE (RFC 7636, S256)
What it provesThe caller at the token endpoint holds the registered application's credentialThe caller redeeming the code created the matching code challenge for this transaction
Scope of the proofEvery request the application makes, for every userOne authorisation code, one transaction, one client instance
Lifetime of the credentialMonths or years for secrets; minutes for a signed assertion, but the key persistsSingle use; a new verifier for every flow
Who can use itOnly confidential clients that can keep a credentialEvery client, public or confidential
Grants it applies toToken endpoint calls for all grants, plus introspection and revocationAuthorisation code grant only
Stops a stolen code being redeemed by a different clientYes, for confidential clients: the attacker lacks the credentialYes: the attacker lacks the verifier
Stops a stolen code being injected into the legitimate clientNo (RFC 9700 section 4.5)Yes (RFC 9700 section 4.5.3.1)
Provides CSRF protection on the redirectNoYes, once the client has confirmed the server supports PKCE (RFC 9700 section 4.7)
Stops client impersonation for app-only tokensYes, while the credential stays secretNo, not involved
Leaves a long-lived secret to sprawlYes for shared secrets; no for asymmetric keys held in a KMS or HSMNo

Threat model: client secret only

Architecture in words

A server-side web application redirects the browser to the authorisation endpoint with its client_id, redirect_uri, scope and state. The user signs in, the authorisation server sends a code to the redirect URI, and the application's backend exchanges the code at the token endpoint, authenticating with client_secret_basic or client_secret_post. The OAuth 2.1 draft says servers must support credentials in the request body and may support HTTP Basic.

What goes wrong

  • Code injection: an attacker obtains a victim's code (from a leaked redirect, a referrer header, browser history or a log) and starts their own login with the legitimate application, swapping in the stolen code at the redirect step. The application's backend redeems it with its own valid secret and logs the attacker into the victim's account. The secret worked exactly as designed, which is the point: it authenticated the right application, not the right transaction.
  • Login CSRF: without state correctly bound to the session, an attacker can push their own code into the victim's browser. PKCE or nonce gives the same protection as state, and RFC 9700 notes PKCE is robust even against an attacker who can read the authorisation response.
  • Secret theft: a shared secret sits in environment variables, CI variables, Helm values and support tickets. Whoever copies it can call the token endpoint as the application (MITRE ATT&CK T1552.001 Credentials In Files), including for app-only tokens with no user involved. The OAuth 2.1 draft notes that a confidential client whose credentials are stolen can be impersonated.
  • Credential addition: attackers with directory rights add their own secret to an existing application (T1098.001 Additional Cloud Credentials), which is invisible if nobody reviews application credentials.

Threat model: PKCE only

Architecture in words

The client generates a random code_verifier (43 to 128 characters, 256 bits of entropy recommended by RFC 7636), sends its S256 hash as code_challenge on the authorisation request, and sends the verifier with the code at the token endpoint. The server checks the hash. No client credential is presented.

What goes wrong

  • Any party can be the client: PKCE binds a code to whoever made the challenge, and an attacker can make their own challenge. For a public client, an attacker's script using the same client_id runs a complete, valid flow. That is why the OAuth 2.1 draft says redirect URI checks and similar measures do not authenticate a native client, and why consent should not be silently remembered for unauthenticated clients.
  • Phishing with a real client ID: the authorisation server cannot tell the real app from an impostor, so consent phishing and device or redirect lures work as well against PKCE-only clients as against any public client. See the Consent Phishing and Illicit OAuth Grants Guide.
  • Token theft after issuance: PKCE protects the code, not the tokens. Stolen access or refresh tokens need sender constraint (DPoP or mTLS) and refresh token rotation. See the DPoP Guide.
  • Downgrade and plain: a server that accepts a verifier when no challenge was sent, or accepts the plain method, can be downgraded. RFC 9700 section 4.8.2 requires rejecting the first; the OAuth 2.1 draft forbids plain.

For a confidential client, dropping client authentication because PKCE is present would turn it, in effect, into a public client: anyone with the client ID could run flows in its name and, if the client also has the client credentials grant, nothing would protect it at all.

What the standards require

Source and statusPKCE for confidential clientsClient authentication
RFC 7636 (Standards Track, September 2015)Written for public clients; S256 mandatory to implement on the server; plain should not be used in new implementationsNotes that secrets in client binaries cannot be considered confidential
RFC 9700, OAuth 2.0 Security BCP (BCP 240, January 2025)Section 2.1.1: confidential clients should use PKCE; OpenID Connect confidential clients may use nonce instead with the section 4.5.3.2 precautions; servers must support PKCESection 2.5: asymmetric methods (mTLS, private_key_jwt) recommended
OAuth 2.1, draft-ietf-oauth-v2-1-17 (Internet-Draft, 31 October 2026)Clients must use PKCE and servers must enforce it, except a confidential client where the server has reasonable assurance it implements nonce; PKCE still recommended then; plain forbiddenServer must require client authentication for confidential clients; asymmetric methods recommended; no shared secrets required of native apps
FAPI 2.0 Security Profile (Final, 22 February 2025)PKCE with S256 required, plus pushed authorisation requestsConfidential clients only; mTLS or private_key_jwt only
OWASP ASVS 5.0V10.4.6: server requires a valid code challenge, refuses plain and validates the verifierV10.4.16: confidential client with strong, replay-resistant public-key client authentication

OAuth 2.1 is still a working group draft (IESG state "I-D Exists" when checked on 14 November 2026), so treat its exact wording as subject to change. Its direction matches RFC 9700, which is already published. The draft also explains why PKCE is preferable to nonce: the server can refuse an injected code before issuing any token, whereas the nonce check happens in the client after tokens exist. See the OAuth 2.0 Security Best Current Practice (RFC 9700) Guide and the OAuth 2.1, GNAP and FAPI Guide.

Where private_key_jwt and mTLS fit

The question in the title is usually really two questions: should a confidential client add PKCE (yes), and should it keep a shared secret (preferably not). Replacing the secret with an asymmetric method answers the second without weakening the first.

  • private_key_jwt (RFC 7523): the client sends client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer and a signed JWT whose iss and sub are the client ID, whose aud identifies the authorisation server and which carries an exp. The server stores only a public key. See the JWT Client Authentication (private_key_jwt) Guide.
  • mTLS (RFC 8705): the client authenticates with a certificate during the TLS handshake, and the same certificate can sender-constrain the access token. See the mTLS Guide.
  • No stored credential: where the client runs on a cloud platform, workload identity federation can remove the long-lived credential altogether. See the Workload Identity Federation Guide.
Client patternPKCEClient authenticationNotes
Server-rendered web appYes, S256, per transactionprivate_key_jwt or mTLS; secret only as a transitional stepBind the verifier to the user's session
Backend for frontend for a single-page appYes, generated in the backendYes, the BFF is a confidential clientThe OAuth 2.1 draft recommends the BFF pattern for browser apps that want credentials. See the OAuth for Browser-Based Applications and the BFF Pattern Guide
Native mobile or desktop appMandatoryNone that the server should trust; dynamic registration per instance is an optionNever ship a shared secret in the binary
Agent or integration backend acting for a userYesprivate_key_jwt with a key in a KMS or HSMThe client credential is a privileged NHI
Machine-to-machine (client credentials)Not applicableAsymmetric or federatedSee the Machine-to-Machine OAuth (Client Credentials) Guide

Secret sprawl: the hidden cost of "secret only"

A confidential client's secret is a non-human identity credential with every permission the application holds. Unlike a PKCE verifier, which lives for one transaction in one process, a client secret is copied into every environment that runs the application, every pipeline that deploys it and often every developer laptop that tests it. RFC 7591 even allows a registration with no secret expiry. Each copy is a place to leak from and a place rotation must reach.

  • Count copies, not clients: one client secret commonly ends up in several stores. Measure it with the method in the Secret Sprawl Measurement Guide.
  • Rotation is the real price: shared secrets need overlapping rotation across every copy; asymmetric keys rotate by publishing a new public key while the old one is still accepted.
  • App permissions raise the stakes: a client that holds application permissions can obtain tokens with no user at all. In the Microsoft Midnight Blizzard breach attackers took over a legacy test OAuth application with elevated access and used application permissions to read executive mailboxes, a reminder that forgotten confidential clients are privileged identities. See the Client ID and Client Secret Guide for storage and leak response.

Decision criteria

  1. Is the grant the authorisation code grant? Yes: PKCE S256 on every request, whatever the client type. No (client credentials, token exchange): PKCE is not relevant.
  2. Can the client keep a credential confidential? Server-side code, a BFF or an agent backend: yes, register it as confidential and authenticate it. Code running on a user's device or in a browser: no, register it as public.
  3. Which credential? Prefer private_key_jwt or mTLS; use workload identity federation where the platform offers it; keep shared secrets only where the authorisation server supports nothing else, with expiry and an owner.
  4. OpenID Connect client relying on nonce? It meets RFC 9700's alternative, but add PKCE anyway: the server then rejects injected codes before tokens are issued, and you no longer depend on every client validating nonce correctly.
  5. Regulated or high-value API? Follow FAPI 2.0: PKCE S256, PAR, asymmetric client authentication and sender-constrained tokens. See the OAuth Pushed Authorization Requests and JAR Guide.

Small versus large organisations: a small team using a hosted identity provider can usually switch on "require PKCE" per application and move to a certificate or key credential in the application settings. A large estate needs an inventory first, because the risky clients are the old ones nobody owns, and a phased rollout because some legacy libraries do not send PKCE.

Worked scenario: a customer portal and its reporting backend

A company runs a server-rendered customer portal and a reporting backend, both confidential clients using shared secrets stored in CI variables and copied to three environments. A review finds the portal sends state but no PKCE, and the authorisation server accepts token requests with or without a verifier.

  1. Turn on PKCE in the client: the portal's OAuth library is configured to send S256 challenges; the verifier is stored server-side against the session.
  2. Observe: the authorisation server logs, per client, whether a code challenge was present, for two weeks.
  3. Enforce: once 100 percent of portal requests carry a challenge, the server is set to require PKCE for that client and to reject a verifier when no challenge was sent.
  4. Replace the secret: each client gets a key pair generated in the cloud KMS; the public key is registered, the clients switch to private_key_jwt, and the shared secrets stay valid for a one week overlap.
  5. Remove the fallback: the secrets are deleted from the authorisation server and from every CI store, and the clients' allowed authentication method is restricted to private_key_jwt.
  6. Roll back if needed: during steps 3 and 4 the previous setting can be restored per client within minutes; after step 5 rollback means issuing a new secret, which is logged and approved.

Detection and measurement

  • Token endpoint logs: record client ID, authentication method used, whether a code verifier was presented and whether the authorisation request carried a challenge.
  • Alerts: code redemption failures for verifier mismatch (possible injection attempts), verifier sent without challenge (downgrade probing), token requests for a client from unexpected networks, and new credentials added to an application.
  • Directory reviews: list every application credential with its type, creation date, expiry and owner.

Metrics:

  • Percentage of authorisation code clients with PKCE enforced by the server (target 100 percent).
  • Percentage of confidential clients using asymmetric or federated authentication instead of shared secrets.
  • Number of client secrets with no expiry or no owner.
  • Average number of stores holding each remaining client secret.
  • Count of servers or clients still accepting the plain method (target zero).

Framework mapping

FrameworkReferenceRelevance
NIST CSF 2.0PR.AA-01, PR.AA-03Managing client credentials; authenticating services
NIST SP 800-53 Rev. 5IA-9 Service Identification and Authentication; IA-5(2) Public Key-based AuthenticationAuthenticating the client application; asymmetric client credentials
NIST SP 800-53 Rev. 5SC-23 Session Authenticity; IA-5 Authenticator ManagementBinding the code to the transaction; managing secret lifecycle
OWASP ASVS 5.0V10.2.1, V10.4.6, V10.4.16CSRF protection by PKCE or state; server-side PKCE enforcement; strong client authentication
MITRE ATT&CKT1552.001 Credentials In Files; T1098.001 Additional Cloud Credentials; T1528 Steal Application Access TokenTheft and addition of client credentials; theft of tokens and codes
MITRE D3FENDD3-TB Token BindingSender-constrained tokens complement both controls

Common failure patterns

  • "We are confidential, so we skip PKCE": leaves code injection and, without strict state handling, login CSRF open.
  • Client-side PKCE without server enforcement: the client sends a challenge but the server never requires one, so an attacker simply omits it.
  • Dropping client authentication after adding PKCE: turns a confidential client into a public one in practice.
  • Secrets embedded in mobile or single-page apps "as well as PKCE": the secret is extractable and adds nothing; register the app as public or use a BFF.
  • Secrets that never expire and have no owner: the credential outlives the project and becomes the easiest way in.
  • Allowing fallback authentication methods: a client moved to private_key_jwt that still accepts its old secret has not been hardened.

Practitioner checklist

  • Inventory every OAuth client with its type (public or confidential), grants and authentication method.
  • Require PKCE S256 on the server for every authorisation code client, including confidential ones.
  • Reject verifiers sent without a challenge and refuse the plain method.
  • Require client authentication for every confidential client; never trust secrets in public clients.
  • Migrate confidential clients from shared secrets to private_key_jwt, mTLS or workload identity federation.
  • Restrict each client to one authentication method and remove fallbacks.
  • Give every remaining secret an owner, an expiry and a single authoritative store.
  • Add sender-constrained tokens for high-value APIs, since neither control protects issued tokens.

Standards and references

Related NHI Mgmt Group resources: PKCE Implementation · Client ID and Client Secret Guide · JWT Client Authentication (private_key_jwt) Guide · OAuth 2.0 Security Best Current Practice (RFC 9700) 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