API Authentication: JWT vs Session vs OAuth 2.0 Security Comparison
On this page
API Authentication: JWT vs Session vs OAuth 2.0
When teams compare JWT vs session vs OAuth 2.0, they are really deciding how much state to keep on the server, how to revoke access quickly, and whether the system is for first-party login or third-party delegation. Choosing the wrong API authentication model creates avoidable risk in scaling, logout, browser security, and cross-domain access.
If you want the short version: sessions are usually safest for traditional web apps, JWTs are useful for stateless and mobile scenarios when handled carefully, and OAuth 2.0 is the right choice when third-party applications need delegated access.
The Authentication Decision Tree
Here's the quick answer:
Your App → Is it a traditional web app?
├── Yes → Server-side sessions (with cookies)
└── No → Is it a mobile app or SPA calling your own API?
├── Yes → Short-lived JWTs + Refresh tokens (HttpOnly cookies)
└── No → Is it third-party API access?
├── Yes → OAuth 2.0 (Authorization Code + PKCE)
└── No → API Keys (for server-to-server)
Option 1: Server-Side Sessions
The oldest and most battle-tested approach. The server stores session data; the client holds only a session ID in a cookie.
How It Works
1. User submits login form
2. Server verifies credentials
3. Server creates session in store (Redis/DB)
4. Server sends session ID as HttpOnly cookie
5. Browser sends cookie with every request
6. Server looks up session to identify user
Implementation
import session from 'express-session';
import RedisStore from 'connect-redis';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
app.use(session({
store: new RedisStore({ client: redis }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
// Login
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: 'Logged in' });
});
// Protected route
app.get('/profile', (req, res) => {
if (!req.session.userId) return res.status(401).json({ error: 'Not authenticated' });
// ... fetch and return user data
});
// Logout — instantly invalidates the session
app.post('/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
Pros & Cons
| ✅ Pros | ❌ Cons |
|---|---|
| Instant revocation (delete session) | Requires session store (Redis) |
| Server controls all session data | Doesn't scale to multiple domains easily |
| Smaller cookie size (~32 bytes) | Stateful — every request hits the store |
| Battle-tested for 25+ years | CSRF protection required |
Option 2: JWT (JSON Web Tokens)
Stateless tokens that contain user claims. The server doesn't need to store anything — the token itself is the proof.
How It Works
1. User submits login credentials
2. Server verifies and creates signed JWT
3. Client stores JWT (cookie or memory)
4. Client sends JWT with every request
5. Server verifies signature — no database lookup needed
Implementation
import jwt from 'jsonwebtoken';
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET!;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET!;
// Login — issue access + refresh tokens
app.post('/login', async (req, res) => {
const user = await verifyCredentials(req.body);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const accessToken = jwt.sign(
{ userId: user.id, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m' } // Short-lived!
);
const refreshToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// ✅ Store tokens in HttpOnly cookies (not localStorage!)
res.cookie('access_token', accessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900000,
});
res.cookie('refresh_token', refreshToken, {
httpOnly: true, secure: true, sameSite: 'lax',
maxAge: 7 * 86400000, path: '/api/auth/refresh',
});
res.json({ message: 'Logged in' });
});
// Middleware: Verify access token
function authenticate(req: Request, res: Response, next: NextFunction) {
const token = req.cookies.access_token;
if (!token) return res.status(401).json({ error: 'No token' });
try {
const payload = jwt.verify(token, ACCESS_SECRET) as JWTPayload;
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });
try {
const payload = jwt.verify(refreshToken, REFRESH_SECRET) as RefreshPayload;
// ✅ Check token version (for revocation)
const user = await db.user.findById(payload.userId);
if (!user || user.tokenVersion !== payload.tokenVersion) {
return res.status(401).json({ error: 'Token revoked' });
}
// Issue new access token
const newAccessToken = jwt.sign(
{ userId: user.id, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.cookie('access_token', newAccessToken, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 900000,
});
res.json({ message: 'Token refreshed' });
} catch {
return res.status(401).json({ error: 'Invalid refresh token' });
}
});
JWT Security Rules
- Never store in localStorage — vulnerable to XSS. Use HttpOnly cookies.
- Keep access tokens short-lived — 15 minutes max.
- Use refresh token rotation — new refresh token with each refresh.
- Include tokenVersion — increment on password change/logout to revoke all tokens.
- Validate algorithm — prevent
alg: noneattacks.
Option 3: OAuth 2.0
For third-party access ("Login with Google") or when external apps need to call your API on behalf of users.
When to Use OAuth vs JWT
| Scenario | Best Choice |
|---|---|
| Your frontend → Your API | Sessions or JWT |
| "Login with Google" button | OAuth 2.0 (OIDC) |
| Third-party app → Your API | OAuth 2.0 |
| Mobile app → Your API | JWT with PKCE flow |
| Microservice → Microservice | JWT or mTLS |
| CLI tool → API | OAuth 2.0 Device Flow |
Comparison Summary
| Feature | Sessions | JWT | OAuth 2.0 |
|---|---|---|---|
| Stateful/Stateless | Stateful | Stateless | Depends |
| Instant revocation | ✅ Easy | ❌ Hard (needs denylist) | ✅ Via provider |
| Scalability | Needs shared store | ✅ No server state | ✅ Delegated |
| Cross-domain | ❌ Difficult | ✅ Easy | ✅ Designed for it |
| XSS risk | Low (HttpOnly cookie) | High if in localStorage | Varies |
| CSRF risk | Yes (needs tokens) | No (if in headers) | Varies |
| Complexity | Low | Medium | High |
| Best for | Traditional web apps | SPAs, mobile, microservices | Third-party access |
The safest option for most web applications: Server-side sessions with Redis. If you need stateless tokens for mobile/SPA: JWT in HttpOnly cookies with short expiry and refresh rotation.
API Authentication FAQ
Is JWT better than sessions for API security?
Not automatically. JWTs are easier to scale across services, but sessions are often simpler to revoke and safer for traditional web apps. The secure choice depends on your architecture, not hype.
When should I use OAuth 2.0 instead of JWT?
Use OAuth 2.0 when a third-party app, external client, or delegated access model is involved. JWTs solve token format and session transport problems; OAuth solves authorization delegation.
What is the safest authentication method for a normal web app?
For most web applications, server-side sessions with secure cookies remain the safest and simplest default. If you need API-first mobile or SPA support, use short-lived JWTs with refresh rotation and strict cookie handling.
Published by SecureCodeReviews
This article is part of our original AI security and cybersecurity content library. We show publish and update dates, keep company and policy pages public, and update important guidance when material changes affect readers.
Shipping an API that needs a hard security pass?
We review authentication, authorization, business logic abuse, rate limiting, and the edge cases automated scanners usually miss.
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Shadow APIs and Zombie APIs: API Discovery, Inventory, and Hidden Attack Surface Security
Learn how to find shadow APIs, track zombie APIs, build an API inventory, and reduce hidden API attack surface risk with practical API discovery and decommissioning strategies.
Threat Modeling for Developers: STRIDE, PASTA & DREAD with Practical Examples
Threat modeling is the most cost-effective security activity — finding design flaws before writing code. This guide covers STRIDE, PASTA, and DREAD methodologies with real-world examples for web, API, and cloud applications.
Building a Security Champions Program: Scaling Security Across Dev Teams
Security teams can't review every line of code. Security Champions embed security expertise in every development team. This guide covers program design, champion selection, training, metrics, and sustaining engagement.