avatar

Le Do Nghiem

Software Engineer

  • About me
  • Books
  • Snippets
  • Blog

© 2026 Le Do Nghiem. All rights reserved.

Contact |

Back to Blog

JWT vs Session Authentication: Choosing the Right Approach

avatar
Le Do NghiemSoftware Engineer
2025-12-17 5 min read

Introduction

Authentication is a critical aspect of web application security. Two popular approaches are JWT (JSON Web Tokens) and Session-based authentication. Each has its strengths and weaknesses. In this post, we'll explore both methods and help you choose the right one for your application.

Session-Based Authentication

Session authentication stores user state on the server. When a user logs in, the server creates a session and sends a session ID (usually in a cookie) to the client.

How It Works

// Login endpoint
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  // Verify credentials
  const user = await User.findOne({ email });
  if (!user || !await bcrypt.compare(password, user.password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Create session
  req.session.userId = user.id;
  req.session.save();
  
  res.json({ message: 'Login successful' });
});

// Protected route
app.get('/profile', requireAuth, (req, res) => {
  // req.session.userId is available
  res.json({ user: req.user });
});

Advantages

  • Server-side control: Can invalidate sessions immediately
  • Secure by default: Sensitive data never leaves the server
  • Simpler token management: No need to handle token expiration on client
  • Better for sensitive operations: Banking, healthcare, etc.

Disadvantages

  • Server storage: Requires database/Redis for session storage
  • Scalability concerns: Need shared session storage in distributed systems
  • Cookie-based: Can have issues with CORS and mobile apps

JWT-Based Authentication

JWT authentication is stateless. The server creates a token containing user information, signs it, and sends it to the client. The client includes this token in subsequent requests.

How It Works

import jwt from 'jsonwebtoken';

// Login endpoint
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await User.findOne({ email });
  if (!user || !await bcrypt.compare(password, user.password)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Create JWT
  const token = jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );
  
  res.json({ token });
});

// Protected route
app.get('/profile', authenticateToken, async (req, res) => {
  // req.user is set by authenticateToken middleware
  const user = await User.findById(req.user.userId);
  res.json({ user });
});

// Middleware
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: 'Invalid token' });
    req.user = user;
    next();
  });
}

Advantages

  • Stateless: No server-side storage needed
  • Scalable: Works well with microservices and load balancers
  • Mobile-friendly: Easy to use with mobile apps
  • Cross-domain: Works across different domains

Disadvantages

  • Cannot revoke easily: Tokens are valid until expiration
  • Larger payload: Token size can be larger than session ID
  • Security concerns: If token is stolen, it's valid until expiration
  • No built-in logout: Need to implement token blacklisting

Hybrid Approach: Refresh Tokens

A common pattern is using both: short-lived JWT access tokens with refresh tokens.

// Login
app.post('/login', async (req, res) => {
  const user = await validateUser(req.body);
  
  const accessToken = jwt.sign(
    { userId: user.id },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
  
  const refreshToken = jwt.sign(
    { userId: user.id },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
  
  // Store refresh token in database
  await RefreshToken.create({
    userId: user.id,
    token: refreshToken
  });
  
  res.json({ accessToken, refreshToken });
});

// Refresh access token
app.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  const storedToken = await RefreshToken.findOne({ token: refreshToken });
  if (!storedToken) {
    return res.status(403).json({ error: 'Invalid refresh token' });
  }
  
  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: 'Invalid token' });
    
    const newAccessToken = jwt.sign(
      { userId: user.userId },
      process.env.ACCESS_TOKEN_SECRET,
      { expiresIn: '15m' }
    );
    
    res.json({ accessToken: newAccessToken });
  });
});

When to Use Each

Use Sessions When:

  • You need immediate logout capability
  • Building traditional web applications
  • Security is paramount (banking, healthcare)
  • You have control over server infrastructure
  • You need to track active sessions

Use JWT When:

  • Building microservices architecture
  • Need stateless authentication
  • Building mobile applications
  • Cross-domain authentication required
  • API-first architecture

Security Best Practices

For Sessions:

// Secure session configuration
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only
    httpOnly: true, // Prevent XSS
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    sameSite: 'strict' // CSRF protection
  }
}));

For JWT:

// Secure JWT implementation
const token = jwt.sign(
  payload,
  process.env.JWT_SECRET,
  {
    expiresIn: '15m',
    issuer: 'your-app',
    audience: 'your-app-users'
  }
);

// Always verify
jwt.verify(token, process.env.JWT_SECRET, {
  issuer: 'your-app',
  audience: 'your-app-users'
});

Common Vulnerabilities

JWT Vulnerabilities:

  1. Weak secret keys: Use strong, random secrets
  2. No expiration: Always set expiration times
  3. Storing sensitive data: Keep payload minimal
  4. No token rotation: Implement refresh tokens

Session Vulnerabilities:

  1. Session fixation: Regenerate session ID on login
  2. Session hijacking: Use HTTPS, secure cookies
  3. CSRF attacks: Use CSRF tokens, SameSite cookies

Conclusion

Both JWT and Session authentication have their place:

  • Sessions are better for traditional web apps requiring immediate control
  • JWT excels in stateless, distributed systems
  • Hybrid approaches combine the best of both worlds

Choose based on your specific requirements: architecture, security needs, and scalability concerns. Remember, security is not just about the authentication method—it's about proper implementation and following best practices!

Previous Post

Dependency Injection in ASP.NET Core

Next Post

Understanding 'use client' and 'use server' in Next.js