Fix guide · medium · oauth_missing_state
OAuth authorize URL has no state parameter and no PKCE
The authorize URL in your bundle includes neither a state parameter nor a PKCE code_challenge. The OAuth callback is vulnerable to CSRF — an attacker can pre-prepare an auth code and trick the user into binding it to their account.
Why it matters
OAuth's CSRF protection comes from one of two mechanisms:
- state parameter — A random value the client generates before sending the user to the authorize endpoint. The provider returns it unchanged on callback. The client checks it matches what it sent. Without this, the callback can be triggered by any URL the attacker constructs.
- PKCE code_challenge — A hash the client commits to before the flow starts; the provider only releases the token when the matching verifier is presented at exchange time. PKCE provides equivalent CSRF protection.
If you have neither, an attacker can:
- Start their own OAuth flow against your app.
- Stop at the authorize step and capture the URL with their auth code attached.
- Send the user a link to that URL.
- The user's browser hits your callback, your app sees the auth code, exchanges it, and stores the resulting tokens against the user's session.
- The session is now tied to the *attacker's* identity at the OAuth provider — the attacker can log in as the user from their own browser.
This is "session fixation via OAuth" and is a real attack class. It's well-known enough that every major OAuth library implements state-or-PKCE by default — if your bundle is missing both, you're almost certainly using a hand-rolled flow that skipped this step.
How to fix it
Easy fix: use PKCE (which is the right move regardless — see /fix/oauth_implicit_flow).
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)));
sessionStorage.setItem('pkce_verifier', verifier);
const params = new URLSearchParams({
client_id: '...',
redirect_uri: 'https://app.example.com/auth/callback',
response_type: 'code',
code_challenge: challenge,
code_challenge_method: 'S256',
scope: 'openid profile email',
});
window.location.href = `https://provider.example.com/authorize?${params}`;
Older alternative — state parameter: generate a random nonce, store it (sessionStorage or signed cookie), include it as state=<nonce>, and on callback verify the returned state matches.
Don't roll your own. Use oauth4webapi, @auth/core, or your provider's official SDK. They get this right.
Did vibecheck flag this on your app?
If you reached this page from a vibecheck inspection report, the redacted match in your scan output is the exact string we found in your bundle. After applying the fix above, run the inspection again — the finding should clear.
Run another inspection