Application Security
jwt
session
oauth
authentication
+3 more

JWT vs Session vs OAuth: Which API Authentication Should You Use?

SCRs Team
March 4, 2026
16 min read
Share

The Authentication Decision Tree

Choosing the right authentication method is one of the most impactful security decisions you'll make. 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.

Advertisement