Modern Authentication: OAuth 2.0, JWT, and Session Management

Modern Authentication: OAuth 2.0, JWT, and Session Management

BySanjay Goraniya
3 min read
Share:

Modern Authentication: OAuth 2.0, JWT, and Session Management

Authentication is one of the most critical parts of any application. Get it wrong, and you have a security vulnerability. Get it right, and users can access your application securely. After implementing authentication for multiple production systems, I've learned the patterns that work.

Authentication vs. Authorization

Authentication

Who are you? - Verifying identity

Code
// User provides credentials
const user = await authenticate(email, password);
// Returns user if credentials are valid

Authorization

What can you do? - Verifying permissions

Code
// Check if user can perform action
if (user.role === 'admin') {
  // Allow action
}

Session-Based Authentication

How It Works

  1. User logs in with credentials
  2. Server creates session (stored server-side)
  3. Server sends session ID (cookie)
  4. Client sends session ID with requests
  5. Server validates session ID

Implementation

Code
const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true, // HTTPS only
    httpOnly: true, // Not accessible via JavaScript
    maxAge: 3600000, // 1 hour
    sameSite: 'strict' // CSRF protection
  }
}));

// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await authenticateUser(email, password);
  
  if (user) {
    req.session.userId = user.id;
    req.session.role = user.role;
    res.json({ success: true });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

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

// Logout
app.post('/logout', (req, res) => {
  req.session.destroy();
  res.json({ success: true });
});

Session Storage

In-memory (development):

Code
// Simple, but doesn't scale

Redis (production):

Code
const RedisStore = require('connect-redis')(session);
const redis = require('redis');

app.use(session({
  store: new RedisStore({ client: redis.createClient() }),
  secret: process.env.SESSION_SECRET,
  // ... other options
}));

JWT (JSON Web Tokens)

How JWT Works

  1. User logs in
  2. Server creates JWT (signed, not encrypted)
  3. Server sends JWT to client
  4. Client stores JWT (localStorage, cookie)
  5. Client sends JWT with requests
  6. Server validates JWT signature

Creating JWTs

Code
const jwt = require('jsonwebtoken');

function generateToken(user) {
  return jwt.sign(
    {
      userId: user.id,
      email: user.email,
      role: user.role
    },
    process.env.JWT_SECRET,
    {
      expiresIn: '1h',
      issuer: 'my-app',
      audience: 'my-app-users'
    }
  );
}

Verifying JWTs

Code
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET, {
      issuer: 'my-app',
      audience: 'my-app-users'
    });
  } catch (error) {
    throw new Error('Invalid token');
  }
}

// Middleware
function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }
  
  try {
    const decoded = verifyToken(token);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Refresh Tokens

Code
// Access token (short-lived)
const accessToken = jwt.sign(
  { userId: user.id },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }
);

// Refresh token (long-lived)
const refreshToken = jwt.sign(
  { userId: user.id, type: 'refresh' },
  process.env.REFRESH_TOKEN_SECRET,
  { expiresIn: '7d' }
);

// Store refresh token in database
await db.query(
  'INSERT INTO refresh_tokens (user_id, token) VALUES ($1, $2)',
  [user.id, refreshToken]
);

// Refresh endpoint
app.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  // Verify refresh token
  const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
  
  // Check if token exists in database
  const token = await db.query(
    'SELECT * FROM refresh_tokens WHERE token = $1',
    [refreshToken]
  );
  
  if (!token.rows.length) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
  
  // Generate new access token
  const newAccessToken = generateToken({ id: decoded.userId });
  res.json({ accessToken: newAccessToken });
});

OAuth 2.0

OAuth 2.0 Flow

  1. User clicks "Login with Google"
  2. Redirect to Google authorization
  3. User authorizes
  4. Google redirects back with code
  5. Exchange code for access token
  6. Use access token to get user info
  7. Create/login user in your app

Implementation

Code
const express = require('express');
const axios = require('axios');

// Step 1: Redirect to OAuth provider
app.get('/auth/google', (req, res) => {
  const params = new URLSearchParams({
    client_id: process.env.GOOGLE_CLIENT_ID,
    redirect_uri: 'http://localhost:3000/auth/google/callback',
    response_type: 'code',
    scope: 'openid email profile'
  });
  
  res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});

// Step 2: Handle callback
app.get('/auth/google/callback', async (req, res) => {
  const { code } = req.query;
  
  // Exchange code for token
  const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', {
    client_id: process.env.GOOGLE_CLIENT_ID,
    client_secret: process.env.GOOGLE_CLIENT_SECRET,
    code,
    redirect_uri: 'http://localhost:3000/auth/google/callback',
    grant_type: 'authorization_code'
  });
  
  const { access_token } = tokenResponse.data;
  
  // Get user info
  const userResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { Authorization: `Bearer ${access_token}` }
  });
  
  const { id, email, name } = userResponse.data;
  
  // Create or login user
  let user = await findUserByGoogleId(id);
  if (!user) {
    user = await createUser({ googleId: id, email, name });
  }
  
  // Create session or JWT
  req.session.userId = user.id;
  res.redirect('/dashboard');
});

When to Use What

Use Sessions When

  • Server-side rendering - Traditional web apps
  • Need to revoke immediately - Logout = destroy session
  • Sensitive data - Tokens stored server-side
  • Simple implementation - Easier to understand

Use JWT When

  • Stateless APIs - Microservices, mobile apps
  • Cross-domain - Multiple domains
  • Scalability - No session storage needed
  • Mobile apps - Native apps

Use OAuth When

  • Third-party login - "Login with Google"
  • API access - Accessing user's data from other services
  • Enterprise SSO - Single sign-on

Security Best Practices

Password Hashing

Code
const bcrypt = require('bcrypt');

// Hash password
const hashedPassword = await bcrypt.hash(password, 10);

// Verify password
const isValid = await bcrypt.compare(password, hashedPassword);

Rate Limiting

Code
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts
  message: 'Too many login attempts'
});

app.post('/login', loginLimiter, loginHandler);

CSRF Protection

Code
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.use(csrfProtection);

app.get('/form', (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

Real-World Example

Application: SaaS platform with web and mobile apps

Authentication Strategy:

  • Web app: Sessions (traditional, secure)
  • Mobile app: JWT with refresh tokens
  • Third-party login: OAuth 2.0 (Google, GitHub)
  • API: JWT for stateless authentication

Implementation:

Code
// Unified authentication
async function authenticate(req) {
  // Try session first (web)
  if (req.session?.userId) {
    return { userId: req.session.userId };
  }
  
  // Try JWT (mobile/API)
  const token = req.headers.authorization?.split(' ')[1];
  if (token) {
    const decoded = verifyToken(token);
    return { userId: decoded.userId };
  }
  
  throw new Error('Not authenticated');
}

Best Practices

  1. Hash passwords - Never store plain text
  2. Use HTTPS - Always in production
  3. Set expiration - Tokens and sessions should expire
  4. Validate input - Sanitize all user input
  5. Rate limit - Prevent brute force attacks
  6. Log security events - Monitor for attacks
  7. Use secure cookies - HttpOnly, Secure, SameSite
  8. Rotate secrets - Change secrets regularly

Conclusion

Authentication is complex, but with the right patterns:

  • Sessions - Simple, secure, server-side
  • JWT - Stateless, scalable, cross-domain
  • OAuth - Third-party, standardized

Choose based on your needs, and always prioritize security over convenience.

Remember: Authentication is the foundation of security. Get it right, and everything else is easier.

What authentication challenges have you faced? What patterns have worked best for your applications?

Share:

Related Posts