Quick Answer
AI-generated OAuth implementations often make three mistakes: missing the state parameter (enabling CSRF attacks on login), not implementing PKCE (allowing authorization code interception), and using wildcard or overly broad redirect URIs (enabling token theft via open redirects). These are not edge cases - they are what a happy-path OAuth tutorial leaves out.
Why OAuth Is Hard for AI to Get Right
OAuth 2.0 is a complex protocol with multiple flows, grant types, and security parameters that interact in non-obvious ways. AI coding tools learn from millions of code examples, but most OAuth tutorials online demonstrate the happy path without security parameters. The result: AI generates "working" OAuth flows that are vulnerable to well-known attacks.
OAuth is easy to implement in a way that works but is not secure. Two of the most common gaps are a missing state parameter and loose redirect URI validation, which is exactly what AI tools tend to generate.
The OAuth 2.0 Security Best Current Practice (RFC 9700) requires PKCE for public clients and recommends it for confidential ones, yet AI-generated code often leaves it out. Authentication failures are A07 in the OWASP Top 10 (2025).
Mistake 1: Missing State Parameter
The state parameter prevents CSRF attacks on the OAuth callback. Without it, an attacker can trick a logged-in user into linking the attacker's account.
// ❌ BAD - No state parameter (CSRF vulnerable)
const authUrl = `https://github.com/login/oauth/authorize?` +
`client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&scope=read:user`;
// Attacker can craft this URL with their own authorization code
// ✅ GOOD - Generate and verify state parameter
import crypto from 'crypto';
// On login initiation
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = state;
const authUrl = `https://github.com/login/oauth/authorize?` +
`client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}` +
`&scope=read:user&state=${state}`;
// On callback (reject a missing stored state too, or undefined === undefined passes)
app.get('/auth/callback', (req, res) => {
if (!req.session.oauthState || req.query.state !== req.session.oauthState) {
return res.status(403).json({ error: 'Invalid state parameter' });
}
delete req.session.oauthState;
// ... exchange code for token
});
Mistake 2: No PKCE Implementation
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks. Without PKCE, a malicious app or browser extension can intercept the authorization code and exchange it for an access token.
// ✅ GOOD - PKCE implementation
import crypto from 'crypto';
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// On login initiation
const { verifier, challenge } = generatePKCE();
req.session.codeVerifier = verifier;
const authUrl = `https://provider.com/authorize?response_type=code` +
`&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}` +
`&code_challenge=${challenge}&code_challenge_method=S256`;
// On callback - include verifier in token exchange
const tokenResponse = await fetch('https://provider.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: req.query.code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: req.session.codeVerifier,
}),
});
Mistake 3: Open Redirect URIs
AI-generated OAuth configurations often use wildcard or overly broad redirect URI patterns, allowing attackers to redirect tokens to their own servers.
// ❌ BAD - Accepting any redirect URI
const allowedRedirects = [/https:\/\/.*\.myapp\.com/];
// Attacker creates: https://evil.myapp.com.attacker.com
// ❌ BAD - Matching prefix only
if (redirectUri.startsWith('https://myapp.com')) { /* allowed */ }
// Attacker uses: https://myapp.com.attacker.com/steal-token
// ✅ GOOD - Exact match on full redirect URI
const ALLOWED_REDIRECTS = new Set([
'https://myapp.com/auth/callback',
'https://staging.myapp.com/auth/callback',
]);
if (!ALLOWED_REDIRECTS.has(redirectUri)) {
return res.status(400).json({ error: 'Invalid redirect URI' });
}
| OAuth Mistake | Attack Enabled | Impact |
|---|---|---|
| Missing state parameter | Login CSRF | Account linking to attacker's identity |
| No PKCE | Authorization code interception | Full account takeover |
| Open redirect URIs | Token theft via redirect | Access token stolen by attacker |
| Token in URL fragment | Referrer header leakage | Token leaked to third-party sites |
| No token expiry check | Stale token reuse | Indefinite unauthorized access |
How to Fix Your OAuth Implementation
- Add the state parameter to every OAuth flow. Generate it with
crypto.randomBytes(32)and verify it on callback. - Implement PKCE for all public clients (SPAs, mobile apps, desktop apps).
- Use exact redirect URI matching. Never use wildcards, prefix matching, or regex for redirect validation.
- Use a proven library instead of hand-coding OAuth. NextAuth.js (Auth.js) runs a PKCE check by default. Passport.js's OAuth 2.0 strategy handles state and PKCE once you turn on its
stateandpkceoptions. - Scan your codebase. Tools like VibeDoctor's Vibe Check automatically flag API routes with no auth middleware, state-changing GET routes (a CSRF risk), insecure cookie settings and hardcoded JWTs. They do not check your OAuth state or PKCE handling, so use the steps above for that. Free to sign up.
If you are still choosing an auth provider, see how Clerk, NextAuth and Supabase Auth compare when AI writes the integration.
FAQ
Should I implement OAuth myself or use a library?
Use a library. OAuth has too many security-critical details for hand implementation. NextAuth.js (for Next.js) runs PKCE or state checks for you; Passport.js (for Express) does once you set state: true and pkce: true on its OAuth 2.0 strategy. Arctic, a lightweight option, gives you helpers to generate the state and code verifier but leaves storing and comparing them to you, and its author deprecated it in July 2026. The time you save avoiding security bugs far outweighs the learning curve.
Is PKCE required for server-side apps?
RFC 9700 (OAuth 2.0 Security Best Practices) recommends PKCE for all OAuth clients, including server-side applications. While the authorization code interception risk is lower for confidential clients, PKCE adds defense-in-depth and is simple to implement.
What is the difference between OAuth 2.0 and OpenID Connect?
OAuth 2.0 is an authorization protocol (granting access to resources). OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0 (verifying identity). If you need "Login with Google", you want OIDC. GitHub is the exception: it offers plain OAuth 2.0 with no ID token, so "Login with GitHub" means OAuth 2.0 plus a call to its user API. If you need "access the user's GitHub repos", you want OAuth 2.0.
Does NextAuth handle PKCE automatically?
Yes. NextAuth.js v5 (Auth.js) runs a PKCE check on the OAuth callback by default. Some providers, such as LinkedIn, use a state check instead, and you can set checks: ["pkce", "state"] on a provider to run both. It also protects its own sign-in requests with a CSRF token. This is one of the strongest arguments for using a library instead of implementing OAuth from scratch.