Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Applied Cryptography for Developers  Module 11 – Tokens, OTPs and passkeys

PKCE, DPoP and certificate-bound tokens

Check a PKCE challenge against RFC 7636, verify DPoP proofs with a replay cache, and bind an access token to a key or a certificate with DPoP or mutual TLS.

  • Advanced
  • 20 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free intro, Pro: 4 of 20 minutes free
Free: PKCE: a stolen code works only for the app that holds the verifier. With a pass: the other 7 sections and 3 interview answers, about 16 minutes.

What you will learn

  • Generate and verify PKCE S256 challenges and check them against RFC 7636
  • Verify DPoP proofs against the request and the token, with a replay cache
  • Explain how DPoP and mutual TLS bind an access token to a key or a certificate
  • Choose between bearer, DPoP-bound and certificate-bound tokens for a given client and server setup

Before you start

What you’ll learn · Before you start

What you will learn

  • Generate and verify PKCE S256 challenges and check them against RFC 7636
  • Verify DPoP proofs against the request and the token, with a replay cache
  • Explain how DPoP and mutual TLS bind an access token to a key or a certificate
  • Choose between bearer, DPoP-bound and certificate-bound tokens for a given client and server setup

Before you start

On this page

An OAuth access token is a bearer secret, so a copy from a log, a proxy or a crash report works for anyone until it expires, often an hour. The authorization code that comes before it crosses the browser in an address, where other software on the device may read it. This lesson is about three ways to make a copy useless on its own.

PKCE ties an authorization code to a secret that only the app that started the login holds. DPoP ties an access token to a key pair, with a fresh signed proof for every request. Certificate-bound tokens, which use mutual TLS, tie an access token to the client’s TLS certificate. Each runs here against the examples printed in RFC 7636, RFC 9449 and RFC 8705.

PKCE: a stolen code works only for the app that holds the verifier

In the authorization code flow the person signs in at the authorization server, and the browser carries a short-lived code back to the app. Whoever holds the code can ask the token endpoint for tokens. PKCE, short for proof key for code exchange and defined in RFC 7636, closes that gap. Before the request, the app draws a random The random secret that an app draws for one login in PKCE and keeps to itself until it asks for tokens., 32 random bytes written as 43 characters. It sends only the SHA-256 hash of the verifier, the code challenge, with the authorization request, and it sends the verifier itself later, with the code, to the token endpoint. The server hashes the verifier it receives and compares the result with the challenge it stored. RFC 9700, the best current practice for OAuth, requires PKCE for public clients and recommends it for confidential ones. Step through one login:

Interactive · message by message A copied code is no use without the verifier Step through one login. Another app on the device can read what comes back in the redirect, but not what the client kept.
A copied code is no use without the verifier, step 6 of 6. Messages so far: Client app to Authorization server: code_challenge, S256; Authorization server to Client app: the code; Other app on the device to Authorization server: code, no verifier; Client app to Authorization server: code + code_verifier.Client appkeeps the verifierAuthorization servercode + challengeOther app on the devicecan read the redirectcode_challenge, S256the codecode, no verifiercode + code_verifier

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The client draws a secret. Before it sends anyone anywhere, the app draws 32 random bytes and writes them as the 43-character code verifier. It keeps the verifier in memory for this one login.
  2. It sends only a hash. The authorization request carries the code challenge, the Base64url SHA-256 hash of the verifier, and the method S256. The hash cannot be turned back into the verifier.
  3. The server stores it. After the person signs in, the server issues a code and writes the challenge beside it. The code alone is not yet enough to get tokens.
  4. The code comes back by redirect. The code returns through the browser as part of an address, and software on the same device may be able to read an address of this kind.
  5. A copy is tried. The other app asks the token endpoint for tokens with the copied code. It has no verifier, because the verifier never left the client.
  6. The client proves itself. The client sends the code and the verifier. The server hashes the verifier, finds the stored challenge and, because they match, issues the tokens to the app that started the login.

The program uses the example of RFC 7636 Appendix B. Its 32 bytes become a 43-character verifier, and the SHA-256 hash of that text is the challenge the RFC prints:

PKCE with the example of RFC 7636, Appendix B JavaScript · pkce-vector-js/pkce_vector.mjs
// PKCE with the example of RFC 7636, Appendix B: 32 random bytes become the code verifier, and the SHA-256 hash of the
// verifier, written in Base64url, is the code challenge. The challenge travels in the authorization request; the
// verifier only goes to the token endpoint, with the code.
import { createHash } from 'node:crypto';

// The RFC's 32 bytes. A real client draws them from the CSPRNG for every login (crypto.randomBytes(32)).
const bytes = Buffer.from([116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141, 121]);
const verifier = bytes.toString('base64url');
const challenge = (v) => createHash('sha256').update(v, 'ascii').digest('base64url');
const published = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';

console.log(`random bytes    ${bytes.length}`);
console.log(`code_verifier   ${verifier}  (${verifier.length} characters)`);
console.log(`code_challenge  ${challenge(verifier)}  (${challenge(verifier).length} characters)`);
console.log(`the RFC's value ${published}  same: ${challenge(verifier) === published}`);
console.log(`syntax of RFC 7636 section 4.1 (43 to 128 of A-Z a-z 0-9 - . _ ~): ${/^[A-Za-z0-9\-._~]{43,128}$/.test(verifier)}`);
console.log('');

// The token endpoint stored the challenge with the code. A copy of the code is no use without the verifier.
const stored = challenge(verifier);
const accepts = (candidate) => challenge(candidate) === stored;
console.log(`token request with the client's verifier       ${accepts(verifier) ? 'tokens issued' : 'invalid_grant'}`);
console.log(`token request with the code and no verifier    ${accepts('') ? 'tokens issued' : 'invalid_grant'}`);
console.log(`token request with the challenge as verifier   ${accepts(stored) ? 'tokens issued' : 'invalid_grant'}`);

Output

random bytes    32
code_verifier   dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk  (43 characters)
code_challenge  E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM  (43 characters)
the RFC's value E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM  same: true
syntax of RFC 7636 section 4.1 (43 to 128 of A-Z a-z 0-9 - . _ ~): true

token request with the client's verifier       tokens issued
token request with the code and no verifier    invalid_grant
token request with the challenge as verifier   invalid_grant

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node pkce_vector.mjs

Both values match the RFC. The challenge is one-way, so the authorization request, which another app may read, carries something that cannot be turned back into the verifier. A token request with the copied code and no verifier, or with the challenge in place of the verifier, was refused. PKCE protects the code. DPoP and certificate-bound tokens protect the access token that the code is exchanged for. A token bound with DPoP works only with a signature, made for each request, by the key it was issued to. A token bound to a certificate works only on a mutual TLS connection, where the client presents that certificate during the handshake as well as the server.

Checkpoint

Check: What PKCE sends and why a copied code fails

  1. Question 1 of 2 Which value does the app send with the authorization request?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The code challenge, the SHA-256 hash of the verifier

    The app keeps the verifier to itself and sends it only with the code, to the token endpoint. The challenge stored by the server is what the verifier is checked against.

  2. Question 2 of 2 Another app reads the code challenge from the redirect and sends a token request with it as the verifier. Why is that refused?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The server hashes what it receives as the verifier, and the hash of the challenge is not the challenge it stored

    The client's verifier is the only input whose SHA-256 hash equals the stored challenge. A copy of the challenge or of the code does not help.

Flashcards · 4 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why is a bearer access token a problem, and what are the three ways to bind it?

    A copy from a log, a proxy or a crash report works for anyone until it expires, often an hour. PKCE ties an authorization code to a secret that only the app that started the login holds. DPoP ties an access token to a key pair, with a fresh signed proof for every request. Certificate-bound tokens, using mutual TLS, tie an access token to the client's TLS certificate.

  2. How does PKCE protect an authorization code?

    Before the request the app draws a random code verifier, 32 random bytes written as 43 characters, and sends just the SHA-256 hash of it, the code challenge, with the authorization request. It sends the verifier itself later, with the code, to the token endpoint, and the server hashes it and compares. RFC 7636 defines it; RFC 9700 requires it for public clients and recommends it for confidential ones.

  3. Why can the PKCE challenge travel in an address another app may read?

    It is one-way: the hash cannot be turned back into the verifier. A token request with the copied code and no verifier, or with the challenge in place of the verifier, was refused. RFC 7636's example matched: the 43-character verifier hashes to the challenge the RFC prints.

  4. What does PKCE protect, and what do DPoP and certificate binding protect?

    PKCE protects the authorization code. DPoP and certificate-bound tokens protect the access token it is exchanged for: a DPoP-bound token works only with a signature, made for each request, by the key it was issued to; a certificate-bound token works only on a mutual TLS connection, where the client presents that certificate in the handshake.

PKCE on the server: what to enforce, and the downgrade it must refuse Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • DPoP: what the client signs and the checks a server runs
    • Check a DPoP proof against the request and the token
  • Certificate-bound tokens: the thumbprint in the token must match the TLS connection
  • Choosing between bearer, DPoP-bound and certificate-bound tokens
  • Practise: bind, check and refuse
  • Interview questions
    • What does PKCE stop, what is the code verifier, and what does the authorization server store?
    • Your authorization server supports PKCE but keeps it optional for older clients. A review says it can still be attacked. Explain how, and change the server so that old clients keep working where they can.
    • Your API accepts DPoP-bound access tokens. A log leak exposes some tokens, and some captured requests with their proofs. Explain what a finder can and cannot do, which checks keep it that way, and how you stop replay across many servers.
    • Your team wants certificate-bound tokens, but all TLS ends at a cloud load balancer and the services behind it learn the client certificate from a header. What must be true for this to be safe, and what breaks at the next certificate renewal?
  • Key takeaways

1 diagram · 3 runnable examples (JavaScript) · 1 exercise with sample tests · 3 interview answers · about 43 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Terms in this lesson

certificate-bound token
An access token tied to a client's TLS certificate through the SHA-256 hash of the certificate (RFC 8705). It is accepted only on a connection that presents that certificate.
code verifier
The random secret an app draws for one login in PKCE: 43 to 128 characters, usually 32 random bytes in Base64url. Only its SHA-256 hash, the code challenge, goes with the authorization request.
DPoP
Demonstrating Proof of Possession (RFC 9449). The client signs a short proof for each request with a private key, and the access token is bound to the matching public key.
DPoP proof
The signed JWT a DPoP client sends in the DPoP header. Its header holds the client's public key and its claims name the method, the address, the time, a unique ID and, with a token, the token's hash.
JWK thumbprint
The SHA-256 hash of a JWK's required members in alphabetical order (RFC 7638), written in Base64url. It names a key without carrying it; DPoP stores it in a token as jkt.
mutual TLS
TLS in which the client proves its identity with a certificate as well as the server, so each side authenticates the other during the handshake.
PKCE
Proof Key for Code Exchange (RFC 7636). The app sends a hash of a random secret with the authorization request and the secret itself with the code, so a copied code is refused.
proof of possession
A check that the sender of a request holds a key or a secret, not only a copy of a token. Without one, anyone holding a bearer token can use it.
sender-constrained token
An access token that works only for the client it was issued to, because the server also checks a key or a certificate of the sender. DPoP-bound and certificate-bound tokens are of this kind.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.