Fairy
Resources

What AI Coding Tools Miss in Authentication Flows

June 20, 2026 · 9-minute read · Fairy

The short answer

AI coding tools commonly generate authentication code with critical security flaws: JWTs signed without expiration claims, passwords stored with weak hashing or plaintext, OAuth callbacks that skip state validation, and route configurations that leave endpoints unprotected. These mistakes happen because AI optimizes for working code, not secure code, and lacks adversarial thinking about how authentication systems get attacked.

AI Authentication Code Contains Predictable Security Flaws

AI coding tools generate authentication code with critical security vulnerabilities. The most common failures include JWTs signed without expiration claims, passwords stored with inadequate hashing, OAuth callbacks that skip state validation, and middleware configurations that leave routes unprotected. These aren't edge cases—they're systematic patterns that appear across AI-generated codebases.

The root cause: AI optimizes for code that runs, not code that resists attack. Authentication systems require adversarial thinking—anticipating how an attacker might exploit each component. AI models lack this threat-modeling instinct because they learn from examples that demonstrate functionality, not security posture.

Let's examine the six authentication mistakes AI makes most often, with code examples showing what goes wrong and how to fix it.

JWT Tokens Signed Without Expiration

When AI generates JWT authentication, it frequently produces tokens that never expire. The jwt.sign() function doesn't require an expiration claim, so AI generates syntactically correct but dangerously insecure code.

What AI generates

const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET
);

Why this breaks security

A JWT without expiration is valid forever. If an attacker obtains this token through any means—XSS, network interception, compromised logs, or a data breach—they have permanent access to that user's account. The only remediation is rotating your JWT secret, which invalidates every user's session.

What the code should look like

const token = jwt.sign(
  { userId: user.id, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }
);

Short-lived access tokens (15 minutes) combined with longer-lived refresh tokens limit the blast radius of token theft. The verifier must also enforce expiration—AI sometimes adds expiresIn at signing but uses ignoreExpiration: true during verification.

Passwords Stored With Weak or No Hashing

AI models have seen countless examples of password handling, including the insecure ones. This training data contamination leads to code that uses fast cryptographic hashes or, worse, stores passwords in plaintext.

What AI generates

// Using a fast hash
const hashedPassword = crypto
  .createHash('sha256')
  .update(password)
  .digest('hex');

// Or sometimes just this
await db.insert({ email, password }); // plaintext

Why this breaks security

SHA-256 processes billions of hashes per second on modern GPUs. An attacker with a stolen database can crack most passwords through brute force within hours. Plaintext storage means passwords are immediately exposed—often reused passwords that unlock the user's accounts on other services.

What the code should look like

const bcrypt = require('bcrypt');

// Registration
const hashedPassword = await bcrypt.hash(password, 12);

// Login verification
const valid = await bcrypt.compare(submittedPassword, storedHash);

Key derivation functions like bcrypt, Argon2, or scrypt are intentionally slow. They include salting automatically and can be tuned to take hundreds of milliseconds per hash, making large-scale cracking economically unfeasible.

OAuth State Parameter Not Validated

OAuth 2.0 requires a state parameter to prevent cross-site request forgery during the authorization flow. AI often includes state generation but skips the critical validation step on the callback.

What AI generates

// Authorization request - state generated but...
app.get('/auth/google', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  req.session.oauthState = state;
  const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
    `client_id=${CLIENT_ID}&state=${state}&...`;
  res.redirect(authUrl);
});

// Callback - state ignored
app.get('/auth/callback', async (req, res) => {
  const { code } = req.query; // state not checked!
  const tokens = await exchangeCode(code);
  // ...
});

Why this breaks security

Without state validation, an attacker can initiate an OAuth flow, capture the callback URL with their authorization code, and trick a victim into visiting it. The victim's browser completes the flow, linking the attacker's external account to the victim's session. This enables full account takeover.

What the code should look like

app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Constant-time comparison prevents timing attacks
  if (!state || !crypto.timingSafeEqual(
    Buffer.from(state),
    Buffer.from(req.session.oauthState || '')
  )) {
    return res.status(403).send('Invalid state parameter');
  }
  
  delete req.session.oauthState; // Prevent replay
  const tokens = await exchangeCode(code);
  // ...
});

Protected Routes Missing Auth Middleware

AI generates authentication middleware correctly in isolation, but frequently fails to apply it consistently across all routes that require protection. This creates gaps where sensitive endpoints are publicly accessible.

What AI generates

// Auth middleware defined
const requireAuth = (req, res, next) => {
  if (!req.session.userId) return res.status(401).send('Unauthorized');
  next();
};

// Applied to some routes
app.get('/api/profile', requireAuth, getProfile);
app.put('/api/profile', requireAuth, updateProfile);

// But missed on others (added later, different file, etc.)
app.delete('/api/account', deleteAccount); // No auth!
app.get('/api/admin/users', listAllUsers); // No auth!

Why this breaks security

A single unprotected endpoint can expose your entire system. The /api/admin/users route above would leak your complete user database to anyone who discovers it. Route-level auth omissions are easy to miss during development but trivial for attackers to find through automated scanning.

What the code should look like

// Apply auth globally, then whitelist public routes
app.use('/api', requireAuth);

// Explicitly mark public routes
const publicRoutes = ['/api/health', '/api/auth/login', '/api/auth/register'];
app.use((req, res, next) => {
  if (publicRoutes.includes(req.path)) return next();
  requireAuth(req, res, next);
});

The secure pattern inverts the default: everything requires authentication unless explicitly marked public. This approach catches new routes automatically rather than relying on developers to remember middleware on every endpoint.

Admin Access Controlled by Client-Supplied Values

This pattern appears repeatedly in AI-generated code: administrative access determined by query parameters, request body fields, or other client-controllable values rather than verified server-side state.

What AI generates

app.get('/api/admin/dashboard', (req, res) => {
  if (req.query.admin === 'true') {
    return res.json(getAdminData());
  }
  res.status(403).send('Admin access required');
});

Why this breaks security

Any user can append ?admin=true to access administrative functionality. This isn't a subtle vulnerability—it's a complete authentication bypass that requires zero sophistication to exploit. AI generates this pattern because it produces working code that satisfies the stated requirement ("only admins should access this") without understanding what authentication actually means.

What the code should look like

app.get('/api/admin/dashboard', requireAuth, async (req, res) => {
  const user = await db.users.findById(req.session.userId);
  
  if (user.role !== 'admin') {
    return res.status(403).send('Admin access required');
  }
  
  res.json(getAdminData());
});

Role verification must come from your database after validating the user's session. Never trust any value that originates from the client request.

Database Access Without Row-Level Security

When AI generates code using platforms like Supabase, it often creates tables and queries without enabling row-level security (RLS). This leaves every row readable and writable by any authenticated user—or anyone at all if auth isn't enforced at the API layer.

What AI generates

CREATE TABLE user_documents (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID REFERENCES auth.users(id),
  content TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);
-- No RLS enabled, no policies defined
// Client-side query - fetches ALL documents
const { data } = await supabase
  .from('user_documents')
  .select('*');

Why this breaks security

Without RLS, the Supabase client can query any row in the table. An attacker using your application's API key (exposed in client-side code) can read every user's documents, modify them, or delete them. RLS is the database-level enforcement that prevents this regardless of what the application code does.

What the code should look like

CREATE TABLE user_documents (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID REFERENCES auth.users(id),
  content TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

ALTER TABLE user_documents ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can only access their own documents"
  ON user_documents
  FOR ALL
  USING (auth.uid() = user_id);

Why AI Makes These Mistakes Systematically

These authentication failures share a common cause: AI models optimize for functional correctness, not security correctness. A JWT that signs successfully, an OAuth flow that completes, a password that stores and compares—these all "work" from the AI's perspective.

Security requires reasoning about adversaries, and AI has no mental model of attackers. It doesn't ask: "What happens if this token leaks?" or "What if someone forges this request?" These questions require threat modeling that current AI systems don't perform.

Additionally, AI training data includes vast amounts of tutorial code, Stack Overflow snippets, and example projects where security is deliberately simplified for pedagogical clarity. The AI learns that jwt.sign(payload, secret) is the pattern, not jwt.sign(payload, secret, { expiresIn: '15m' }).

How to Catch AI Authentication Bugs

Given these systematic failure modes, every AI-generated authentication flow needs security review focused on specific questions:

For JWTs:

For passwords:

For OAuth:

For route protection:

For database access:

Tools like Fairy Scout can flag many of these patterns automatically during pull request review. For production authentication systems, having a human Fairy review your code provides the adversarial thinking that AI lacks—staff engineers who ask "how could this be exploited?" rather than "does this compile?"

Authentication Is Where AI Assistance Needs Human Verification

Authentication code protects everything else in your system. A SQL injection might expose one table; a broken auth flow exposes your entire user base, potentially forever if tokens don't expire.

AI accelerates authentication implementation dramatically—what once took days of OWASP documentation reading now takes minutes. But that speed is dangerous when the generated code contains the systematic vulnerabilities covered here.

The pattern that works: use AI to scaffold your authentication flows quickly, then apply rigorous security review before deployment. Treat AI-generated auth code as a first draft that needs expert editing, not production-ready output.

When you're shipping authentication code that AI helped write, Fairy Intelligence can answer specific questions about your security implementation, grounded in patterns from thousands of reviewed codebases. For code that protects user accounts and sensitive data, human verification isn't overhead—it's the difference between a working system and a secure one.

Frequently asked questions

Why does AI generate JWTs without expiration?

AI models learn from code examples where expiration is often omitted for brevity. The jwt.sign() call works without expiresIn, so AI produces functional but insecure tokens. A leaked token without expiry remains valid indefinitely.

How do I check if AI-generated OAuth code is secure?

Verify that your OAuth implementation generates a cryptographically random state parameter, stores it server-side or in an httpOnly cookie, and validates that the callback state matches before exchanging the code. AI frequently skips the validation step.

What password hashing mistakes does AI make?

AI often uses fast hashes like MD5 or SHA-256 instead of slow key derivation functions like bcrypt or Argon2. Sometimes it stores passwords in plaintext. Both mistakes make credentials trivially crackable if the database is breached.

Can AI-generated auth middleware miss protecting routes?

Yes. AI frequently applies auth middleware to some routes but misses others, especially when using pattern matchers or when routes are defined across multiple files. Manual review of every protected endpoint is essential.

Should I trust AI to write authentication code?

AI can scaffold authentication flows quickly, but the code requires careful security review. Authentication is high-stakes—a single flaw can expose your entire user base. Human verification of AI-generated auth code is strongly recommended.


Have AI-generated work you’d want verified? Connect with a Fairy → or run a free check with Scout.

More resources