Step 3
OAuth + state · PKCE
25 min
OAuth + state · PKCE
Social login delegates identity to an IdP. Two small details (state · PKCE) prevent CSRF and code interception.
1. Flow
1. User clicks "Sign in with Kakao"
2. App → redirect to Kakao (client_id · redirect_uri · state)
3. Kakao → user approves
4. Kakao → redirect_uri?code=xxx&state=xxx
5. App (server) → exchange code for access_token
6. App → fetch user info
7. App → issue session cookie
2. state — CSRF defense
// initiate
const state = crypto.randomUUID();
res.cookies.set("oauth_state", state, { httpOnly: true, secure: true, sameSite: "lax", maxAge: 300 });
url.searchParams.set("state", state);
// callback
const queryState = req.nextUrl.searchParams.get("state");
const cookieState = req.cookies.get("oauth_state")?.value;
if (cookieState !== queryState) return redirect("/login?e=csrf");
3. PKCE
Required for SPAs / mobile (no secure client_secret), recommended everywhere.
const codeVerifier = base64url(randomBytes(32));
const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
url.searchParams.set("code_challenge", codeChallenge);
url.searchParams.set("code_challenge_method", "S256");
res.cookies.set("oauth_verifier", codeVerifier, { httpOnly: true, maxAge: 300 });
At token exchange include the verifier.
4. Per provider
| Provider | state | PKCE |
|---|---|---|
| Kakao | recommended | supported |
| Naver | required | supported |
| recommended | required |
5. Redirect URI whitelist
Register exact URLs in the provider console. No wildcards.
6. Token storage
- access_token — encrypt at rest (AES-256-GCM) if stored in DB
- email · id —
userstable withauth_provider - session cookie — HTTP-only JWT
7. Account linking / duplicates
- A — only the first provider
- B — auto-link same email (vector for takeover)
- C — ask user to log in via the existing provider
A is safest.
8. Logout
res.cookies.delete("session");
res.cookies.delete("refresh_token");
App session only; provider-level logout is a separate UX decision.
9. Gotchas
- No state check → login CSRF
- Lax redirect_uri → open redirect
- Plaintext access_token → neighbour-service takeover
- id_token signature skipped (OIDC)
- Tokens in localStorage → XSS takes them
10. Checklist
- state verified
- PKCE where supported
- exact redirect_uri
- encrypted access_token
- id_token signature verified
Closing
Most OAuth risk sits in state, redirect_uri, and token storage. Get these three right and the provider SDK handles the rest.
Next
- 04-input-validation