API Authentication: JWT vs Session vs OAuth 2.0 Security Comparison

SCRs Team
March 4, 2026
Updated May 8, 2026
16 min read
589 words
Share

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 dataDoesn't scale to multiple domains easily
Smaller cookie size (~32 bytes)Stateful — every request hits the store
Battle-tested for 25+ yearsCSRF 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

  1. Never store in localStorage — vulnerable to XSS. Use HttpOnly cookies.
  2. Keep access tokens short-lived — 15 minutes max.
  3. Use refresh token rotation — new refresh token with each refresh.
  4. Include tokenVersion — increment on password change/logout to revoke all tokens.
  5. Validate algorithm — prevent alg: none attacks.

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

ScenarioBest Choice
Your frontend → Your APISessions or JWT
"Login with Google" buttonOAuth 2.0 (OIDC)
Third-party app → Your APIOAuth 2.0
Mobile app → Your APIJWT with PKCE flow
Microservice → MicroserviceJWT or mTLS
CLI tool → APIOAuth 2.0 Device Flow

Comparison Summary

FeatureSessionsJWTOAuth 2.0
Stateful/StatelessStatefulStatelessDepends
Instant revocation✅ Easy❌ Hard (needs denylist)✅ Via provider
ScalabilityNeeds shared store✅ No server state✅ Delegated
Cross-domain❌ Difficult✅ Easy✅ Designed for it
XSS riskLow (HttpOnly cookie)High if in localStorageVaries
CSRF riskYes (needs tokens)No (if in headers)Varies
ComplexityLowMediumHigh
Best forTraditional web appsSPAs, mobile, microservicesThird-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.

Editorial standards

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.

Named author: SCRs Team
Published: Mar 4, 2026
Updated: May 8, 2026

Questions or corrections?

Review our editorial standards, learn more about the company, or contact us if a page needs clarification.

API Security Review

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.

OWASP API risk coverage and business logic testing
Auth and access control review with practical fixes
Clear technical findings your team can act on fast

Talk to SecureCodeReviews

Get a scoped review path fast

Manual review
Actionable fixes
Fast turnaround
Security-focused

Advertisement