TL;DR: Authentication is how an app verifies who you are. The two main approaches are sessions (server stores your login state) and JWT tokens (a signed proof of identity you carry with you). AI can scaffold both, but auth code is high-stakes — always use a battle-tested library and keep secrets out of your code.
Why AI Coders Need to Know This
Authentication is one of the most consequential pieces of code in any app. Get it wrong and you expose your users' data, allow account takeovers, or accidentally give strangers admin access. Get it right and it becomes nearly invisible — users log in, stay logged in, and log out without thinking about it.
When you ask an AI coding tool to "add login," you are handing over a high-stakes implementation to a system that generates statistically plausible code — not necessarily secure code. AI-generated auth systems often have subtle but critical flaws: hardcoded secrets, missing token expiry, no rate limiting on login attempts, plain-text password storage, or tokens transmitted in URLs where they get logged.
You do not need to become a security expert. But you do need a working mental model of how authentication works so you can:
- Understand what the AI built and why
- Know which questions to ask before going live
- Recognize when something looks wrong
- Choose the right library or service instead of rolling your own
Authentication vs. Authorization — Two Different Things
Before anything else: these two words sound similar and get confused constantly, even by experienced developers. Here is the distinction:
- Authentication — "Who are you?" Proving your identity. Logging in with a password, a magic link, or a Google account.
- Authorization — "What can you do?" Once the app knows who you are, it decides what you're allowed to access. An admin sees the dashboard; a regular user does not.
Most auth bugs fall into one of two categories: broken authentication (anyone can log in as anyone) or broken authorization (logged-in users can access things they shouldn't). Both are critical. This article focuses on authentication — proving identity.
Real Scenario
You are building a SaaS app and type this into Cursor:
Prompt I Would Type
Add authentication to this Next.js app. Users should be able to:
- Register with email and password
- Log in and stay logged in across page refreshes
- Log out
- Have protected routes only logged-in users can see
Use industry-standard security practices. Store passwords safely.
Claude or Cursor might scaffold a full auth system using NextAuth.js, or it might write a custom implementation with Express sessions or JWT. Either way, you will see code involving bcrypt, secret keys, tokens, and middleware you have never seen before. Let's break down what each part does.
What AI Generated
Here is a realistic example of what AI generates for a simple JWT-based auth API route in Next.js. Pay attention to the comments — this is the mental model you need to build.
// app/api/auth/login/route.ts
import { NextResponse } from 'next/server';
import bcrypt from 'bcryptjs'; // library that safely hashes passwords
import jwt from 'jsonwebtoken'; // library that creates and verifies JWT tokens
import { db } from '@/lib/db'; // your database connection
export async function POST(request: Request) {
const { email, password } = await request.json(); // pull credentials from request body
// 1. Find the user in the database by email
const user = await db.user.findUnique({ where: { email } });
if (!user) {
// Don't reveal whether email exists — say credentials are wrong, not "email not found"
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
// 2. Compare the submitted password against the stored hash
// bcrypt.compare() is safe — it handles timing attacks automatically
const passwordMatch = await bcrypt.compare(password, user.passwordHash);
if (!passwordMatch) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
// 3. Create a JWT token — a signed proof that this user is authenticated
const token = jwt.sign(
{ userId: user.id, email: user.email }, // payload: data embedded in the token
process.env.JWT_SECRET!, // secret key — MUST live in .env, never in code
{ expiresIn: '7d' } // token expires in 7 days — important!
);
// 4. Return the token to the client
// The client stores this (usually in an httpOnly cookie or memory) and sends it with future requests
const response = NextResponse.json({ success: true });
response.cookies.set('auth-token', token, {
httpOnly: true, // JavaScript can't access this cookie — prevents XSS theft
secure: true, // only sent over HTTPS
sameSite: 'lax', // prevents CSRF attacks
maxAge: 60 * 60 * 24 * 7 // 7 days in seconds
});
return response;
}
Understanding Each Part
Password Hashing with bcrypt
You must never store passwords in plain text. If your database is ever breached, plain-text passwords mean every user account on every site where they reused that password is now compromised. bcrypt solves this by converting the password into a fixed-length hash — a string that can't be reversed back to the original password.
When a user registers, you hash their password with bcrypt.hash(password, 12) and store the hash. When they log in, you use bcrypt.compare(submittedPassword, storedHash) — bcrypt computes the hash of what they typed and compares it to the stored hash without ever decrypting it.
The number (12 in the example) is the "cost factor" — higher means slower hashing, which makes brute-force attacks harder. 12 is a reasonable default in 2026.
JWT Tokens
A JSON Web Token (JWT) is a string that looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJpYXQiOjE3NDIwMDAwMDAsImV4cCI6MTc0MjYwNDgwMH0.SIGNATURE_HERE
It has three dot-separated parts: header (algorithm), payload (your data, base64-encoded — not encrypted, just encoded), and signature (cryptographic proof the server created this). Anyone can decode the payload; only the server can create a valid signature.
The key insight: JWTs are stateless. The server doesn't need to look up your session in a database — it just verifies the signature. That makes them fast and scalable, but it also means you can't "invalidate" a JWT before it expires. That's why short expiry times matter.
httpOnly Cookies vs. localStorage
Where you store a JWT matters a lot. Many tutorials store tokens in localStorage. This is convenient but dangerous: any JavaScript on the page — including injected malicious scripts — can read it. httpOnly cookies cannot be accessed by JavaScript at all, which eliminates a whole class of attacks (XSS token theft). Always prefer httpOnly cookies for auth tokens.
The JWT_SECRET Environment Variable
The secret key used to sign JWT tokens must be long, random, and kept out of your code. If someone gets your secret, they can forge tokens and log in as any user. This lives in a .env file locally and in your hosting platform's environment variable settings (Vercel, Railway, etc.) in production. See What Is an Environment Variable? if this is new to you.
What AI Gets Wrong About Authentication
⚠️ Review auth code carefully. Authentication is one of the highest-risk areas for AI-generated code. The mistakes below are common and can have serious security consequences.
1. Hardcoded Secrets
AI frequently generates code with secret keys written directly in the source: jwt.sign(payload, 'mysecretkey123'). This is a critical vulnerability. Anyone who sees your code — including anyone with access to your public GitHub repo — can forge auth tokens. Always use environment variables.
2. No Token Expiry
Tokens without expiresIn are valid forever. If a token is stolen (device lost, session hijacked), there's no way to force re-authentication without invalidating all tokens. Always set an expiry.
3. Storing Tokens in localStorage
Very common in AI-generated tutorials. Use httpOnly cookies instead. This single change significantly reduces your XSS attack surface.
4. Missing Rate Limiting on Login Endpoints
Without rate limiting, an attacker can try thousands of password combinations per second. Add rate limiting middleware (like express-rate-limit or Vercel's edge middleware) to login and registration endpoints.
5. Verbose Error Messages
AI sometimes generates errors like "Email not found" or "Password incorrect" as separate messages. This tells attackers which emails are registered. Always return a generic "Invalid credentials" for both cases.
Sessions vs. JWT: Which Should You Use?
There are two main authentication patterns:
- Session-based auth — The server stores your login state in a database or memory. The client gets a session ID cookie. On each request, the server looks up the session. Simple, easy to invalidate, but requires server-side storage and doesn't scale as easily.
- JWT-based auth — The server issues a signed token. The client stores it and sends it with each request. The server just verifies the signature — no database lookup. Scales well, but harder to invalidate before expiry.
For most apps starting out: use a library. NextAuth.js / Auth.js handles both patterns for Next.js and works with dozens of OAuth providers. Clerk is a hosted auth service that handles everything including UI. Supabase Auth is great if you're already using Supabase. These libraries encode years of security decisions you don't want to make from scratch.
OAuth: Login with Google / GitHub
OAuth is a protocol that lets users authenticate via an existing trusted account instead of creating a new one. When you click "Login with Google," here's what happens:
- Your app redirects the user to Google's login page
- The user authenticates with Google
- Google redirects back to your app with a temporary code
- Your server exchanges that code for an access token
- You use the token to get the user's profile (name, email, avatar)
- You create or look up the user in your database and start a session
OAuth is complicated to implement correctly from scratch. NextAuth.js and similar libraries abstract this to a few lines of config. When AI generates OAuth code manually, review it carefully — the redirect URI validation and state parameter handling are common places for bugs.
How to Debug Authentication with AI
In Cursor
When auth breaks, open the problematic file and use Cursor's chat: "This login route returns 401 even with correct credentials. Walk me through what could cause that." Then paste the error message. Cursor is good at spotting bcrypt comparison errors, missing await keywords on async calls, and misconfigured cookie settings.
In Windsurf
Use Cascade to trace the auth flow end-to-end: "Starting from the login form submit, trace every step of the authentication process in this codebase and identify where the token validation might be failing." Windsurf's codebase-wide context is valuable here.
In Claude Code
Paste your auth route and ask: "Review this JWT authentication implementation for security issues. Focus on: secret key handling, token storage, expiry, rate limiting, and error messages." Claude is strong at security review — this is one of the best uses of the tool for auth work.
General Debugging Tips
- Check browser DevTools → Application → Cookies. Is the auth cookie being set? Is it httpOnly?
- Use jwt.io to decode your token (never paste real user tokens from production)
- Add
console.logafter each auth step to find exactly where the flow breaks - Check that your
JWT_SECRETenvironment variable is set in your deployment environment, not just locally
What to Learn Next
Authentication connects to several other foundational concepts:
Frequently Asked Questions
Authentication verifies who you are (proving your identity with a password, token, or biometric). Authorization determines what you're allowed to do once you're logged in (access control, permissions). They work together but are separate concepts.
A JSON Web Token (JWT) is a compact, self-contained string that encodes user identity and claims. It has three parts separated by dots: a header, a payload (user data), and a signature. The server signs it; the client sends it back with each request to prove identity.
Use a library or service for production apps. Options include NextAuth.js, Auth.js, Clerk, Supabase Auth, and Firebase Auth. Building your own auth from scratch is error-prone and a common source of security vulnerabilities, even for experienced developers.
OAuth is a protocol that lets users log in with an existing account (Google, GitHub, etc.) instead of creating a new one. You need it when you want "Login with Google" or "Login with GitHub" buttons. It's also used when your app needs to access another service on a user's behalf.
AI often generates auth code that stores passwords in plain text, uses weak or no secret keys, exposes tokens in URLs or logs, skips token expiry, or doesn't validate inputs. Always review AI-generated auth code with a security lens or use a battle-tested auth library instead.