6 min read

OAuth + OIDC security — the PKCE and state checks you cannot skip

Most OAuth bugs come from skipping PKCE, ignoring state, or accepting tokens issued for a different client. Here is the correct implementation in a Next.js + NextAuth app.

OAuth and OIDC are secure protocols with many unsafe implementations. The safe version requires PKCE for public clients, state for CSRF, nonce for OIDC, and token-audience verification.

What it is

OAuth 2.0 grants an app delegated access to a resource on behalf of a user. OIDC builds identity on top. Secure use requires the small set of checks below.

Vulnerable example

// Vulnerable: no state, no PKCE, no audience check on ID token
const auth_url = `https://provider.com/authorize?client_id=${id}&redirect_uri=${redir}&response_type=code`;
// redirect user; receive code; exchange for tokens; decode JWT without verifying audience.

Fixed example

// Fixed: PKCE + state + nonce + audience check
import { randomBytes, createHash } from "crypto";

const state = randomBytes(16).toString("base64url");
const nonce = randomBytes(16).toString("base64url");
const code_verifier = randomBytes(32).toString("base64url");
const code_challenge = createHash("sha256").update(code_verifier).digest("base64url");

// store state, nonce, code_verifier in session
// include state, nonce, code_challenge_method=S256, code_challenge in authorize URL
// on callback: verify state matches, exchange code with code_verifier,
// verify id_token signature, audience, issuer, nonce

How Securie catches it

Securie findingmedium
apps/web/app/api/route.ts:22

OAuth + OIDC security

Securie audits every OAuth implementation in the repository — NextAuth, Clerk, custom, Lucia — requiring PKCE, state, and explicit audience verification.

Suggested fix — ready as a PR
// Fixed: PKCE + state + nonce + audience check
import { randomBytes, createHash } from "crypto";

const state = randomBytes(16).toString("base64url");
const nonce = randomBytes(16).toString("base64url");
const code_verifier = randomBytes(32).toString("base64url");
const code_challenge = createHash("sha256").update(code_verifier).digest("base64url");

// store state, nonce, code_verifier in session
// include state, nonce, code_challenge_method=S256, code_challenge in authorize URL
// on callback: verify state matches, exchange code with code_verifier,
// verify id_token signature, audience, issuer, nonce
Catch this in my repo →Securie reviews every PR · proves the issue · ships a verified fix PR

Checklist

  • PKCE enforced for all public clients (required for SPAs and mobile)
  • State parameter included and validated on callback
  • OIDC nonce included in id_token and verified
  • ID token audience + issuer verified
  • Access token never passed through a query parameter (use Authorization header)
  • Refresh tokens rotated with reuse detection

FAQ

If I use NextAuth, do I need to do any of this?

NextAuth does most of it. Review that your provider config sets the right scopes and that any custom providers inherit the safe defaults.

Related guides