Join our Newsletter — 33% off our NHI Course
Home› Guides› PKCE Implementation: Get RFC 7636 Right on Clients…
Guide Authentication

PKCE Implementation: Get RFC 7636 Right on Clients and Servers

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

Most OAuth deployments now say they "use PKCE", yet many still accept the plain method, skip the downgrade check or reuse one verifier across flows, which leaves authorisation codes open to interception and injection. Proof Key for Code Exchange (PKCE, RFC 7636) binds an authorisation code to the client instance that asked for it, so a stolen code is useless without a secret only that instance holds. This guide goes deeper than the overview in the OAuth 2.0 and OpenID Connect Guide: it covers the exact parameters and limits in RFC 7636, the extra server duties added by RFC 9700, how OAuth 2.1, FAPI 2.0 and the MCP authorisation specification tighten them, and how to test and evidence a correct implementation. It is written for identity engineers, application developers and assessors who build or review authorisation servers and clients.

Key takeaways

  • PKCE is no longer only for mobile apps. RFC 9700 says public clients MUST use PKCE and recommends it for confidential clients, and the OAuth 2.1 draft makes it part of the authorisation code grant for everyone.
  • S256 is the only method worth supporting. The plain method sends the verifier itself in the front channel; OWASP ASVS 5.0 requirement V10.4.6 says the server must not accept plain at all.
  • The server side is where most bugs live. Authorisation servers must enforce the verifier whenever a challenge was sent and must reject a code_verifier when no challenge was sent, which is the PKCE downgrade defence in RFC 9700 section 4.8.2.
  • One verifier per transaction, bound to the session. The verifier needs 256 bits of entropy from a cryptographic random generator, must never be reused or hard coded, and must be tied to the user agent session that started the flow.
  • PKCE protects the code, not the tokens. Once tokens are issued, theft of the access or refresh token is a separate problem that needs sender-constrained tokens such as DPoP or mutual TLS and refresh token rotation.

What it is and who it applies to

RFC 7636 was published by the IETF OAuth Working Group in September 2015 as a Proposed Standard. It was written to stop the authorisation code interception attack on native apps, where a malicious app registered for the same custom URI scheme receives the redirect and redeems the code. The mechanism is simple: the client creates a random secret (the code_verifier), sends a transformed version of it (the code_challenge) with the authorisation request, and proves possession of the original at the token endpoint. An attacker who sees only the redirect has the code but not the verifier.

Its scope has widened since 2015. RFC 8252 (BCP 212, October 2017) made PKCE mandatory for public native app clients. RFC 9700, the OAuth 2.0 Security Best Current Practice published in January 2025 as BCP 240, extended the requirement to all public clients, recommended it for confidential clients, and added duties for authorisation servers. As checked on 8 November 2026, the OAuth 2.1 specification is still an Internet-Draft (draft-ietf-oauth-v2-1-17, dated 31 October 2026, a working group document) and states that PKCE is required in OAuth 2.1; treat the draft's detailed wording as subject to change until it is published as an RFC.

PartyRequirementSource
Public clients (SPAs, native and desktop apps, CLIs)MUST use PKCERFC 9700 section 2.1.1; RFC 8252 section 6 for native apps
Confidential clients (server-side web apps)PKCE RECOMMENDED; OpenID Connect clients may use nonce instead, if they validate it before using any tokenRFC 9700 section 2.1.1
Authorisation serversMUST support PKCE, enforce the verifier when a challenge was sent, and mitigate downgradeRFC 9700 sections 2.1.1 and 4.8.2
FAPI 2.0 clients and serversServers MUST require PKCE with S256; clients MUST use PKCE with S256FAPI 2.0 Security Profile (Final, February 2025) section 5.3.2.2 and 5.3.3.2
MCP clientsMUST implement PKCE, use S256 when technically capable, and refuse to proceed if the server's metadata does not advertise code_challenge_methods_supportedMCP authorisation specification 2025-11-25

See the OAuth 2.0 and OpenID Connect Guide for the flows themselves and the OAuth 2.0 Security Best Current Practice (RFC 9700) Guide for the wider RFC 9700 requirements.

Structure and key requirements

PKCE adds three parameters and one check to the authorisation code grant. Getting each detail right matters because interoperability bugs are usually silent: a server that ignores an unknown parameter will happily issue an unbound code.

ElementWhere it is sentExact rule (RFC 7636)
code_verifierKept by the client, sent in the token request43 to 128 characters from the unreserved set A-Z, a-z, 0-9, "-", ".", "_", "~" (section 4.1). Recommended: 32 random octets, base64url encoded without padding, giving 43 characters and 256 bits of entropy
code_challengeAuthorisation request (front channel)For S256: BASE64URL(SHA256(ASCII(code_verifier))). For plain: equal to the verifier (section 4.2)
code_challenge_methodAuthorisation request"S256" or "plain". If omitted, the server treats it as plain (section 4.3), so clients should always send S256 explicitly
Server bindingAuthorisation server stateThe server associates the challenge and method with the issued code, either stored server side or encrypted inside the code, and must not expose the challenge in extractable form (section 4.4)
VerificationToken endpointServer recomputes the challenge from the verifier and compares; mismatch returns invalid_grant (section 4.6)
Missing or unsupported challengeAuthorisation endpointWhere PKCE is required, return invalid_request (section 4.4.1)

S256 versus plain

S256 is mandatory to implement on servers. RFC 7636 allows plain only when the client cannot perform SHA-256 for a technical reason and knows out of band that the server supports plain, and says clients must not fall back to plain after an S256 attempt fails, because that failure suggests a faulty server or an active downgrade. In practice every current platform can compute SHA-256, so there is no legitimate reason left to accept plain. RFC 9700 notes that S256 is currently the only method that does not expose the verifier in the authorisation request, and ASVS 5.0 V10.4.6 requires servers to refuse plain.

The server duties RFC 9700 added

  • Enforce when present: if the authorisation request carried a valid code_challenge, the token request must carry a matching code_verifier.
  • Reject when absent: if the authorisation request had no code_challenge, a token request that includes a code_verifier must be rejected. Without this, an attacker can strip the challenge from a victim's request, obtain an unbound code and inject it into the victim's session (the downgrade attack in section 4.8.1). Servers that require PKCE for every client meet this implicitly.
  • Advertise support: the server must give clients a way to detect PKCE support, and publishing code_challenge_methods_supported in RFC 8414 authorisation server metadata is the recommended way. RFC 8414 states that if the field is omitted, the server does not support PKCE.
  • Watch for constant values: servers are encouraged to detect and prevent clients that send the same challenge on every request, which defeats the purpose.

PKCE, state and nonce

RFC 9700 section 4.7.1 lets a client rely on PKCE for cross-site request forgery protection only after it has confirmed the server supports PKCE; otherwise it must use a one-time state value or the OpenID Connect nonce. Many teams keep state anyway, because it also carries application context and costs nothing. Nonce does not protect public clients against code injection, so it is never a substitute for PKCE in a SPA or mobile app.

Identity and NHI requirements

PKCE is a user-delegation control, but the clients that use it are increasingly non-human: command line tools, desktop agents, IDE plug-ins and AI agents acting for a user. These are all public clients, so they cannot hold a client secret and PKCE is the main thing standing between an intercepted redirect and a working token.

  • CLIs and desktop tools: use the loopback redirect (http://127.0.0.1:{port} or http://[::1]:{port}) with an ephemeral port, as RFC 8252 section 7.3 describes, and the system browser rather than an embedded web view (section 8.12). Any local process can listen on a loopback port, so PKCE is what stops a hostile process from redeeming a code it catches. See the OAuth for Native and Mobile Apps Guide.
  • AI agents and MCP clients: the MCP authorisation specification requires S256 and requires clients to check code_challenge_methods_supported and refuse to continue when it is missing. It also warns that a malicious client can present a legitimate client's metadata URL with its own localhost redirect, so authorisation servers should show the redirect host clearly. See the MCP Authorisation Specification Guide.
  • Browser-based apps: a SPA can run PKCE in the browser, but tokens then live in JavaScript reach. The backend-for-frontend pattern keeps the code exchange and tokens on a confidential server, which still benefits from PKCE. See the OAuth for Browser-Based Applications and the BFF Pattern Guide.
  • Machine-to-machine flows: the client credentials grant has no authorisation code, so PKCE does not apply. Do not try to bolt it on; protect those clients with private_key_jwt or mutual TLS. See the Machine-to-Machine OAuth (Client Credentials) Guide.

Treat each public client registration as an identity in your inventory: record its owner, redirect URIs and whether the server enforces PKCE for it. A forgotten test client with PKCE disabled is a common bypass.

Implementation steps

Client side

  1. Generate the verifier per transaction. Take 32 bytes from a cryptographically secure generator (for example crypto.getRandomValues in browsers or the secrets module in Python), base64url encode without padding. Never derive it from a timestamp, user ID or session ID, and never ship a fixed value in the app.
  2. Bind it to the session. Store the verifier where only the initiating session can read it: server-side session for web apps, memory or session storage scoped to the tab for SPAs, protected app storage for mobile. Delete it after the token exchange, success or failure.
  3. Compute and send the challenge. SHA-256 over the ASCII verifier, base64url encode, and send code_challenge plus code_challenge_method=S256 explicitly on every request. With PAR, put them in the pushed request body. See the OAuth Pushed Authorization Requests and JAR Guide.
  4. Check server support first. Read code_challenge_methods_supported from /.well-known/oauth-authorization-server (or the OpenID configuration) and refuse to start if S256 is not listed, as the MCP specification requires.
  5. Exchange and clean up. Send code_verifier with the code at the token endpoint, never fall back to plain on error, and log the outcome without logging the verifier or code.

Server side

  1. Advertise only S256. Publish code_challenge_methods_supported: ["S256"] in metadata and remove plain from the accepted list.
  2. Bind and protect. Store the challenge and method with the code record (or encrypt them into the code), and keep codes short-lived and single use as RFC 6749 already expects.
  3. Enforce in audit mode first. Log, per client, requests that arrive without a challenge or with plain. Use the data to find clients that will break.
  4. Require per client, then globally. Turn on "require PKCE" for public clients first, then confidential clients in rings (internal apps, then partners), with a dated exception list owned by named people.
  5. Add the downgrade check. Reject token requests that carry a code_verifier when the code was issued without a challenge, and test it explicitly, because a server that "supports" PKCE often lacks this.
  6. Roll back safely. Keep enforcement as a per-client flag so a broken integration can be exempted for a fixed period without disabling PKCE platform-wide, and record every exemption.
SituationChooseWhy
New public client of any kindPKCE with S256, mandatoryRequired by RFC 9700 and RFC 8252; no client secret to fall back on
Existing confidential web app using OpenID ConnectAdd PKCE; keep nonce validationPKCE also covers code injection where nonce checks are missed; low cost
Open ecosystem or high value API (banking, health)FAPI 2.0 profile: PKCE S256 plus PAR plus sender-constrained tokensFAPI 2.0 makes all three mandatory
Small organisation on a managed identity providerTurn on the provider's "require PKCE" setting for every app registrationMost of the work is configuration and testing, not code
Large estate with legacy clientsAudit mode, per-client enforcement rings, dated exceptionsAvoids a single cut-over that breaks partner integrations

Evidence and assessment

Assessors should test behaviour, not read configuration screens. The following negative tests show whether a server really enforces PKCE:

TestExpected result
Authorisation request with no code_challenge for a client marked PKCE-requiredinvalid_request error, no code issued
code_challenge_method=plainRejected (ASVS 5.0 V10.4.6)
Token request with the wrong code_verifierinvalid_grant
Token request with no code_verifier after a challenge was sentRejected
Code issued without challenge, token request with a code_verifier (downgrade)Rejected (RFC 9700 section 4.8.2)
Same code redeemed twiceSecond attempt rejected; ideally tokens from the first are revoked
Verifier shorter than 43 or longer than 128 characters, or with illegal charactersRejected

Metrics that show it is working:

  • Enforcement coverage: percentage of client registrations with PKCE required (target 100% of public clients, rising for confidential clients).
  • Plain method usage: count of authorisation requests using plain or no method per week (target zero).
  • Verifier failures: rate of invalid_grant errors caused by PKCE mismatch per client; a spike can indicate a buggy release or an injection attempt.
  • Downgrade rejections: number of token requests rejected because a verifier was sent without a challenge.
  • Repeated challenges: clients sending the same code_challenge value across transactions.
  • Open exceptions: number and age of clients exempted from enforcement.

Keep the server's token endpoint logs with client_id, grant type, PKCE result and error code, so that these metrics and incident investigations do not depend on application logs.

Mapping to other frameworks

FrameworkItemHow PKCE relates
OWASP ASVS 5.0V10.4.6Authorisation server requires a valid code_challenge, refuses plain and validates code_verifier
OWASP ASVS 5.0V10.2.1Client CSRF protection on the code flow via PKCE or the state parameter
OWASP ASVS 5.0V10.1.2code_verifier, state and nonce must be unguessable, transaction-specific and bound to the session
NIST SP 800-53 Rev. 5IA-8, IA-2Identification and authentication of users through federated, delegated flows
NIST SP 800-53 Rev. 5SC-23 Session AuthenticityBinding the code to the session that requested it
NIST SP 800-53 Rev. 5SC-8 Transmission Confidentiality and IntegrityTLS on all endpoints, which RFC 7636 section 7.5 still assumes
NIST CSF 2.0PR.AA-03Users, services and hardware are authenticated
MITRE ATT&CKT1528 Steal Application Access Token; T1550.001 Application Access TokenPKCE blocks the code-to-token step of token theft through intercepted redirects; it does not stop use of tokens already stolen
FAPI 2.0 Security Profile5.3.2.2, 5.3.3.2PKCE with S256 mandatory for servers and clients

Common pitfalls

  • Accepting plain or an omitted method. Because a missing code_challenge_method defaults to plain, a server that accepts the default is accepting the verifier in clear in the browser history and proxy logs.
  • No downgrade check. The server enforces PKCE only when a challenge appears, and never rejects a verifier sent for an unbound code. Penetration tests regularly find this in home-grown servers and older libraries.
  • Static or reused verifiers. A verifier generated once at app start, compiled into a mobile build or shared across tabs gives an attacker who learns it every future code. ASVS V10.1.2 explicitly requires transaction-specific values.
  • Weak randomness. Math.random or time-seeded generators produce guessable verifiers; use the platform's cryptographic generator only.
  • Treating PKCE as token protection. Teams switch off refresh token rotation or sender-constraining because "we have PKCE". The Salesloft Drift breach shows what stolen OAuth tokens do once issued, which PKCE cannot prevent. See the DPoP Guide and the OAuth Refresh Token Rotation and Expiry Guide.
  • Exempt test and legacy clients. A tenant-wide "PKCE required" switch with per-app overrides often leaves old registrations exempt and forgotten; review overrides as part of client inventory.

Practitioner checklist

  • Publish code_challenge_methods_supported with only S256 in authorisation server metadata.
  • Require PKCE for every public client now, and set a date for confidential clients.
  • Reject plain and missing methods, and return invalid_request where PKCE is required but absent.
  • Implement and test the downgrade check: no code_verifier accepted for a code issued without a challenge.
  • Generate a fresh 32-byte verifier per transaction from a cryptographic generator, bound to the initiating session, deleted after use.
  • Have clients read server metadata and refuse to start if S256 is not supported; never fall back to plain.
  • Run the negative test table in every release pipeline for the authorisation server and key clients.
  • Track enforcement coverage, plain usage, downgrade rejections and open exceptions monthly.
  • Pair PKCE with sender-constrained tokens and refresh token rotation for public clients.

Standards and references

Related NHI Mgmt Group resources: OAuth 2.0 and OpenID Connect Guide · OAuth 2.0 Security Best Current Practice (RFC 9700) Guide · OAuth 2.1, GNAP and FAPI Guide · Top 10 OAuth Security Risks

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 8 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