OAuth 2.0 PKCE Debugger
Browser-to-TargetVisually debug OAuth 2.0 and PKCE authorization flows.
Use the Challenge to start the flow, and the Verifier when exchanging the code for an access token.
Generated Auth URL
Enter test data. Results update in real-time.
Learn More
JWT Tokens Explained: Decode, Verify & Common Mistakes (2026 Guide)
How TOTP 2FA Actually Works (and How to Test It Without Your Phone)
We Fed 200 Malformed JWTs to 5 Libraries: What Actually Broke
A practical test of JWT validation behavior across major libraries, focusing on algorithm confusion, exp requirements, malformed encoding, and real-world verification failures.
What is OAuth 2.0 PKCE Debugger?
Frequently Asked Questions
Technical Deep Dive
OAuth 2.0 PKCE Debugger
Generate secure code verifiers and challenges for PKCE. Build authorization URLs and trace the handshake steps to debug your authentication implementation.
Cryptographically Sound
Uses well-established algorithms and primitives, no homegrown crypto, no guesswork.
Stays in Your Browser
Secrets, hashes, and keys never touch a server. Audit the Network tab, it's empty.
Tunable & Inspectable
Adjust parameters, inspect outputs, and verify against known vectors during integration.
OAuth 2.0 + PKCE: The Modern Authorization Standard
OAuth 2.0 is twelve years old; PKCE is ten. Together they're the standard way every modern application, web app, mobile app, single-page app, CLI, delegates authentication to an identity provider. The flow has a lot of moving parts, and most OAuth bugs are subtle: a wrong redirect URI, a missing state parameter, a mismatched code_verifier. This debugger lets you walk the handshake step-by-step.
The Authorization Code Flow with PKCE
Client generates code_verifier. A cryptographically random string of 43-128 characters from the unreserved set. 32 random bytes base64url-encoded works perfectly.
Client derives code_challenge.
SHA-256(code_verifier)then base64url-encode (no padding). This is the value that goes to the auth server.Client redirects user to authorization endpoint with:
response_type=code(this is the auth code flow)client_id=<your client ID>redirect_uri=<registered URI>(must exactly match a pre-registered URI in your provider config)scope=<requested permissions>state=<random>(CSRF protection, required, not optional)code_challenge=<derived above>code_challenge_method=S256
User authenticates with the provider (logs in, approves the consent screen). The provider redirects back to the redirect_uri with
?code=<auth_code>&state=<echoed>.Client verifies state. Must match what was sent. If not, abort, it's a CSRF attempt.
Client exchanges code for tokens. POST to the token endpoint:
grant_type=authorization_codecode=<from step 4>redirect_uri=<same as step 3>client_id=<same>code_verifier=<original from step 1>- (
client_secretfor confidential clients only, public clients omit it)
Auth server validates. Hashes the provided
code_verifier, compares to thecode_challengeit stored at step 3. If they match, returnaccess_token,refresh_token,id_token(for OIDC).
Each step has failure modes. The debugger helps you isolate which step is broken.
Common Failure Modes
"invalid_redirect_uri" at step 3. The redirect_uri parameter must exactly match (character-for-character) one of the URIs registered with the provider. Trailing slashes count. http://localhost:3000/callback โ http://localhost:3000/callback/. Many providers also forbid wildcards; the URI must be exact.
"invalid_client" at step 6. Could be wrong client_id, wrong client_secret (for confidential clients), or, sneakier, the client is registered as "confidential" but you're trying public-client flow without secret. Provider config matters.
"invalid_grant" at step 6. Often means code_verifier mismatch. The verifier you're sending doesn't hash to the challenge the auth server stored. Common causes: re-generating the verifier between steps (use the SAME value from step 1), base64 padding bugs (the URL-safe variant must omit padding), encoding bugs (UTF-8 vs ASCII).
"invalid_request" at step 6. Missing required parameter. With PKCE: missing code_verifier. Without PKCE: missing client_secret. Read the actual error_description.
State mismatch at step 5. Either you didn't store state across the redirect (lost in session) or an attacker injected one. Either way, abort and start over.
Code already exchanged. Auth codes are single-use. If you accidentally exchange the same code twice, the second call fails. Common in React StrictMode (effects run twice in dev), guard the exchange call.
PKCE Across Provider Implementations
Most major providers support PKCE:
- Google. Supports S256, recommends PKCE for SPAs and mobile.
- GitHub. Supports PKCE since 2020. Required for public clients.
- Microsoft Entra ID (Azure AD). PKCE mandatory for SPAs.
- Auth0. Recommends PKCE; older flows still accepted but deprecated.
- Okta. Same.
- Apple Sign In. PKCE-aware.
- Twitch, Discord, Slack. Variable; check their OAuth docs.
Some providers add custom parameters (Microsoft's tenant, Google's access_type for refresh tokens). The debugger doesn't enforce these, read the provider's OAuth docs.
State, Nonce, and Replay Protection
state prevents CSRF on the redirect: an attacker can't forge a callback to your app because they don't know the state value you generated and stored. Always include it.
nonce (OIDC only) prevents replay of the id_token. The provider embeds your nonce in the id_token's claims; you verify it matches. Include in the auth request, validate in the response.
Both should be cryptographically random. Both should be tied to the user's session (so an attacker can't replay across sessions).
Where Tokens Should Live
After step 7, you have tokens. Where you put them is its own security topic.
- Access token. Short-lived (often 1 hour). Send with each API call as
Authorization: Bearer .... For SPAs: in-memory (not localStorage, XSS risk). For mobile: secure storage (Keychain on iOS, Keystore on Android). For server-side: in the session. - Refresh token. Long-lived (hours to weeks). Used to get new access tokens without re-auth. Must be stored securely; localStorage is risky for SPAs. Pattern: keep refresh on server, return new access tokens via a same-origin endpoint.
- id_token. OIDC user-identity claims. Validate signature, expiration, audience, issuer. Use for identity; don't send to APIs (that's what access_token is for).
The Backend-For-Frontend (BFF) pattern is increasingly popular for SPAs: a thin server-side layer holds the tokens; the SPA talks to BFF endpoints with same-origin session cookies; BFF talks to APIs with the OAuth tokens. Cleaner security boundary than browser-side token management.
Why "Implicit Flow" Is Dead
Original OAuth 2.0 had an "implicit flow" where the access token was returned directly in the URL fragment of the redirect, no code-for-token exchange step. Designed for SPAs that couldn't do server-side exchange. Dead now because:
- Tokens in URL fragments leaked into browser history, referer headers, server logs.
- No refresh tokens (you couldn't safely return them; same leak risk).
- PKCE made the auth code flow safe for public clients, removing implicit's reason to exist.
OAuth 2.1 (the consolidation spec) formally deprecates implicit. If you're reading documentation that recommends it, that documentation is stale.
Privacy
This debugger does its work in your browser via the Web Crypto API: random verifier generation, SHA-256 hashing, base64url encoding. The constructed URLs are shown but not auto-navigated. Open DevTools Network: zero requests to this tool's origin. Any provider call (when you click an authorization URL) goes directly to the provider you specified.
03 Real-World Use Cases
OAuth failure modes are easy to diagnose only after you've seen each one. These are the four that account for ~90% of integration tickets.
-
redirect_uri mismatch Provider returns
error=redirect_uri_mismatch. The URI in your authorize call must match (character-for-character) one of the URIs registered in the provider's app settings. Trailing slashes, http vs https, port number, all count. -
state parameter as CSRF defense RFC 6749 ยง10.12 makes state non-optional in practice. Without it, an attacker can trigger a callback to your app with their own auth code and silently link their account to your victim's session.
-
PKCE on mobile and SPA RFC 7636 PKCE is mandatory in the OAuth 2.1 draft for all public clients. Generate
code_verifier(43-128 chars), derivecode_challenge = BASE64URL(SHA256(verifier)), send withcode_challenge_method=S256. -
Refresh-token rotation Each refresh call returns a new refresh token; the old one is invalidated. If a stolen refresh is used, the legitimate client's next refresh fails, your server detects the breach and revokes the family. Standard practice since OAuth 2.1.
-
nonce for OIDC id_token replay defense OpenID Connect requires a
nonceclaim echoed back inside the id_token. Verifying it stops an attacker from replaying a captured id_token in a new session.
04 Worked Examples
code_verifier = base64url(random_bytes(32)) // e.g. "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = base64url(sha256(code_verifier))
// e.g. "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
code_challenge_method = S256
POST /token
grant_type=authorization_code
code=<received from /authorize callback>
redirect_uri=<same URI used at /authorize>
client_id=<public client id>
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkThe server recomputes SHA-256 of the supplied verifier and compares to the challenge it stored at step 1. Match โ tokens returned. Mismatch โ invalid_grant. The verifier is what proves the same client that started the flow is finishing it.
1. Attacker starts OAuth with their own account, gets ?code=ATTACKER
2. Tricks victim into clicking https://yourapp.com/callback?code=ATTACKER
3. Your app exchanges the code, links ATTACKER's identity into victim's session
4. Anything the victim uploads goes to attacker's account// before /authorize
session.oauth_state = base64url(random_bytes(16));
// on callback
if (req.query.state !== session.oauth_state) {
return abort("state_mismatch"); // RFC 6749 ยง10.12
}State is to the redirect what CSRF tokens are to forms. It must be unguessable, session-bound, and one-time, bind to SameSite=Lax cookie session so the attacker's forged callback can't carry it.
https://app.example.com/callback
?error=invalid_scope
&error_description=The%20requested%20scope%20'admin:write'%20is%20not%20granted
&state=abc123error: invalid_scope
description: The requested scope 'admin:write' is not granted
state: abc123 (validate this matches before trusting any of the above)Always URL-decode error_description before logging, providers percent-encode quotes and spaces. Standard error codes per RFC 6749: invalid_request, unauthorized_client, access_denied, unsupported_response_type, invalid_scope, server_error, temporarily_unavailable.
05 Related Tools
OAuth flows produce JWTs, get triggered by URLs, and end in API calls. These cover the surrounding terrain.
OAuth 2 URL Builder
Compose the /authorize URL with all parameters (client_id, scope, state, PKCE challenge), the form-builder companion to this debugger.
JWT Decoder
Inspect the id_token / access_token returned at the end of the flow, read claims, check exp, identify the signing kid.
curl Generator
Build the POST to /token outside the browser. Most providers don't enable CORS on the token endpoint, curl or Postman is how you actually finish the exchange.