DEV Community

Cover image for JWT vs Sessions: A Complete Guide to Modern Web Authentication (Security, Flow, and Best Practices)
Yukti Sahu
Yukti Sahu

Posted on

JWT vs Sessions: A Complete Guide to Modern Web Authentication (Security, Flow, and Best Practices)

Table of Contents

  1. Understanding HTTP Statelessness
  2. Sessions - The Traditional Approach
  3. JWT - Modern Stateless Authentication
  4. Cookies Explained
  5. Authentication Types
  6. OAuth 2.0 & OpenID Connect
  7. When to Use What

1. Understanding HTTP Statelessness

The Problem

HTTP is stateless by design - it has no memory of past interactions. Each request is independent and the server doesn't remember previous requests from the same client.

Early Web (Static Websites):

  • Simple HTML pages
  • No user accounts or personalization
  • Statelessness wasn't a problem

Modern Web (Dynamic Content):

  • User profiles and dashboards
  • Shopping carts
  • Personalized content
  • Need to keep users logged in
  • Server needs to "remember" who you are

Why We Needed Sessions

When websites became dynamic, we needed stateful interactions - a way for the server to maintain context about each user across multiple requests.

Example Scenario:

Without Sessions:
User logs in → Server verifies ✓
User views profile → Server asks: "Who are you?" ❌
User adds to cart → Server asks: "Who are you?" ❌
Enter fullscreen mode Exit fullscreen mode

This is where sessions came to rescue!


2. Sessions - The Traditional Approach

How Sessions Work

A session is a temporary, server-side context that stores information about a user during their visit to a website.

Flow Diagram:

1. User logs in (username + password)
   ↓
2. Server validates credentials ✓
   ↓
3. Server creates unique Session ID
   ↓
4. Session ID stored in database/Redis
   ↓
5. Session ID sent to client as Cookie
   ↓
6. Client sends cookie with every request
   ↓
7. Server looks up session to identify user
Enter fullscreen mode Exit fullscreen mode

Code Example - Session-Based Authentication

Server-side (Node.js with Express):

const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');

const app = express();
const redisClient = redis.createClient();

// Configure session middleware
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: { 
    maxAge: 3600000, // 1 hour
    httpOnly: true,  // Cannot be accessed by JavaScript
    secure: true     // Only sent over HTTPS
  }
}));

// Login endpoint
app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  // Validate credentials (simplified)
  const user = await validateUser(username, password);

  if (user) {
    // Store user info in session
    req.session.userId = user.id;
    req.session.username = user.username;
    req.session.role = user.role;

    res.json({ message: 'Login successful' });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

// Protected route
app.get('/profile', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  res.json({
    userId: req.session.userId,
    username: req.session.username
  });
});

// Logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' });
    }
    res.json({ message: 'Logged out successfully' });
  });
});
Enter fullscreen mode Exit fullscreen mode

What's Stored in Redis:

Session ID: "sess:abc123xyz"
Data: {
  "userId": 42,
  "username": "john_doe",
  "role": "admin",
  "createdAt": "2025-11-21T10:00:00Z",
  "expiresAt": "2025-11-21T11:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Evolution of Session Storage

1. File-Based Sessions (Early Days):

  • Stored in server's file system
  • Problems: Slow, not scalable, difficult to share across servers

2. Database Sessions:

  • Stored in MySQL, PostgreSQL
  • Better than files but still had performance issues

3. In-Memory Stores (Current):

  • Redis, Memcached
  • Fast lookups (microseconds)
  • Easy to scale horizontally

Problems with Sessions in Distributed Systems

As the web evolved into distributed architectures, sessions created bottlenecks:

1. Memory/Storage Costs:

Scenario: 1 million active users
Session Size: ~1KB per user
Total Storage: 1GB just for sessions
Cost: $$$$ at scale
Enter fullscreen mode Exit fullscreen mode

2. Replication Issues:

User in USA → Server A (has session)
User in Europe → Server B (no session) ❌
Need to sync sessions across regions → Latency
Enter fullscreen mode Exit fullscreen mode

3. Consistency Challenges:

User updates profile on Server A
Server B still has old session data
Race conditions and stale data
Enter fullscreen mode Exit fullscreen mode

This led to the birth of JWT!


3. JWT - Modern Stateless Authentication

What is JWT?

JWT (JSON Web Token) was formalized in 2015 as a stateless mechanism for securely transferring claims between parties.

A JWT is a self-contained token that includes:

  • User data (payload)
  • Metadata (header)
  • Cryptographic signature (for verification)

JWT Structure

A JWT has three parts separated by dots (.):

xxxxx.yyyyy.zzzzz
[HEADER].[PAYLOAD].[SIGNATURE]
Enter fullscreen mode Exit fullscreen mode

Example JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzMyMTg4MDAwLCJleHAiOjE3MzIxOTE2MDB9.4p8JkB8k3Yn8G7X9xF5qZ2eR4wT6vY3nM1oL7pQ2sA4
Enter fullscreen mode Exit fullscreen mode

Decoded:

1. Header (Algorithm & Token Type):

{
  "alg": "HS256",
  "typ": "JWT"
}
Enter fullscreen mode Exit fullscreen mode

2. Payload (Claims/User Data):

{
  "userId": 42,
  "username": "john_doe",
  "role": "admin",
  "iat": 1732188000,  // Issued at
  "exp": 1732191600   // Expires at (1 hour later)
}
Enter fullscreen mode Exit fullscreen mode

3. Signature (Verification):

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)
Enter fullscreen mode Exit fullscreen mode

How JWT Works - Step by Step

Flow:

1. User logs in with credentials
   ↓
2. Server validates user
   ↓
3. Server creates JWT with user data + signs it with secret key
   ↓
4. JWT sent to client (usually in response body)
   ↓
5. Client stores JWT (localStorage/cookie)
   ↓
6. Client sends JWT in Authorization header with each request
   ↓
7. Server verifies signature using secret key
   ↓
8. If valid → Grant access | If invalid → Reject
Enter fullscreen mode Exit fullscreen mode

Code Example - JWT Authentication

Server-side (Node.js):

const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

const app = express();
const SECRET_KEY = 'your-super-secret-key-keep-it-safe';

// Login endpoint
app.post('/login', async (req, res) => {
  const { username, password } = req.body;

  // Find user in database
  const user = await findUserByUsername(username);

  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Verify password
  const isPasswordValid = await bcrypt.compare(password, user.passwordHash);

  if (!isPasswordValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Create JWT payload
  const payload = {
    userId: user.id,
    username: user.username,
    role: user.role
  };

  // Sign JWT (expires in 1 hour)
  const token = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });

  // Send token to client
  res.json({ 
    token: token,
    message: 'Login successful'
  });
});

// Middleware to verify JWT
function authenticateToken(req, res, next) {
  // Get token from Authorization header
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // "Bearer TOKEN"

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  // Verify token
  jwt.verify(token, SECRET_KEY, (err, decoded) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid or expired token' });
    }

    // Attach user info to request
    req.user = decoded;
    next();
  });
}

// Protected route
app.get('/profile', authenticateToken, (req, res) => {
  res.json({
    userId: req.user.userId,
    username: req.user.username,
    role: req.user.role
  });
});
Enter fullscreen mode Exit fullscreen mode

Client-side (JavaScript):

// Login
async function login(username, password) {
  const response = await fetch('/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password })
  });

  const data = await response.json();

  if (response.ok) {
    // Store token in localStorage
    localStorage.setItem('token', data.token);
  }
}

// Making authenticated requests
async function getProfile() {
  const token = localStorage.getItem('token');

  const response = await fetch('/profile', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  return await response.json();
}
Enter fullscreen mode Exit fullscreen mode

Sessions vs JWT Comparison

Aspect Sessions JWT
Storage Server-side (DB/Redis) Client-side (browser)
State Stateful Stateless
Scalability Harder (need session sync) Easy (no server storage)
Memory High (stores all sessions) Low (no server storage)
Revocation Easy (delete from DB) Hard (token valid until expiry)
Database Lookup Yes (every request) No (just verify signature)
Cost Higher (storage costs) Lower (no storage)
Security Risk Lower (can invalidate anytime) Higher (stolen token valid until expiry)

JWT Advantages

Stateless - No database lookup needed
Scalable - Works perfectly in distributed systems
Portable - Can be used across different domains
Lightweight - No server-side storage
Cost-effective - Saves storage costs
Fast - No database queries for authentication

JWT Disadvantages

Token Theft - If someone steals your token, they can impersonate you
No Immediate Revocation - Can't invalidate a token before it expires
Payload Size - Larger than session IDs (sent with every request)
Secret Key Management - If secret key is compromised, all tokens are invalid

Hybrid Approaches (Best of Both Worlds)

Modern applications often combine both approaches:

1. JWT with Blacklist:

// When user logs out or token is compromised
app.post('/logout', authenticateToken, async (req, res) => {
  const token = req.headers['authorization'].split(' ')[1];

  // Add token to blacklist in Redis
  await redisClient.set(
    `blacklist:${token}`, 
    'true', 
    'EX', 
    3600 // Expire when token would expire anyway
  );

  res.json({ message: 'Logged out successfully' });
});

// Modify authentication middleware
function authenticateToken(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  // Check if token is blacklisted
  redisClient.get(`blacklist:${token}`, (err, result) => {
    if (result) {
      return res.status(403).json({ error: 'Token has been revoked' });
    }

    // Verify JWT as usual
    jwt.verify(token, SECRET_KEY, (err, decoded) => {
      if (err) {
        return res.status(403).json({ error: 'Invalid token' });
      }
      req.user = decoded;
      next();
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

2. Refresh Tokens Pattern:

// Issue both access token (short-lived) and refresh token (long-lived)
app.post('/login', async (req, res) => {
  const user = await validateUser(req.body.username, req.body.password);

  if (user) {
    // Short-lived access token (15 minutes)
    const accessToken = jwt.sign(
      { userId: user.id, username: user.username },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    // Long-lived refresh token (7 days)
    const refreshToken = jwt.sign(
      { userId: user.id },
      REFRESH_SECRET,
      { expiresIn: '7d' }
    );

    // Store refresh token in database
    await saveRefreshToken(user.id, refreshToken);

    res.json({ accessToken, refreshToken });
  }
});

// Refresh endpoint
app.post('/refresh', async (req, res) => {
  const { refreshToken } = req.body;

  // Verify refresh token
  jwt.verify(refreshToken, REFRESH_SECRET, async (err, decoded) => {
    if (err) {
      return res.status(403).json({ error: 'Invalid refresh token' });
    }

    // Check if refresh token exists in database
    const isValid = await verifyRefreshToken(decoded.userId, refreshToken);

    if (!isValid) {
      return res.status(403).json({ error: 'Refresh token revoked' });
    }

    // Issue new access token
    const newAccessToken = jwt.sign(
      { userId: decoded.userId },
      ACCESS_SECRET,
      { expiresIn: '15m' }
    );

    res.json({ accessToken: newAccessToken });
  });
});
Enter fullscreen mode Exit fullscreen mode

3. Using Auth Providers (Third-Party Solutions):

Modern applications often use Authentication Providers like:

  • Auth0
  • Firebase Authentication
  • AWS Cognito
  • Clerk
  • Supabase Auth

What Auth Providers Solve:

  • Handle complex security logic for you
  • Provide built-in user management
  • Offer social login (Google, Facebook, etc.)
  • Handle token refresh automatically
  • Provide MFA (Multi-Factor Authentication)
  • Manage password resets and email verification
  • Give you security best practices out of the box

Example with Auth0:

// Frontend
import { Auth0Provider, useAuth0 } from '@auth0/auth0-react';

function App() {
  return (
    <Auth0Provider
      domain="your-domain.auth0.com"
      clientId="your-client-id"
      redirectUri={window.location.origin}
    >
      <MyApp />
    </Auth0Provider>
  );
}

function LoginButton() {
  const { loginWithRedirect } = useAuth0();
  return <button onClick={loginWithRedirect}>Log In</button>;
}

function Profile() {
  const { user, isAuthenticated, getAccessTokenSilently } = useAuth0();

  const callAPI = async () => {
    const token = await getAccessTokenSilently();

    const response = await fetch('/api/protected', {
      headers: {
        Authorization: `Bearer ${token}`
      }
    });
  };

  return isAuthenticated && <div>Hello {user.name}</div>;
}
Enter fullscreen mode Exit fullscreen mode

4. Cookies Explained {#cookies}

What are Cookies?

A cookie is a small piece of data (text) that a server sends to a user's browser, which the browser stores and sends back with every subsequent request to that server.

Think of it like a ticket:

  • You go to an amusement park (website)
  • They give you a wristband (cookie)
  • Every time you go to a ride (make a request), they check your wristband

Cookie Properties

// Setting a cookie from server
res.cookie('sessionId', 'abc123', {
  httpOnly: true,    // Cannot be accessed by JavaScript
  secure: true,      // Only sent over HTTPS
  sameSite: 'strict', // CSRF protection
  maxAge: 3600000,   // Expires in 1 hour
  domain: '.example.com', // Available to all subdomains
  path: '/'          // Available to all paths
});
Enter fullscreen mode Exit fullscreen mode

Cookie Example in HTTP Response:

HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
Content-Type: application/json
Enter fullscreen mode Exit fullscreen mode

How Cookies Work

1. Server Sets Cookie:

app.post('/login', (req, res) => {
  // Validate user...

  // Set cookie
  res.cookie('userId', user.id, {
    httpOnly: true,
    secure: true,
    maxAge: 3600000
  });

  res.json({ message: 'Logged in' });
});
Enter fullscreen mode Exit fullscreen mode

2. Browser Stores Cookie:

Browser Storage:
userId=42; expires=Fri, 21-Nov-2025 11:00:00 GMT; path=/; HttpOnly; Secure
Enter fullscreen mode Exit fullscreen mode

3. Browser Sends Cookie Automatically:

// Every request to same domain automatically includes cookies
fetch('/api/profile')
// Browser automatically adds: Cookie: userId=42
Enter fullscreen mode Exit fullscreen mode

4. Server Reads Cookie:

app.get('/profile', (req, res) => {
  const userId = req.cookies.userId;

  // Find user by ID
  const user = findUserById(userId);

  res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

Important Cookie Rules

Security Rules:

  1. httpOnly - JavaScript cannot access the cookie (prevents XSS attacks)
  2. secure - Cookie only sent over HTTPS
  3. sameSite - Prevents CSRF attacks
    • strict - Cookie only sent to same site
    • lax - Cookie sent with top-level navigation
    • none - Cookie sent with cross-site requests (needs secure)

Domain Rules:

  • A server can only access cookies it set
  • example.com cannot read cookies from google.com
  • Cookies are automatically sent with every request to the domain

Cookies vs LocalStorage

Feature Cookies LocalStorage
Sent to Server Yes (automatically) No
Size Limit 4KB 5-10MB
Accessible by JS Only if not httpOnly Always
Expiration Can be set Never (until cleared)
Security More secure (httpOnly) Less secure (XSS vulnerable)

5. Authentication Types

1. Stateful Authentication (Session-Based)

Components:

  • Client (Browser)
  • Server
  • Database/Redis (for session storage)

How it Works:

┌─────────┐                    ┌─────────┐                    ┌─────────┐
│ Client  │                    │ Server  │                    │Database │
└────┬────┘                    └────┬────┘                    └────┬────┘
     │                              │                              │
     │ POST /login                  │                              │
     │ {username, password}         │                              │
     ├─────────────────────────────>│                              │
     │                              │                              │
     │                              │ Validate credentials         │
     │                              ├─────────────────────────────>│
     │                              │                              │
     │                              │<─────────────────────────────┤
     │                              │ User found ✓                 │
     │                              │                              │
     │                              │ Generate Session ID          │
     │                              │ Store in Redis               │
     │                              ├─────────────────────────────>│
     │                              │                              │
     │  Set-Cookie: sessionId       │                              │
     │<─────────────────────────────┤                              │
     │                              │                              │
     │ GET /profile                 │                              │
     │ Cookie: sessionId            │                              │
     ├─────────────────────────────>│                              │
     │                              │                              │
     │                              │ Lookup session               │
     │                              ├─────────────────────────────>│
     │                              │                              │
     │                              │<─────────────────────────────┤
     │                              │ Session data                 │
     │                              │                              │
     │  Profile data                │                              │
     │<─────────────────────────────┤                              │
Enter fullscreen mode Exit fullscreen mode

When to Use:

  • When you need instant token revocation
  • When managing long-lived sessions
  • When you have a monolithic application
  • When security is the top priority

2. Stateless Authentication (JWT-Based)

Components:

  • Client (Browser)
  • Server
  • No database needed for authentication!

How it Works:

┌─────────┐                          ┌─────────┐
│ Client  │                          │ Server  │
└────┬────┘                          └────┬────┘
     │                                    │
     │ POST /login                        │
     │ {username, password}               │
     ├───────────────────────────────────>│
     │                                    │
     │                                    │ Validate credentials
     │                                    │ (checks database)
     │                                    │
     │                                    │ Generate JWT
     │                                    │ Sign with secret key
     │                                    │
     │  { token: "eyJhbG..." }            │
     │<───────────────────────────────────┤
     │                                    │
     │ Store token in localStorage        │
     │                                    │
     │ GET /profile                       │
     │ Authorization: Bearer eyJhbG...    │
     ├───────────────────────────────────>│
     │                                    │
     │                                    │ Verify JWT signature
     │                                    │ (No database lookup!)
     │                                    │ Decode payload
     │                                    │
     │  Profile data                      │
     │<───────────────────────────────────┤
Enter fullscreen mode Exit fullscreen mode

When to Use:

  • Microservices architecture
  • Mobile applications
  • When you need horizontal scalability
  • When you want to minimize database queries
  • Cross-domain authentication (different domains)

3. API Key Authentication

What is an API Key?
An API key is a simple token that identifies an application or user making API requests.

How it Works:

1. User/App registers and receives API key
   ↓
2. API key is long random string: "sk_live_51HabcD123..."
   ↓
3. Client sends API key with every request
   ↓
4. Server validates API key against database
Enter fullscreen mode Exit fullscreen mode

Code Example:

Server-side:

// Generate API key
const crypto = require('crypto');

function generateAPIKey() {
  return 'sk_' + crypto.randomBytes(32).toString('hex');
}

// Store in database
const apiKey = generateAPIKey();
await db.apiKeys.create({
  key: apiKey,
  userId: user.id,
  createdAt: new Date()
});

// Middleware to validate API key
function validateAPIKey(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' });
  }

  // Check if API key exists and is valid
  db.apiKeys.findOne({ key: apiKey }, (err, keyData) => {
    if (err || !keyData) {
      return res.status(403).json({ error: 'Invalid API key' });
    }

    req.userId = keyData.userId;
    next();
  });
}

// Protected route
app.get('/api/data', validateAPIKey, (req, res) => {
  res.json({ data: 'Your secret data', userId: req.userId });
});
Enter fullscreen mode Exit fullscreen mode

Client-side:

// Making API request with API key
fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': 'sk_live_51HabcD123...'
  }
});
Enter fullscreen mode Exit fullscreen mode

Real-world Examples:

  • Stripe API: sk_live_51H...
  • OpenAI API: sk-proj-...
  • Google Maps API: Uses API keys
  • SendGrid: Uses API keys

API Key Best Practices:

  • Never commit API keys to Git
  • Use environment variables
  • Rotate keys periodically
  • Have separate keys for development and production
  • Rate limit API key usage

When to Use:

  • Server-to-server communication
  • Third-party integrations
  • Automated scripts/bots
  • Webhooks
  • Public APIs with usage tracking

4. OAuth 2.0 Authentication

(See detailed section below)


6. OAuth 2.0 & OpenID Connect {#oauth}

The Problem OAuth Solved

Before OAuth - The Delegation Problem:

Imagine you want Facebook to access your Google contacts:

The Bad Old Way:

You: "Hey Facebook, get my Google contacts"
Facebook: "Give me your Google password"
You: "Here's my password: myPassword123"
Facebook: *logs into Google with your password*
Facebook: *can now access EVERYTHING - emails, drive, calendar*
Enter fullscreen mode Exit fullscreen mode

Problems:

  • You had to share your password
  • Third-party app had FULL access to your account
  • No way to revoke access without changing password
  • If third-party got hacked, your password is exposed

What is OAuth 2.0?

OAuth 2.0 is an authorization framework that allows third-party applications to access your resources without sharing your password.

Key Concept: Instead of sharing passwords, you share tokens with limited permissions.

OAuth Terminology

  • Resource Owner: You (the user)
  • Client: The app wanting access (e.g., Facebook)
  • Resource Server: Where your data lives (e.g., Google)
  • Authorization Server: Issues tokens (often same as resource server)

How OAuth 2.0 Works

Example: "Login with Google" on a Website

┌──────────┐              ┌──────────┐              ┌──────────┐
│   You    │              │  Website │              │  Google  │
│ (User)   │              │ (Client) │              │  (Auth)  │
└────┬─────┘              └────┬─────┘              └────┬─────┘
     │                         │                         │
     │ Click "Login with Google"                        │
     ├────────────────────────>│                         │
     │                         │                         │
     │                         │ Redirect to Google      │
     │                         │ with client_id          │
     │<────────────────────────┤                         │
     │                         │                         │
     │              Browser redirects to Google          │
     ├──────────────────────────────────────────────────>│
     │                         │                         │
     │                         │      Google login page  │
     │<──────────────────────────────────────────────────┤
     │                         │                         │
     │ Enter Google credentials                          │
     ├──────────────────────────────────────────────────>│
     │                         │                         │
     │       "Allow Website to access your profile?"     │
     │<──────────────────────────────────────────────────┤
     │                         │                         │
     │ Click "Allow"                                     │
     ├──────────────────────────────────────────────────>│
     │                         │                         │
     │                         │  Redirect with auth code│
     │<────────────────────────┤<────────────────────────┤
     │                         │                         │
     │                         │ Exchange code for token │
     │                         ├────────────────────────>│
     │                         │                         │
     │                         │<────────────────────────┤
     │                         │  Access Token           │
     │                         │                         │
     │   You're logged in!     │                         │
     │<────────────────────────┤                         │
Enter fullscreen mode Exit fullscreen mode

OAuth 2.0 Flow Types (Grant Types)

1. Authorization Code Flow (Most Secure - for web apps)

// Step 1: Redirect user to authorization server
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://yourapp.com/callback&
  response_type=code&
  scope=profile email&
  state=random_string_for_security`;

window.location.href = authUrl;

// Step 2: Handle callback (server-side)
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state to prevent CSRF
  if (state !== expectedState) {
    return res.status(403).send('Invalid state');
  }

  // Exchange code for token
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      code: code,
      client_id: YOUR_CLIENT_ID,
      client_secret: YOUR_CLIENT_SECRET,
      redirect_uri: 'https://yourapp.com/callback',
      grant_type: 'authorization_code'
    })
  });

  const tokens = await tokenResponse.json();
  // tokens = { access_token, refresh_token, expires_in }

  // Use access token to get user info
  const userResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
    headers: { Authorization: `Bearer ${tokens.access_token}` }
  });

  const user = await userResponse.json();
  // user = { id, email, name, picture }

  // Create session or JWT for your app
  req.session.userId = user.id;
  res.redirect('/dashboard');
});
Enter fullscreen mode Exit fullscreen mode

2. Implicit Flow (Legacy - for single-page apps, less secure)

// Returns token directly in URL (not recommended anymore)
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://yourapp.com/callback&
  response_type=token&  // Returns token directly
  scope=profile email`;
Enter fullscreen mode Exit fullscreen mode

3. Client Credentials Flow (For server-to-server)

// No user involved - app authenticates itself
const response = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  body: JSON.stringify({
    client_id: YOUR_CLIENT_ID,
    client_secret: YOUR_CLIENT_SECRET,
    grant_type: 'client_credentials',
    scope: 'https://www.googleapis.com/auth/cloud-platform'
  })
});

const { access_token } = await response.json();
Enter fullscreen mode Exit fullscreen mode

4. Password Grant Flow (Not recommended - requires sharing password)

// User provides username/password to client app
// Only use if you trust the client completely
const response = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  body: JSON.stringify({
    grant_type: 'password',
    username: 'user@example.com',
    password: 'user_password',
    client_id: YOUR_CLIENT_ID,
    client_secret: YOUR_CLIENT_SECRET
  })
});
Enter fullscreen mode Exit fullscreen mode

OAuth 2.0 Tokens

Access Token:

Purpose: Short-lived token to access resources
Lifetime: 15 minutes - 1 hour
Example: "ya29.a0AfH6SMBx..."
Enter fullscreen mode Exit fullscreen mode

Refresh Token:

Purpose: Long-lived token to get new access tokens
Lifetime: Days to months
Example: "1//0gKz8..."
Enter fullscreen mode Exit fullscreen mode

Token Refresh Flow:

// When access token expires
async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    body: JSON.stringify({
      client_id: YOUR_CLIENT_ID,
      client_secret: YOUR_CLIENT_SECRET,
      refresh_token: refreshToken,
      grant_type: 'refresh_token'
    })
  });

  const { access_token, expires_in } = await response.json();
  return access_token;
}
Enter fullscreen mode Exit fullscreen mode

OAuth Scopes (Permissions)

Scopes define what the third-party app can access:

// Example scopes for Google
scope: 'profile email'  // Only basic profile
scope: 'https://www.googleapis.com/auth/drive.readonly'  // Read Drive files
scope: 'https://www.googleapis.com/auth/gmail.send'  // Send emails

// GitHub scopes
scope: 'read:user'  // Read user profile
scope: 'repo'  // Access repositories
scope: 'user:email'  // Access email addresses
Enter fullscreen mode Exit fullscreen mode

User sees:

Website XYZ wants to:
✓ View your basic profile
✓ View your email address
✗ NOT access your Drive files
✗ NOT send emails on your behalf

[Allow] [Deny]
Enter fullscreen mode Exit fullscreen mode

Evolution: OAuth 1.0 → OAuth 2.0

OAuth 1.0 (2007):

  • Very complex cryptographic signatures
  • Difficult for developers to implement
  • Required signing every request
  • Limited to web applications
// OAuth 1.0 - Complex signature generation
const signature = crypto
  .createHmac('sha1', signingKey)
  .update(signatureBase)
  .digest('base64');
Enter fullscreen mode Exit fullscreen mode

OAuth 2.0 (2012):

  • Simplified, uses bearer tokens
  • Multiple flow types for different app types
  • Easier to implement
  • Works with mobile and single-page apps
  • Relies on HTTPS for security
// OAuth 2.0 - Simple bearer token
headers: {
  Authorization: `Bearer ${access_token}`
}
Enter fullscreen mode Exit fullscreen mode

OpenID Connect (OIDC) - Authentication Layer

The Problem:
OAuth 2.0 is for authorization (what you can do), not authentication (who you are).

Example:

OAuth 2.0: "This token can access your Google Drive"
But: Who owns this token? What's their identity?
Enter fullscreen mode Exit fullscreen mode

Solution: OpenID Connect

OIDC extends OAuth 2.0 by adding an ID Token - a JWT that contains user identity information.

OIDC ID Token

Structure:

{
  "iss": "https://accounts.google.com",  // Who issued it
  "sub": "110169484474386276334",        // User's unique ID
  "aud": "YOUR_CLIENT_ID",               // Who it's for
  "exp": 1732191600,                     // Expiration
  "iat": 1732188000,                     // Issued at
  "email": "user@example.com",           // User's email
  "email_verified": true,
  "name": "John Doe",
  "picture": "https://...",
  "given_name": "John",
  "family_name": "Doe",
  "locale": "en"
}
Enter fullscreen mode Exit fullscreen mode

OIDC Flow

// Step 1: Request with openid scope
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://yourapp.com/callback&
  response_type=code&
  scope=openid profile email&  // Note: "openid" scope
  state=random_string`;

// Step 2: Exchange code for tokens
const response = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  body: JSON.stringify({
    code: authorizationCode,
    client_id: YOUR_CLIENT_ID,
    client_secret: YOUR_CLIENT_SECRET,
    redirect_uri: 'https://yourapp.com/callback',
    grant_type: 'authorization_code'
  })
});

const tokens = await response.json();
// tokens = {
//   access_token: "ya29.a0...",      // For accessing resources
//   refresh_token: "1//0g...",       // For getting new access tokens
//   id_token: "eyJhbGciOiJSUz...",   // JWT with user identity
//   expires_in: 3600
// }

// Step 3: Verify and decode ID token
const jwt = require('jsonwebtoken');
const decoded = jwt.decode(tokens.id_token);

console.log(decoded);
// {
//   email: "user@example.com",
//   name: "John Doe",
//   sub: "110169484474386276334",
//   ...
// }

// Step 4: Create your own session/JWT
const yourAppToken = jwt.sign(
  { userId: decoded.sub, email: decoded.email },
  YOUR_SECRET,
  { expiresIn: '7d' }
);

// Send to client
res.json({ token: yourAppToken, user: decoded });
Enter fullscreen mode Exit fullscreen mode

"Login with Google/Facebook/GitHub" Explained

This uses OIDC (OpenID Connect):

How "Login with Google" Works:

1. User clicks "Login with Google"
   ↓
2. Redirect to Google with openid scope
   ↓
3. User logs into Google (if not already)
   ↓
4. User grants permission
   ↓
5. Google redirects back with authorization code
   ↓
6. Your server exchanges code for:
   - ID Token (JWT with user info)
   - Access Token (to access Google APIs)
   ↓
7. Your server verifies ID Token signature
   ↓
8. Extract user info (email, name, picture)
   ↓
9. Create/find user in your database
   ↓
10. Create your own session/JWT
   ↓
11. User is logged into YOUR app
Enter fullscreen mode Exit fullscreen mode

Code Example - Complete "Login with Google":

const express = require('express');
const axios = require('axios');
const jwt = require('jsonwebtoken');

const app = express();

const GOOGLE_CLIENT_ID = 'your-client-id';
const GOOGLE_CLIENT_SECRET = 'your-client-secret';
const REDIRECT_URI = 'http://localhost:3000/auth/google/callback';

// Step 1: Initiate OAuth flow
app.get('/auth/google', (req, res) => {
  const googleAuthUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
  const scope = 'openid profile email';
  const state = generateRandomString(); // CSRF protection

  const url = `${googleAuthUrl}?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code&scope=${scope}&state=${state}`;

  // Store state in session to verify later
  req.session.oauthState = state;

  res.redirect(url);
});

// Step 2: Handle OAuth callback
app.get('/auth/google/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state (CSRF protection)
  if (state !== req.session.oauthState) {
    return res.status(403).send('Invalid state parameter');
  }

  try {
    // Exchange code for tokens
    const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', {
      code,
      client_id: GOOGLE_CLIENT_ID,
      client_secret: GOOGLE_CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
      grant_type: 'authorization_code'
    });

    const { access_token, id_token } = tokenResponse.data;

    // Decode ID token (contains user info)
    const userInfo = jwt.decode(id_token);
    // userInfo = { sub, email, name, picture, email_verified }

    // Find or create user in your database
    let user = await db.users.findOne({ googleId: userInfo.sub });

    if (!user) {
      user = await db.users.create({
        googleId: userInfo.sub,
        email: userInfo.email,
        name: userInfo.name,
        picture: userInfo.picture
      });
    }

    // Create YOUR app's JWT
    const appToken = jwt.sign(
      { userId: user.id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    // Send token to client
    res.json({ token: appToken, user });

  } catch (error) {
    console.error('OAuth error:', error);
    res.status(500).send('Authentication failed');
  }
});

function generateRandomString() {
  return Math.random().toString(36).substring(2, 15);
}
Enter fullscreen mode Exit fullscreen mode

Frontend (React):

function LoginPage() {
  const handleGoogleLogin = () => {
    // Redirect to your backend OAuth endpoint
    window.location.href = 'http://localhost:3000/auth/google';
  };

  return (
    <div>
      <h1>Login</h1>
      <button onClick={handleGoogleLogin}>
        <img src="google-icon.png" alt="Google" />
        Login with Google
      </button>
    </div>
  );
}

// Handle callback (in a different component)
function AuthCallback() {
  useEffect(() => {
    const urlParams = new URLSearchParams(window.location.search);
    const code = urlParams.get('code');

    if (code) {
      // Your backend will handle the token exchange
      // and redirect to dashboard
    }
  }, []);

  return <div>Logging you in...</div>;
}
Enter fullscreen mode Exit fullscreen mode

Is "Login with Google/GitHub" Secure?

✅ Yes, very secure when implemented correctly:

Security Benefits:

  1. No password sharing - You never give your Google password to third-party apps
  2. Limited permissions - Apps only get what you approve (scopes)
  3. Revokable - You can revoke access anytime from Google settings
  4. Strong authentication - Leverages Google's security (2FA, etc.)
  5. Regular security updates - Google maintains the auth infrastructure

What to Verify:

  • Always check the permissions (scopes) being requested
  • Use only trusted apps
  • Review connected apps regularly in your Google account

Should You Use It?
Yes - It's generally more secure than having users create weak passwords


7. When to Use What {#best-practices}

Decision Matrix

Scenario Recommended Approach Why
Traditional web app Sessions Easy to invalidate, secure with httpOnly cookies
Mobile app JWT with refresh tokens Stateless, works offline, scalable
Microservices JWT No shared session storage needed
Internal API API Keys Simple, easy to track usage
Third-party integration OAuth 2.0 Secure delegation without password sharing
Social login OIDC (OpenID Connect) Get user identity from trusted providers
SPA (Single Page App) JWT with httpOnly cookies Prevents XSS, still stateless
Banking/Financial app Sessions + MFA Maximum security, immediate revocation

Security Best Practices

1. Password Security:

const bcrypt = require('bcrypt');

// ALWAYS hash passwords
async function hashPassword(password) {
  const saltRounds = 10;
  return await bcrypt.hash(password, saltRounds);
}

// NEVER store plain text
// ❌ BAD
user.password = req.body.password;

// ✅ GOOD
user.passwordHash = await hashPassword(req.body.password);
Enter fullscreen mode Exit fullscreen mode

2. Secure JWT Storage:

// ❌ BAD - Vulnerable to XSS
localStorage.setItem('token', jwt);

// ✅ BETTER - Use httpOnly cookies
res.cookie('token', jwt, {
  httpOnly: true,   // Cannot be accessed by JavaScript
  secure: true,     // Only sent over HTTPS
  sameSite: 'strict' // CSRF protection
});
Enter fullscreen mode Exit fullscreen mode

3. Token Expiration:

// ✅ GOOD - Short-lived access tokens
const accessToken = jwt.sign(payload, secret, { expiresIn: '15m' });

// ✅ GOOD - Long-lived refresh tokens
const refreshToken = jwt.sign(payload, secret, { expiresIn: '7d' });
Enter fullscreen mode Exit fullscreen mode

4. HTTPS Only:

❌ http://yoursite.com  - Tokens can be intercepted
✅ https://yoursite.com - Encrypted, secure
Enter fullscreen mode Exit fullscreen mode

5. Input Validation:

// ✅ ALWAYS validate and sanitize input
const { body, validationResult } = require('express-validator');

app.post('/login',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process login...
  }
);
Enter fullscreen mode Exit fullscreen mode

Understanding Security Terms

Password Breach:
When hackers gain access to a database containing user passwords, usually through:

  • SQL injection attacks
  • Database misconfiguration
  • Insider threats
  • Server vulnerabilities

Example:

2019: 773 million email addresses and 21 million passwords leaked
Sites affected: LinkedIn, Adobe, Yahoo, etc.

Why it's dangerous:
- Many users reuse passwords across sites
- If one site is breached, all accounts with same password are at risk
Enter fullscreen mode Exit fullscreen mode

Protection:

// 1. Hash passwords (so breach exposes hashes, not passwords)
const hashedPassword = await bcrypt.hash(password, 10);

// 2. Add salt (makes rainbow table attacks useless)
// bcrypt does this automatically

// 3. Monitor for breaches
// Use services like "Have I Been Pwned" API
const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`);
Enter fullscreen mode Exit fullscreen mode

Modern Auth Recommendations

For New Projects in 2025:

1. Consumer Apps (B2C):

✅ Use: OAuth/OIDC (Login with Google/GitHub)
✅ Add: JWT with refresh tokens
✅ Store: httpOnly cookies for web, secure storage for mobile
✅ Extra: Magic links, passwordless authentication
Enter fullscreen mode Exit fullscreen mode

2. Enterprise Apps (B2B):

✅ Use: SAML or OIDC
✅ Add: Single Sign-On (SSO)
✅ Consider: Auth0, Okta, Azure AD
Enter fullscreen mode Exit fullscreen mode

3. Internal Tools:

✅ Use: Sessions or JWT
✅ Add: VPN or IP restrictions
✅ Consider: API keys for scripts
Enter fullscreen mode Exit fullscreen mode

4. Public APIs:

✅ Use: API keys + OAuth 2.0
✅ Add: Rate limiting
✅ Monitor: Usage and abuse
Enter fullscreen mode Exit fullscreen mode

Summary Cheat Sheet

Quick Comparison

┌─────────────────────────────────────────────────────────────┐
│                    AUTHENTICATION METHODS                    │
├────────────┬──────────┬──────────┬──────────┬──────────────┤
│   Method   │  State   │ Storage  │  Speed   │   Use Case   │
├────────────┼──────────┼──────────┼──────────┼──────────────┤
│  Sessions  │ Stateful │  Server  │  Medium  │  Web apps    │
│    JWT     │Stateless │  Client  │   Fast   │ APIs/Mobile  │
│  API Keys  │ Stateful │  Server  │  Medium  │  B2B/Bots    │
│  OAuth 2.0 │ Stateless│  Client  │   Fast   │ Delegation   │
└────────────┴──────────┴──────────┴──────────┴──────────────┘
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Sessions = Server remembers you (like a locker with your stuff)
  2. JWT = You carry your ID card (self-contained proof)
  3. Cookies = Automatic note attached to every request
  4. OAuth = Valet key (limited access, no password sharing)
  5. OIDC = OAuth + your identity card

Remember

  • Always use HTTPS in production
  • Hash passwords with bcrypt
  • Use httpOnly cookies for tokens when possible
  • Implement refresh tokens for better security
  • Never store secrets in frontend code
  • Validate all inputs to prevent injection attacks
  • Monitor and log authentication attempts
  • Use proven libraries - don't roll your own crypto

Additional Resources

Libraries & Tools:

  • jsonwebtoken - JWT for Node.js
  • bcrypt - Password hashing
  • Passport.js - Authentication middleware
  • express-session - Session management
  • Auth0 - Complete auth platform
  • OAuth.net - OAuth specifications

Top comments (0)