ASK KNOX
beta
LESSON 251

PKCE — Why Modern Apps Don't Use Implicit Flow

The Implicit flow put access tokens in URL fragments — visible in browser history and referrer headers. PKCE replaced it with a cryptographic handshake that works without a client secret. One config line brought down 8 hours of debugging.

6 min read

The Implicit flow was invented in 2012 to solve a real problem: browser-based apps (SPAs) cannot safely store a client secret. The app bundle is public — anyone can view the source, decompile the bundle, and extract any secret baked into it.

The Implicit flow's solution was to skip the client secret entirely and return the access token directly in the URL fragment after the user authenticates:

https://your-app.com/callback#access_token=eyJ...&token_type=bearer

This worked. It also created a different problem: access tokens now lived in the URL, which means they lived in browser history, server logs, referrer headers, and were readable by any JavaScript on the page.

RFC 7636 (PKCE — Proof Key for Code Exchange, pronounced "pixy") replaced Implicit with a better solution.

The PKCE Mechanism

PKCE solves the "no client secret" problem without putting tokens in URLs. Instead, it uses a one-time cryptographic challenge-response.

┌──────────┐    code_challenge      ┌───────────────┐
│  Client  │ ──────────────────►   │  Auth Server  │
│          │                        │   (GoTrue)    │
│ stores   │                        │               │
│ verifier │    ?code=abc123        │  stores       │
│ locally  │ ◄──────────────────   │  challenge    │
│          │                        │               │
│          │  code + verifier       │               │
│          │ ──────────────────►   │  verifies:    │
│          │                        │  SHA256(v)==c │
│          │    access_token        │               │
│          │ ◄──────────────────   │               │
└──────────┘                        └───────────────┘
Step 1: Generate a random code_verifier
        code_verifier = base64url(random_bytes(32))
        Example: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

Step 2: Compute the code_challenge
        code_challenge = base64url(SHA256(code_verifier))
        Example: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

Step 3: Send the challenge to the auth server
        GET /authorize
          ?code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
          &code_challenge_method=S256
          &response_type=code
          &client_id=...

Step 4: Auth server redirects back with an authorization code
        GET /callback?code=AUTHORIZATION_CODE

Step 5: Exchange the code + the raw verifier for a token
        POST /token
          code=AUTHORIZATION_CODE
          code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
          grant_type=authorization_code

Step 6: Auth server verifies SHA256(verifier) == stored challenge
        If it matches → token issued
        If it doesn't → request rejected

The security property: an attacker who intercepts the authorization code from the URL cannot exchange it for a token without the code_verifier. The verifier was never transmitted — it only lives in the app's memory during the flow. The attacker has the code but not the verifier. The code is useless without it.

Why the Verifier Storage Matters

The code_verifier must survive the redirect. The flow starts on your site, redirects to the auth server, then redirects back to your site. In a browser, that means two full page navigations.

In a server-side app (SSR): The verifier can be stored in a server-side session, keyed to the browser's session cookie. It survives the redirects because the server holds it.

In a browser-only app (SPA/static): The verifier must be stored in sessionStorage or localStorage before the redirect, then read back after the callback. This works, but only if the callback happens on the same origin as where the flow started.

This is a subtle but critical constraint. If your app starts the PKCE flow on www.jeremyknox.ai but the redirect goes to academy.jeremyknox.ai/auth/callback, the verifier stored in www's sessionStorage is not accessible on academy. Different origins. The exchange fails.

Case Study: flowType: 'implicit' vs GoTrue's PKCE Response

In April 2026, the jeremyknox.ai auth system had a silent, hard-to-debug failure.

The Supabase client was initialized with the default configuration:

// What we had (wrong)
const supabase = createBrowserClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      flowType: 'implicit',  // ← the problem
    }
  }
)

Supabase's GoTrue server, by default, uses PKCE. After the user authenticates with Discord, GoTrue redirected back to the app with:

https://academy.jeremyknox.ai/auth/callback?code=AUTHORIZATION_CODE

The client, expecting an Implicit flow response, looked for #access_token= in the URL hash. It found nothing. It then found ?code= in the query string and recognized this as a PKCE response — for which it was not configured.

It threw AuthImplicitGrantRedirectError.

But here is the devastating part: this error was not surfaced visibly. The callback page showed the homepage. The user saw no error message. They were simply not logged in, with no explanation.

The fix was one line:

// What we needed (correct)
const supabase = createBrowserClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      flowType: 'pkce',  // ← match GoTrue's default
    }
  }
)

Eight hours of debugging. One configuration line. The lesson: when using Supabase, the client flowType must match what GoTrue is configured to return. GoTrue defaults to PKCE. Your client should too.

The Implicit Flow Deprecation

The OAuth Security Best Current Practice (BCP 212) formally deprecated the Implicit flow in 2019. Modern authorization servers either reject it outright or emit warnings. Browsers have added features (like removing tokens from history) that reduce but do not eliminate the risk.

There is no legitimate use case where Implicit is better than Authorization Code + PKCE today. If you are maintaining a codebase that still uses Implicit, migrate it. The flow type configuration is usually a one-line change.

Lesson 3 Drill

Find the Supabase (or any OAuth SDK) initialization in a project you own. Check the flowType setting:

  1. Is it set to pkce? If not, why not?
  2. Where is the code_verifier stored during the flow? sessionStorage? An in-memory variable?
  3. What origin does the callback land on? Is it the same origin where the flow started?

If the answers reveal a potential verifier-survival problem, you have just found a latent auth bug before it became an incident.