Skip to content

Why Does AI Write TODO: Add Authentication Instead of Real Auth?

I’ve done it. You’ve probably done it too. We ask AI to generate some routes, it gives us beautiful, working CRUD endpoints with a neat comment:

routes/users.js
// TODO: add authentication
router.delete('/users/:id', async (req, res) => {
const { id } = req.params;
await User.findByIdAndDelete(id);
res.json({ message: 'User deleted' });
});

The route works. The feature looks done. So it ships.

And that // TODO: add authentication comment? It never gets addressed.

The TODO Authentication Problem

This is the dark side of “vibe coding” – the trend where developers let AI write code based on natural language prompts without fully reviewing every line. AI generates functional, impressive code that handles the happy path beautifully. But when it comes to security-critical features like authentication, it often leaves behind placeholder comments instead of actual implementations.

A developer on Reddit captured it perfectly: “AI generates full CRUD routes, admin panels, delete endpoints – all without auth middleware. And it leaves behind helpful comments like // TODO: add authentication that never get addressed.”

The real problem isn’t just the incomplete code. It’s the false sense of completeness. The app works, tests pass, features look finished. That TODO comment becomes invisible noise in a sea of other priorities.

Why AI Skips Authentication

Understanding why AI does this helps us prevent it. There are three main reasons:

1. Happy Path Bias

AI is trained to generate working code. Authentication isn’t about making things work – it’s about preventing things from working when they shouldn’t. That’s a fundamentally different mindset.

When I prompt: “Create a route to delete users,” AI optimizes for deletion functionality. It doesn’t inherently know that deletion should be restricted to admins or the user themselves. Without that context, it produces the most direct solution.

2. Missing Context

Authentication requires domain-specific knowledge:

  • What’s your user model structure?
  • Are you using sessions or JWTs?
  • Where’s your secret key stored?
  • What are your authorization rules?
  • Which routes need protection?

AI can’t guess these. Without explicit context, it falls back to TODO comments as a polite way of saying “I need more information here.”

3. Training Data Patterns

Open source code – AI’s training ground – often has authentication implemented separately from route handlers. Middleware in one file, routes in another. AI learns this pattern and reproduces it, leaving you to connect the pieces.

The Security Risk

Let’s be honest about what happens when we ship TODO authentication:

Scenario 1: The delete route ships without authentication. Anyone can delete any user. This isn’t hypothetical – I’ve seen staging environments with this exact vulnerability.

Scenario 2: You remember the TODO but implement authentication inconsistently across routes. Some routes check for admin rights, others just check “is logged in,” and a few still have the TODO comment.

Scenario 3: The TODO gets copied across multiple files during development. Six months later, you have 47 TODO comments scattered throughout your codebase, and no one knows which ones are critical.

The Reddit post that sparked this discussion wasn’t theoretical. The developer admitted: “I’ve definitely shipped one of those, convinced myself I’d ‘come back to it’ and obviously never did.”

Before and After: Real Authentication Implementation

Here’s what AI typically generates:

routes/users.js (AI-generated)
const express = require('express');
const router = express.Router();
const User = require('../models/User');
// TODO: add authentication
router.get('/users', async (req, res) => {
const users = await User.find({});
res.json(users);
});
// TODO: add authentication
router.delete('/users/:id', async (req, res) => {
const { id } = req.params;
await User.findByIdAndDelete(id);
res.json({ message: 'User deleted' });
});
module.exports = router;

And here’s what you should write instead:

routes/users.js (secure)
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const auth = require('../middleware/auth');
const requireAdmin = require('../middleware/requireAdmin');
// Get all users - admin only
router.get('/users', auth, requireAdmin, async (req, res) => {
try {
const users = await User.find({}).select('-password -refreshTokens');
res.json(users);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch users' });
}
});
// Delete user - admin or self
router.delete('/users/:id', auth, async (req, res) => {
try {
const { id } = req.params;
// Authorization: admin can delete anyone, user can delete themselves
if (!req.user.isAdmin && req.user.id !== id) {
return res.status(403).json({ error: 'Not authorized' });
}
const deleted = await User.findByIdAndDelete(id);
if (!deleted) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ message: 'User deleted' });
} catch (error) {
res.status(500).json({ error: 'Failed to delete user' });
}
});
module.exports = router;

The authentication middleware itself:

middleware/auth.js
const jwt = require('jsonwebtoken');
function auth(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
}
module.exports = auth;

And the admin check:

middleware/requireAdmin.js
function requireAdmin(req, res, next) {
if (!req.user?.isAdmin) {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
module.exports = requireAdmin;

Notice the differences:

  • No TODO comments – authentication is implemented immediately
  • Authorization logic – not just “is authenticated” but “is allowed”
  • Error handling – proper HTTP status codes and messages
  • Security hygiene – password and tokens excluded from responses

Preventing TODO Authentication Through Prompt Engineering

You can get better results from AI by providing authentication context upfront:

Vague prompt (produces TODO):

Create a route to delete users

Better prompt:

Create an Express.js route to delete users. Requirements:
- Use JWT authentication (check Authorization: Bearer <token> header)
- Only admins (req.user.isAdmin === true) or the user themselves can delete
- Return 401 if not authenticated, 403 if not authorized
- Handle case where user doesn't exist (404)
- Use User.findByIdAndDelete
- Environment variable JWT_SECRET for token verification

Even better – provide your middleware:

Create a route to delete users. Use this auth middleware:
import { auth, requireAdmin } from '../middleware';
Routes should:
- Require authentication
- Allow admins to delete any user
- Allow users to delete their own account
- Return proper error codes

The more context you provide, the less AI relies on TODO placeholders.

Express.js Authentication Best Practices

When implementing authentication (whether AI-assisted or not), follow these patterns:

Use Middleware Consistently

middleware/auth.js (complete example)
const jwt = require('jsonwebtoken');
const { promisify } = require('util');
const verify = promisify(jwt.verify);
async function auth(req, res, next) {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Invalid authorization header' });
}
const token = authHeader.split(' ')[1];
const payload = await verify(token, process.env.JWT_SECRET);
req.user = {
id: payload.sub,
email: payload.email,
isAdmin: payload.isAdmin || false
};
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
return res.status(500).json({ error: 'Authentication failed' });
}
}
module.exports = auth;

Separate Authentication from Authorization

middleware/authorization.js
function requireOwnership(paramName = 'id') {
return (req, res, next) => {
const resourceId = req.params[paramName];
if (req.user.isAdmin || req.user.id === resourceId) {
return next();
}
return res.status(403).json({ error: 'Not authorized' });
};
}
module.exports = { requireOwnership };

Usage:

routes/users.js
const { auth } = require('../middleware/auth');
const { requireOwnership } = require('../middleware/authorization');
router.delete('/users/:id', auth, requireOwnership('id'), deleteUser);
router.put('/users/:id', auth, requireOwnership('id'), updateUser);

Never Store Secrets in Code

.env
JWT_SECRET=your-super-secret-key-here
JWT_EXPIRES_IN=7d
config/auth.js
module.exports = {
jwtSecret: process.env.JWT_SECRET || (() => {
throw new Error('JWT_SECRET environment variable is required');
})(),
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d'
};

Rate Limit Authentication Endpoints

middleware/rateLimiter.js
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: { error: 'Too many attempts, please try again later' },
standardHeaders: true,
legacyHeaders: false
});
module.exports = { authLimiter };

Apply to login/register routes:

routes/auth.js
const { authLimiter } = require('../middleware/rateLimiter');
router.post('/login', authLimiter, loginController);
router.post('/register', authLimiter, registerController);

Summary

AI-generated TODO comments for authentication aren’t a bug – they’re a signal. A signal that the AI lacks context, that you’re prioritizing the happy path, or that you’re treating AI as a replacement for security thinking rather than a productivity tool.

The fix isn’t to stop using AI. The fix is to:

  1. Provide authentication context in prompts – middleware files, user models, authorization rules
  2. Never ship TODO comments – implement authentication before marking features complete
  3. Use consistent middleware patterns – make auth a non-negotiable part of route creation
  4. Review AI output for security placeholders – grep for “TODO” and “FIXME” before merging

I’ve shipped TODO authentication. You probably have too. Now we know better – and we have the patterns to do better. The next time AI hands you a working route with a TODO comment, don’t just copy-paste. Stop, implement the auth, then ship.

Because “I’ll come back to it” is a lie we’ve all told ourselves. And it’s a lie that shows up in production.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments