← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

OAuth 2.0 PKCE — Proof Key for Code Exchange

Cryptography PHP 7.0+ Intermediate
debt(d5/e3/b3/t7)
d5 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'specialist tool catches it' (d5). The detection_hints list semgrep as the tool, with automated=yes and code patterns for missing code_challenge, plain method, or implicit flow. Semgrep is a specialist SAST tool — not a default linter — so misuse won't be caught by a compiler or standard linter, but can be caught with a configured semgrep rule.

e3 Effort Remediation debt — work required to fix once spotted

Closest to 'simple parameterised fix' (e3). The quick_fix is essentially a pattern replacement: generate fresh random_bytes(32) per request, compute BASE64URL(SHA256(verifier)), set method to S256. Common mistakes (plain method, weak verifier, reuse) are each one-to-a-few line corrections within the OAuth flow initiation code. This is contained within one component but requires touching the auth flow correctly, so slightly more than a one-liner (e1) but well within a single component.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3). The PKCE implementation is scoped to the OAuth authorisation flow — the code_verifier/challenge generation and storage per request. Applies_to is web context only. Once correctly implemented it doesn't significantly shape other areas of the codebase; future maintainers only feel the weight when modifying the OAuth flow itself.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field states that developers believe PKCE is only needed for mobile/public clients, when OAuth 2.1 mandates it for all clients including confidential server-side apps. Additionally, using code_challenge_method=plain (which many docs show as an option) looks equivalent to S256 but completely defeats the protection — contradicting how developers expect a 'method parameter' to provide equivalent security. These are documented gotchas that contradict intuition from reading other OAuth guides.

About DEBT scoring →

Also Known As

PKCE Proof Key for Code Exchange pixy RFC 7636 code verifier code challenge

TL;DR

An OAuth 2.0 extension that prevents authorisation code interception attacks in public clients (SPAs, mobile apps) by binding each authorisation request to a cryptographic secret the client generates.

Explanation

PKCE (RFC 7636, pronounced 'pixy') was designed for public clients that cannot securely store a client_secret — native mobile apps and single-page applications. Without PKCE, if an attacker intercepts the authorisation code (via a malicious app registered to the same redirect URI, or a URL leak), they can exchange it for tokens. PKCE binds the authorisation request to a secret the client controls. Flow: (1) Client generates a random `code_verifier` (43–128 chars, URL-safe). (2) Client computes `code_challenge = BASE64URL(SHA256(code_verifier))`. (3) Client sends `code_challenge` and `code_challenge_method=S256` with the authorisation request. (4) Auth server stores the challenge. (5) Client sends `code_verifier` with the token request. (6) Auth server hashes it and compares to the stored challenge — only the original client can pass. OAuth 2.1 mandates PKCE for all clients including confidential ones, making it the universal standard.

Diagram

sequenceDiagram
    participant App as Client App
    participant Auth as Auth Server
    participant API as Resource API
    App->>App: Generate code_verifier + SHA256 challenge
    App->>Auth: Auth request + code_challenge + S256
    Auth->>App: Redirect with auth code
    App->>Auth: Token request + code + code_verifier
    Auth->>Auth: SHA256(verifier) == stored challenge?
    Auth->>App: Access token + refresh token
    App->>API: Request with access token

Common Misconception

PKCE is only needed for mobile apps — OAuth 2.1 mandates PKCE for all OAuth clients including server-side web apps with a client_secret. PKCE adds defence-in-depth even for confidential clients because it prevents code interception regardless of whether the client_secret is secure.

Why It Matters

The implicit flow (historically used by SPAs) was deprecated because tokens in URL fragments are exposed in browser history and referrer headers. PKCE enables SPAs to use the authorisation code flow safely without a client_secret, delivering the security of server-side OAuth to browser and mobile clients.

Common Mistakes

  • Using `code_challenge_method=plain` instead of `S256` — plain sends the verifier unhashed, meaning interception of the challenge is enough to complete the attack.
  • Generating a weak `code_verifier` (too short or low entropy) — must be 43–128 chars of cryptographically random URL-safe characters.
  • Reusing `code_verifier` across requests — each authorisation request must use a fresh, unique verifier.
  • Still using the implicit flow for SPAs — the implicit flow is deprecated; use PKCE authorisation code flow instead.

Code Examples

💡 Note
The verifier is stored server-side in the session; only the SHA-256 challenge travels in the URL, making interception useless.
✗ Vulnerable
// Plain method — challenge equals verifier, no security benefit:
$codeVerifier = 'my-static-verifier'; // weak, static, reused
$codeChallenge = $codeVerifier;       // plain method — trivially reversed

$authUrl = 'https://auth.example.com/oauth/authorize'
    . '?code_challenge=' . $codeChallenge
    . '&code_challenge_method=plain'; // insecure
✓ Fixed
// Correct PKCE with S256:
function generatePKCE(): array {
    // 32 bytes = 43 base64url chars after encoding (within 43-128 range)
    $verifierBytes = random_bytes(32);
    $codeVerifier = rtrim(strtr(base64_encode($verifierBytes), '+/', '-_'), '=');

    $challengeBytes = hash('sha256', $codeVerifier, true);
    $codeChallenge = rtrim(strtr(base64_encode($challengeBytes), '+/', '-_'), '=');

    return [
        'code_verifier'  => $codeVerifier,  // store in session
        'code_challenge' => $codeChallenge, // send to auth server
    ];
}

$pkce = generatePKCE();
$_SESSION['code_verifier'] = $pkce['code_verifier'];

$authUrl = 'https://auth.example.com/oauth/authorize'
    . '?response_type=code'
    . '&code_challenge=' . $pkce['code_challenge']
    . '&code_challenge_method=S256';

// At token exchange:
// POST code_verifier = $_SESSION['code_verifier'] — server hashes and compares

Added 24 Mar 2026
Views 74
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 2 pings F 0 pings S 1 ping S 2 pings M 0 pings T 0 pings W 1 ping T 0 pings F 1 ping S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 0 pings F 2 pings S 0 pings S 1 ping M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Amazonbot 16 Perplexity 6 Ahrefs 5 Google 4 SEMrush 4 Unknown AI 3 Scrapy 3 Bing 2 ChatGPT 1 Claude 1 Meta AI 1 Majestic 1 Sogou 1 PetalBot 1
crawler 47 crawler_json 2
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: Medium
⚡ Quick Fix
Generate a fresh `random_bytes(32)` verifier per request, compute `BASE64URL(SHA256(verifier))` as the challenge, use `S256` method — never `plain`
📦 Applies To
PHP 7.0+ web Laravel Symfony
🔗 Prerequisites
🔍 Detection Hints
OAuth flow without code_challenge parameter; code_challenge_method=plain; implicit flow (response_type=token)
Auto-detectable: ✓ Yes semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: High False Positives: Low ✗ Manual fix Fix: Medium Context: Function Tests: Update
CWE-287 CWE-345


✓ schema.org compliant