Table of Contents
- Understanding HTTP Statelessness
- Sessions - The Traditional Approach
- JWT - Modern Stateless Authentication
- Cookies Explained
- Authentication Types
- OAuth 2.0 & OpenID Connect
- 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?" ❌
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
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' });
});
});
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"
}
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
2. Replication Issues:
User in USA → Server A (has session)
User in Europe → Server B (no session) ❌
Need to sync sessions across regions → Latency
3. Consistency Challenges:
User updates profile on Server A
Server B still has old session data
Race conditions and stale data
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]
Example JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJ1c2VybmFtZSI6ImpvaG5fZG9lIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzMyMTg4MDAwLCJleHAiOjE3MzIxOTE2MDB9.4p8JkB8k3Yn8G7X9xF5qZ2eR4wT6vY3nM1oL7pQ2sA4
Decoded:
1. Header (Algorithm & Token Type):
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload (Claims/User Data):
{
"userId": 42,
"username": "john_doe",
"role": "admin",
"iat": 1732188000, // Issued at
"exp": 1732191600 // Expires at (1 hour later)
}
3. Signature (Verification):
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
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
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
});
});
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();
}
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();
});
});
}
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 });
});
});
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>;
}
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
});
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
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' });
});
2. Browser Stores Cookie:
Browser Storage:
userId=42; expires=Fri, 21-Nov-2025 11:00:00 GMT; path=/; HttpOnly; Secure
3. Browser Sends Cookie Automatically:
// Every request to same domain automatically includes cookies
fetch('/api/profile')
// Browser automatically adds: Cookie: userId=42
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);
});
Important Cookie Rules
Security Rules:
- httpOnly - JavaScript cannot access the cookie (prevents XSS attacks)
- secure - Cookie only sent over HTTPS
-
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.comcannot read cookies fromgoogle.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 │ │
│<─────────────────────────────┤ │
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 │
│<───────────────────────────────────┤
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
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 });
});
Client-side:
// Making API request with API key
fetch('https://api.example.com/data', {
headers: {
'X-API-Key': 'sk_live_51HabcD123...'
}
});
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*
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! │ │
│<────────────────────────┤ │
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');
});
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`;
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();
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
})
});
OAuth 2.0 Tokens
Access Token:
Purpose: Short-lived token to access resources
Lifetime: 15 minutes - 1 hour
Example: "ya29.a0AfH6SMBx..."
Refresh Token:
Purpose: Long-lived token to get new access tokens
Lifetime: Days to months
Example: "1//0gKz8..."
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;
}
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
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]
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');
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}`
}
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?
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"
}
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 });
"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
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);
}
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>;
}
Is "Login with Google/GitHub" Secure?
✅ Yes, very secure when implemented correctly:
Security Benefits:
- No password sharing - You never give your Google password to third-party apps
- Limited permissions - Apps only get what you approve (scopes)
- Revokable - You can revoke access anytime from Google settings
- Strong authentication - Leverages Google's security (2FA, etc.)
- 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);
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
});
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' });
4. HTTPS Only:
❌ http://yoursite.com - Tokens can be intercepted
✅ https://yoursite.com - Encrypted, secure
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...
}
);
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
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}`);
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
2. Enterprise Apps (B2B):
✅ Use: SAML or OIDC
✅ Add: Single Sign-On (SSO)
✅ Consider: Auth0, Okta, Azure AD
3. Internal Tools:
✅ Use: Sessions or JWT
✅ Add: VPN or IP restrictions
✅ Consider: API keys for scripts
4. Public APIs:
✅ Use: API keys + OAuth 2.0
✅ Add: Rate limiting
✅ Monitor: Usage and abuse
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 │
└────────────┴──────────┴──────────┴──────────┴──────────────┘
Key Takeaways
- Sessions = Server remembers you (like a locker with your stuff)
- JWT = You carry your ID card (self-contained proof)
- Cookies = Automatic note attached to every request
- OAuth = Valet key (limited access, no password sharing)
- 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)