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

OAuth 2.0 PKCE — Proof Key for Code Exchange

cryptography PHP 7.0+ Intermediate

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 34
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 0 pings S 2 pings M 0 pings T 0 pings W 0 pings T 1 ping F 2 pings S 0 pings S 0 pings M 0 pings T 1 ping W 0 pings T 1 ping F 1 ping S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T
No pings yet today
No pings yesterday
Amazonbot 14 Perplexity 6 Unknown AI 3 Ahrefs 3 Google 2 ChatGPT 1 SEMrush 1
crawler 29 crawler_json 1
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