Module 6

Authentication & Security

Sessions โ€ข JWT โ€ข OAuth 2.0 โ€ข Vulnerabilities

๐Ÿ” Securing Your Application

Every real application needs authentication (who are you?) and authorization (what can you do?). This module covers the three main approaches and the vulnerabilities you need to protect against.

๐ŸŽฏ By the end of this module, you will:

  • Understand why HTTP is stateless and how to add state
  • Implement session-based authentication with cookies
  • Create and verify JSON Web Tokens (JWT)
  • Understand the OAuth 2.0 flow (Google, GitHub login)
  • Protect against XSS, CSRF, SQL injection, and more
  • Compare Sessions vs JWT vs OAuth โ€” when to use what
  • Run hands-on security attack labs (XSS, SQLi, CSRF, IDOR, Brute Force)

In Module 5, you built REST APIs with Express โ€” routes, middleware, JSON responses. But those APIs are wide open: anyone can call any endpoint. In the real world, you need to identify users (authentication) and control access (authorization). That's exactly what this module covers โ€” from cookies to tokens to OAuth, plus the vulnerabilities you must defend against.

๐Ÿ”‘ Authentication vs Authorization โ€” What's the Difference?

These two concepts sound similar but serve very different purposes. You need both, and they always happen in this order:

  • Authentication (AuthN) โ€” "Who are you?" โ†’ Verifying identity (login, password, biometrics, OAuth token)
  • Authorization (AuthZ) โ€” "What can you do?" โ†’ Checking permissions (admin vs regular user, read vs write access)

Authentication always comes first โ€” you can't check what someone is allowed to do until you know who they are. Think of a building: authentication is showing your ID badge at the door, authorization is which floors your badge unlocks.

โ“ The Stateless Problem

HTTP is stateless โ€” each request is independent. The server doesn't remember who you are between requests. So how does a website know you're logged in?

โš ๏ธ HTTP has no memory! Every single request starts from scratch. The server has no idea if you just logged in, added items to your cart, or completed a form. Each request is like meeting a stranger for the first time.

๐Ÿ’ก What "stateless" really means:

  • No memory โ€” Server doesn't remember previous requests
  • No context โ€” Each request is completely independent
  • No persistence โ€” Login status, cart items, preferences โ€” all lost between requests

The Problem

๐Ÿ“จ
Request 1

POST /login with username + password โ†’ Server validates credentials, responds "Welcome!" โ€” but immediately forgets who you are. The response is sent and the connection is closed. HTTP has no built-in way to link this request to the next one.

โ†“ new connection
๐Ÿคท
Request 2

GET /dashboard โ†’ "Who are you?" The server has zero memory of the previous login. Without some form of proof attached to this request, the server treats you as a complete stranger โ€” even though you logged in 1 second ago.

โ†“ the fix
โœ…
Solution

Send a token or cookie with every request to prove your identity. The server issues this proof after a successful login, and the client includes it in every subsequent request. This is the foundation of Sessions, JWT, and OAuth.

๐Ÿ’ก How do we solve this? We need a way for the server to recognize returning users. The three main solutions are: Sessions (server remembers), JWT (client carries proof), and OAuth (delegate to a trusted provider).

๐Ÿ—บ๏ธ Authentication Overview

There are three main approaches to authentication. Each has different trade-offs:

๐Ÿช Sessions (Server-side)

Server stores state, sends a session ID cookie to the client. Like a hotel giving you a room key โ€” they track which room is yours.

๐ŸŽŸ๏ธ JWT (Client-side)

Server issues a signed token, client sends it in every request. Like a concert wristband โ€” it proves you paid, no need to check a list.

๐Ÿ”‘ OAuth 2.0 (Delegated)

"Login with Google" โ€” a trusted third party handles authentication. Like using your passport at a hotel instead of creating a new ID.

๐Ÿค” When to use what?

๐Ÿช Use Sessions whenโ€ฆ

Traditional web apps (server-rendered pages), you need easy logout/invalidation, you're building a monolithic app with a single server.

โœ… Pros: Simple, easy to revoke, server controls everything

โŒ Cons: Server memory/storage, harder to scale across multiple servers, not ideal for mobile apps

๐ŸŽŸ๏ธ Use JWT whenโ€ฆ

SPAs (React, Vue, Angular), mobile apps, microservices that need to communicate, you need stateless auth that scales.

โœ… Pros: Stateless, scales easily, works everywhere (web, mobile, IoT)

โŒ Cons: Can't easily revoke tokens, larger request size, complex refresh logic

๐Ÿ”‘ Use OAuth whenโ€ฆ

You want "Login with Google/GitHub/Facebook", you need access to user's data on other platforms, you want to avoid managing passwords entirely.

โœ… Pros: No password management, trusted identity, access to provider APIs

โŒ Cons: Depends on third party, more complex setup, provider can change their API

๐Ÿ’ก Pro Tip: In real apps, these are often combined! A common pattern: use OAuth for login โ†’ create a Session or JWT for your app. You'll see this pattern in the comparison section.

๐Ÿช Sessions & Cookies

Now that you understand the stateless problem, let's explore the first solution: sessions. The idea is beautifully simple โ€” after a successful login, the server creates a "session" (a record of who you are) and gives the browser a tiny ID card (a cookie) to present on future visits. It's like checking into a hotel: the front desk creates a record for you and gives you a room key. Every time you come back, you show the key, and they know exactly who you are.

What is a Cookie?

A cookie is a small piece of data (max ~4KB) that the server sends to the browser. The browser automatically includes it in every subsequent request to that server. Think of it as a name badge the server gives you.

Session Flow

๐Ÿ”
Login

Client sends credentials (email + password) via POST /login. The server verifies them against the database โ€” if they match, authentication succeeds and the server prepares to create a session.

โ†“
๐Ÿ—ƒ๏ธ
Create Session

Server generates a unique session ID, stores session data (userId, role, timestamps) in server-side storage โ€” memory, Redis, or a database. This is the key difference from JWT: the server holds the state.

โ†“
๐Ÿช
Set Cookie

Server sends Set-Cookie: sessionId=abc123 in the response header. The browser stores this cookie automatically โ€” no JavaScript code needed on the client side.

โ†“
๐Ÿ”„
Auto-Send

On every subsequent request, the browser automatically includes the cookie. The server looks up the session ID, retrieves the stored data, and knows who the user is โ€” seamless authentication without manual header management.

๐Ÿ“Š Example: Visit Counter with Cookies

Let's see cookies in action. This simple server counts how many times you visit:

JavaScript
// visit-counter.js โ€” Manual cookie handling
const http = require('http');

const server = http.createServer((req, res) => {
  // Parse cookies from the request header
  const cookies = {};
  const cookieHeader = req.headers.cookie;
  if (cookieHeader) {
    cookieHeader.split(';').forEach(cookie => {
      const [name, value] = cookie.trim().split('=');
      cookies[name] = value;
    });
  }

  // Increment visit count
  const visits = parseInt(cookies.visits || '0') + 1;
  res.setHeader('Set-Cookie', `visits=${visits}; Max-Age=86400`);
  res.end(`You have visited ${visits} time(s)`);
});

server.listen(3000);

Now the same thing with the cookie-parser middleware (much cleaner):

JavaScript
// visit-counter-express.js โ€” Using cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();

app.use(cookieParser());

app.get('/', (req, res) => {
  const visits = parseInt(req.cookies.visits || '0') + 1;
  res.cookie('visits', visits, { maxAge: 86400000 });
  res.send(`You have visited ${visits} time(s)`);
});

app.listen(3000);

๐Ÿ”’ Cookie Security Attributes

Cookies have important security settings you should always configure:

โš ๏ธ Cookie Hijacking Warning: If someone steals your session cookie, they can impersonate you! Open DevTools โ†’ Application โ†’ Cookies to see your cookies. This is why HttpOnly + Secure + SameSite are so important.

โœ… Common Session Use Cases

  • ๐Ÿ›’ Shopping carts (remember items across pages)
  • ๐Ÿ” Login status (stay logged in)
  • ๐ŸŒ Language/theme preferences
  • ๐Ÿ“Š Analytics & tracking (visit count, etc.)
  • ๐Ÿ“ Form data (remember multi-step form progress)
  • ๐ŸŽฎ Game state (save progress)

๐Ÿ” Session Authentication Implementation

JavaScript
// session-auth.js โ€” Complete session authentication
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const app = express();

app.use(express.json());
app.use(session({
  secret: process.env.SESSION_SECRET || 'change-this-secret',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    sameSite: 'strict' // CSRF protection
  }
}));

// Fake user database (โš ๏ธ SHA-256 is a DEMO shortcut โ€” use bcrypt in real apps!)
const users = [
  { id: 1, email: 'alice@example.com',
    passwordHash: crypto.createHash('sha256').update('password123').digest('hex') }
];

// Login
app.post('/login', (req, res) => {
  const { email, password } = req.body;
  const hash = crypto.createHash('sha256').update(password).digest('hex');
  const user = users.find(u => u.email === email && u.passwordHash === hash);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });
  req.session.userId = user.id;
  res.json({ message: 'Logged in!' });
});

// Auth middleware
function requireAuth(req, res, next) {
  if (!req.session.userId) return res.status(401).json({ error: 'Not authenticated' });
  next();
}

// Protected route
app.get('/profile', requireAuth, (req, res) => {
  const user = users.find(u => u.id === req.session.userId);
  res.json({ user: { id: user.id, email: user.email } });
});

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

app.listen(3000, () => console.log('Server: http://localhost:3000'));

๐Ÿ’พ Where to Store Sessions in Production? The express-session middleware stores sessions in memory by default โ€” fine for development, but terrible for production (sessions vanish on server restart, and memory grows with each user). In production, use a dedicated session store: Redis (fastest, most popular โ€” connect-redis), MongoDB (connect-mongo), or PostgreSQL (connect-pg-simple). Redis is the industry standard because it's blazing fast and handles expiration automatically.

๐Ÿ”’ Session Fixation Prevention: After a successful login, always regenerate the session ID with req.session.regenerate(). Without this, an attacker could set a known session ID before the user logs in, then hijack the session after authentication. Our simplified example above skips this for clarity, but in production it's essential โ€” the old session is destroyed and a new ID is issued.

๐ŸŽŸ๏ธ JSON Web Tokens (JWT)

A JWT is a self-contained token that carries user data. The server doesn't need to store anything โ€” it just verifies the token's signature.

๐Ÿ›‚ Passport Analogy: A JWT is like a passport. Your passport contains your info (name, nationality, photo) and has a government stamp (signature). Any border agent can verify it without calling your country's database โ€” they just check the stamp is authentic.

While sessions store user data on the server (the server remembers you), JWT takes the opposite approach: it puts the user data inside the token itself and sends it to the client. The server doesn't remember anything โ€” it just checks that the token's signature is valid. This is a fundamental architectural difference: with sessions, the server is the "source of truth"; with JWT, the token is. That's why JWT is called stateless โ€” the server holds no state about logged-in users.

๐Ÿ“ฆ JWT Structure: Three Parts

๐Ÿ“‹
Header {"alg":"HS256","typ":"JWT"}

Metadata about the token: which signing algorithm is used (HS256, RS256, ES256) and the token type (always "JWT"). This part is Base64Url-encoded and becomes the first segment of the token string. The verifier reads this to know how to check the signature.

. dot separator
๐Ÿ“ฆ
Payload {"userId":1,"role":"admin","exp":...}

Your actual data (called "claims") โ€” user info, permissions, and expiration. โš ๏ธ Anyone can decode and read this โ€” it's just Base64, not encrypted! Never store passwords, credit cards, or secrets here. Keep it small since it's sent with every request.

. dot separator
๐Ÿ”
Signature HMACSHA256(header + payload, secret)

The cryptographic proof that the token is authentic. Created by hashing the header + payload with a secret key. When the server receives a JWT, it recalculates this signature โ€” if it doesn't match, the token was tampered with and is rejected.

๐Ÿ“ What a real JWT looks like: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.dBjftJeZ...signature โ€” Three Base64-encoded strings separated by dots. That's it! The first two parts are not encrypted โ€” anyone can paste them into jwt.io and read the contents. Try it with any JWT and you'll instantly see the header and payload decoded. This is why you never store secrets in a JWT.

๐Ÿ“‹ Registered Claims

sub Subject (user ID)
exp Expiration time
iat Issued at
nbf Not before
iss Issuer
aud Audience
jti JWT ID (unique)

๐Ÿ” Signature & Cryptography

HS256 (HMAC)

Symmetric โ€” same secret for signing and verifying. Simple, good for single-server setups.

RS256 (RSA)

Asymmetric โ€” private key signs, public key verifies. Better for microservices (only the auth server needs the private key).

ES256 (ECDSA)

Asymmetric like RSA but with smaller keys and faster. Modern choice for performance.

โš ๏ธ Signing โ‰  Encryption! JWTs are signed, not encrypted. Anyone can read the payload (it's just Base64). The signature only proves the token wasn't tampered with. Never put passwords, credit cards, or secrets in a JWT!

โœ… What to Store in JWT

  • User ID
  • Role / permissions
  • Email (if needed for display)
  • Expiration time

โŒ What NOT to Store

  • Passwords
  • Credit card numbers
  • Personal data (SSN, address)
  • API keys or secrets

๐Ÿšซ 5 Common JWT Pitfalls

1
Storing in localStorage

Vulnerable to XSS attacks. Use HttpOnly cookies instead.

2
No expiration

Always set expiresIn. Tokens without expiration live forever if leaked.

3
Weak secret

Use a long, random secret (โ‰ฅ256 bits). Never use 'secret123'.

4
Not verifying algorithm

Always specify algorithms in verify: jwt.verify(token, secret, {algorithms: ['HS256']})

5
Too much data

JWTs are sent with every request. Keep payload small!

๐Ÿ”„ Why Refresh Tokens? If access tokens are short-lived (15 minutes), users would need to log in again every 15 minutes โ€” terrible UX! Refresh tokens solve this elegantly: they're long-lived (7 days) and stored securely (HttpOnly cookie or secure storage). When the access token expires, the client silently exchanges the refresh token for a new access token โ€” the user never notices. It's the best of both worlds: short attack windows if an access token leaks, but no constant re-login. The code below implements this dual-token pattern.

๐Ÿ› ๏ธ Step-by-Step: Build JWT Authentication

Step 1: Install dependencies

Bash
npm init -y
npm install express jsonwebtoken bcrypt dotenv

Step 2: Complete JWT Server (Access + Refresh Tokens)

JavaScript
// jwt-auth.js โ€” Complete JWT auth with refresh tokens
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
require('dotenv').config();

const app = express();
app.use(express.json());

const ACCESS_SECRET = process.env.ACCESS_SECRET || 'access-secret-change-me';
const REFRESH_SECRET = process.env.REFRESH_SECRET || 'refresh-secret-change-me';

// In-memory storage (use DB in production!)
const users = [];
let refreshTokens = [];

// Register
app.post('/register', async (req, res) => {
  const hash = await bcrypt.hash(req.body.password, 12);
  users.push({ id: users.length + 1, email: req.body.email, passwordHash: hash });
  res.status(201).json({ message: 'User registered!' });
});

// Login โ†’ issue access + refresh tokens
app.post('/login', async (req, res) => {
  const user = users.find(u => u.email === req.body.email);
  if (!user || !await bcrypt.compare(req.body.password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  const accessToken = jwt.sign({ userId: user.id }, ACCESS_SECRET, { expiresIn: '15m' });
  const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: '7d' });
  refreshTokens.push(refreshToken);
  res.json({ accessToken, refreshToken });
});

// Verify token middleware
function verifyToken(req, res, next) {
  const auth = req.headers.authorization;
  if (!auth?.startsWith('Bearer ')) return res.status(401).json({ error: 'No token' });
  try {
    req.user = jwt.verify(auth.split(' ')[1], ACCESS_SECRET);
    next();
  } catch {
    res.status(403).json({ error: 'Invalid or expired token' });
  }
}

// Refresh token endpoint
app.post('/token/refresh', (req, res) => {
  const { refreshToken } = req.body;
  if (!refreshTokens.includes(refreshToken)) return res.status(403).json({ error: 'Invalid refresh token' });
  try {
    const payload = jwt.verify(refreshToken, REFRESH_SECRET);
    const newAccessToken = jwt.sign({ userId: payload.userId }, ACCESS_SECRET, { expiresIn: '15m' });
    res.json({ accessToken: newAccessToken });
  } catch {
    res.status(403).json({ error: 'Expired refresh token' });
  }
});

// Protected route
app.get('/api/profile', verifyToken, (req, res) => {
  res.json({ userId: req.user.userId });
});

// Logout โ€” remove refresh token from whitelist
app.post('/logout', (req, res) => {
  refreshTokens = refreshTokens.filter(t => t !== req.body.refreshToken);
  res.json({ message: 'Logged out' });
});

app.listen(3000, () => console.log('JWT server: http://localhost:3000'));

๐Ÿ›ก๏ธ JWT Security Best Practices

1
Short-lived access tokens (15m)

If stolen, the window of attack is small.

2
Secure token storage

Best: HttpOnly cookies. OK: in-memory variable. Worst: localStorage (XSS vulnerable!).

3
Token rotation & revocation

Implement a token blacklist or rotation strategy to invalidate tokens on logout.

4
Always verify algorithm

Specify algorithms: ['HS256'] in verify to prevent algorithm switching attacks.

5
Use HTTPS only

Tokens sent over HTTP can be intercepted. Always use HTTPS in production.

๐Ÿ”‘ OAuth 2.0

OAuth 2.0 lets users log in with Google, GitHub, etc. instead of creating a new account. The third-party provider handles authentication โ€” you get a token with user info.

Think of OAuth like a valet parking service. You (the user) want to give the valet (the app) limited access to your car (your data). Instead of handing over your master key, you give them a valet key that only opens the door and starts the engine โ€” it can't open the trunk or glove box. OAuth works the same way: Google gives your app a limited-access token, not your actual password. The app can read your name and email, but can't change your password or delete your account.

โš ๏ธ OAuth does NOT replace Sessions or JWT! This is the most common misconception. OAuth only handles the initial authentication โ€” proving who the user is via Google/GitHub. Once the user is verified, OAuth's job is done. Your app still needs Sessions or JWT to maintain the logged-in state across requests. Think of it this way: OAuth answers "Who is this person?" (one-time verification), while Sessions/JWT answer "Is this person still logged in?" (every single request). In practice, you always use OAuth together with Sessions or JWT โ€” never OAuth alone.

๐Ÿ” No Password Management

You never see or store user passwords. Less risk, less responsibility.

โšก Better User Experience

One-click login. No forms, no "forgot password" flows.

โœ… Verified Identity

Google/GitHub already verified the user's email and identity.

๐Ÿ“‰ Reduced Liability

If there's a data breach, you don't have password hashes to leak.

๐Ÿ“– Key Terminology

Resource Owner The user who owns the data
Client Your app that wants access
Authorization Server Google/GitHub login page
Resource Server Google/GitHub API
Authorization Code Temporary code exchanged for tokens
Access Token Key to access user data
Scope What data you want (email, profile, etc.)
Redirect URI Where Google sends the user back

๐Ÿ” OAuth vs OpenID Connect (OIDC): OAuth is for authorization (access to resources). OIDC is a layer on top that adds authentication (identity). When you do "Login with Google", you're actually using OIDC, which gives you an ID token with the user's identity.

OAuth 2.0 Authorization Code Flow

๐Ÿ”€
Redirect

User clicks "Login with Google" โ†’ your app redirects them to Google's authorization server with your Client ID, requested scopes (email, profile), and a redirect URI. The user leaves your site entirely.

โ†“ user on Google
โœ‹
Consent

Google shows a consent screen: "MyApp wants to access your email and profile. Allow?" The user approves or denies. Google handles all the login UI โ€” you never see the user's password.

โ†“ redirect back
๐Ÿ“
Code

Google redirects the user back to your redirect URI with a short-lived authorization code in the URL: /callback?code=abc123. This code is useless on its own โ€” it must be exchanged server-side.

โ†“ server-to-server
๐Ÿ”„
Token Exchange

Your server sends the authorization code + Client Secret to Google's token endpoint (server-to-server, never exposed to the browser). Google validates and returns an access token and optionally a refresh token.

โ†“ use the token
๐Ÿ‘ค
Profile

Your server uses the access token to call Google's user-info API and retrieves the user's name, email, and avatar. You then create or find a user in your database and establish your own session or JWT.

๐Ÿค” Why the "code โ†’ token" exchange? Why doesn't Google just return the access token directly in the redirect URL? Because URLs are visible everywhere โ€” they appear in browser history, server logs, and the Referer header. By returning a short-lived, single-use authorization code instead, and requiring the server to exchange it using the Client Secret (which never leaves the server), the actual access token is never exposed to the browser. This is the Authorization Code flow's key security advantage over the older (deprecated) Implicit flow.

๐Ÿ› ๏ธ Step-by-Step: OAuth with Passport.js

Step 1: Set up Google Cloud Console

2

Create a new project

3

APIs & Services โ†’ Credentials โ†’ OAuth 2.0 Client ID

4

Set redirect URI to: http://localhost:3000/auth/google/callback

5

Copy your Client ID and Client Secret

Step 2: Install dependencies

Bash
npm install express passport passport-google-oauth20 express-session dotenv

Step 3: Create .env file

Env
GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here
SESSION_SECRET=a-very-long-random-string

โš ๏ธ Never commit .env to Git! Add .env to your .gitignore file immediately. Pushing secrets (API keys, Client Secrets) to GitHub is the #1 beginner mistake โ€” and bots scan GitHub for leaked credentials 24/7. Run: echo .env >> .gitignore

Step 4: Complete OAuth Server

JavaScript
// oauth-google.js โ€” Complete Google OAuth with Passport.js
const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const session = require('express-session');
require('dotenv').config();

const app = express();

// Session setup (Passport needs sessions)
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false, saveUninitialized: false
}));

app.use(passport.initialize());
app.use(passport.session());

// User storage (use a real DB in production!)
const users = [];

// Configure Google Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/auth/google/callback'
  },
  (accessToken, refreshToken, profile, done) => {
    // Find or create user
    let user = users.find(u => u.googleId === profile.id);
    if (!user) {
      user = {
        id: users.length + 1,
        googleId: profile.id,
        name: profile.displayName,
        email: profile.emails[0].value
      };
      users.push(user);
    }
    done(null, user);
  }
));

// Serialize/deserialize user for sessions
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => {
  const user = users.find(u => u.id === id);
  done(null, user);
});

// Routes
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/' }),
  (req, res) => res.redirect('/dashboard')
);

app.get('/dashboard', (req, res) => {
  if (!req.user) return res.redirect('/');
  res.send(`Welcome ${req.user.name}! Email: ${req.user.email}`);
});

app.get('/', (req, res) => {
  res.send('<a href="/auth/google">Login with Google</a>');
});

app.listen(3000, () => console.log('OAuth server: http://localhost:3000'));

๐Ÿ›ก๏ธ OAuth Security Best Practices

1

Use Authorization Code flow โ€” never Implicit flow (tokens in URL = dangerous)

2

Keep Client Secret server-side โ€” never expose it in frontend code

3

Use state parameter โ€” prevents CSRF attacks on the OAuth flow

4

Request minimal scopes โ€” only ask for what you actually need

5

Validate redirect URIs โ€” restrict to known URLs in provider console

โš ๏ธ When NOT to use OAuth

  • Internal company tools where all users have company accounts
  • Apps where you need full control over user identity
  • Systems where third-party dependency is a risk
  • Privacy-sensitive applications where users may not want social login
  • When you need offline access without OAuth token refresh complexity

โš–๏ธ Sessions vs JWT vs OAuth

You've now learned three different approaches to the same fundamental problem: "How do I know who's making this request?" Each one makes different trade-offs between simplicity, scalability, and control. There's no single "best" choice โ€” the right answer depends on your architecture, your users, and your security requirements. Let's put them side by side to help you decide.

๐Ÿช Sessions

โœ… Pros: Easy to revoke, server controls everything, simple to implement, works great for traditional web apps

โŒ Cons: Server memory/storage needed, harder to scale horizontally, not ideal for mobile/APIs, sticky sessions in load balancing

๐ŸŽŸ๏ธ JWT

โœ… Pros: Stateless, scales perfectly, works everywhere (web, mobile, IoT), great for microservices

โŒ Cons: Can't easily revoke, token size in every request, complex refresh logic, XSS risk if stored in localStorage

๐Ÿ”‘ OAuth

โœ… Pros: No password management, verified identity, better UX with one-click login, access to provider APIs

โŒ Cons: Depends on third party, complex setup, provider can change API, users need provider account

๐ŸŒณ Decision Tree

โ“ Do you need "Login with Google/GitHub"?

โ†’ YES: Use OAuth (+ Sessions or JWT for your app's auth)

โ“ Is it a SPA / mobile app / microservices?

โ†’ YES: Use JWT (stateless, works everywhere)

โ“ Traditional server-rendered web app?

โ†’ YES: Use Sessions (simple, reliable, easy logout)

๐Ÿ”— Common Combinations

OAuth โ†’ Session

Login with Google, then create a session for your app. Most common for traditional web apps.

OAuth โ†’ JWT

Login with Google, then issue your own JWT. Great for SPAs and mobile apps.

Session + JWT

Sessions for web app, JWT for API endpoints. Covers both browser and API clients.

๐Ÿ”’ Password Security

If your application stores user credentials, password security is your most critical responsibility. A single data breach exposing plain-text passwords can destroy user trust and violate regulations (GDPR, CCPA). The solution: never store the password itself โ€” store a one-way hash instead.

โš ๏ธ Why bcrypt and not SHA-256 or MD5? Regular hash functions (SHA-256, MD5, SHA-1) are designed to be fast โ€” a modern GPU can compute billions of SHA-256 hashes per second. That's great for file checksums, but terrible for passwords: an attacker can try every possible password extremely quickly. bcrypt is deliberately slow โ€” it's designed specifically for password hashing, with a tunable cost factor that makes brute-force attacks impractical. That's also why the session example above uses crypto.createHash('sha256') only as a simplified demo โ€” in real production code, always use bcrypt!

NEVER store plain-text passwords! Always hash them with a strong algorithm like bcrypt.

JavaScript
// npm install bcrypt
const bcrypt = require('bcrypt');

// Registration: hash the password
const SALT_ROUNDS = 12; // higher = slower = more secure

app.post('/register', async (req, res) => {
  const { email, password } = req.body;

  if (password.length < 8) {
    return res.status(400).json({ error: 'Password too short' });
  }

  const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
  // passwordHash = "$2b$12$LJ3m4ys..." (60 chars, includes salt)

  await createUser({ email, passwordHash });
  res.status(201).json({ message: 'User created' });
});

// Login: compare password with hash
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await findUserByEmail(email);
  if (!user) return res.status(401).json({ error: 'Invalid credentials' });

  const isMatch = await bcrypt.compare(password, user.passwordHash);
  if (!isMatch) return res.status(401).json({ error: 'Invalid credentials' });

  // โœ… Password matches โ€” create session or JWT
});

๐Ÿ”ฌ How Hashing Works

A hash function converts any input into a fixed-length string. It's a one-way operation โ€” you can't reverse a hash back to the original password. When a user logs in, you hash the submitted password and compare it to the stored hash. If they match, the password is correct โ€” without ever storing the actual password.

๐Ÿง‚ What are Salt Rounds? A salt is a random value added to the password before hashing, so identical passwords produce different hashes. The rounds parameter (cost factor) controls how many times the hash is computed โ€” higher rounds = slower hashing = harder to brute-force. 12 rounds takes ~250ms, which is fast enough for users but painfully slow for attackers trying millions of passwords.

โš ๏ธ Common Vulnerabilities

๐Ÿ† Golden Rule: Never Trust the Client

Everything from the client can be manipulated โ€” form data, headers, cookies, URL parameters, even hidden fields. Always validate and sanitize on the server.

Authentication tells you who the user is. But even authenticated users can be exploited, and your code might have holes that attackers target. This section covers the most dangerous vulnerabilities in web applications โ€” the ones that appear in real-world breaches every single year. For each vulnerability, you'll see exactly how the attack works, why it's dangerous, and how to prevent it in your Express apps.

โœ… Server-side Validation with express-validator

JavaScript
const { body, validationResult } = require('express-validator');

app.post('/register',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }).escape(),
  body('name').trim().notEmpty().escape(),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // โœ… Input is validated and sanitized, safe to proceed
  }
);

๐Ÿ“‹ OWASP Top 10

The OWASP Top 10 is the definitive list of the most critical web application security risks. We'll focus on the ones most relevant to Express/Node.js development.

CRITICAL

๐Ÿ•ท๏ธ Cross-Site Scripting (XSS)

Attacker injects malicious JavaScript into your page through user input. When other users view the page, the malicious script runs in their browser and can steal cookies, session tokens, or redirect users.

๐ŸŽฏ Attack Example

JavaScript
// โŒ Vulnerable: Using raw user input in HTML
app.get('/search', (req, res) => {
  res.send(`Results for: ${req.query.q}`); // XSS!
});

// Attacker sends: /search?q=<script>alert('XSS')</script>

โœ… Fix: Escape output

JavaScript
function escapeHtml(text) {
  return text
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

// โœ… SAFE: Escape before rendering
app.get('/search', (req, res) => {
  res.send(`Results for: ${escapeHtml(req.query.q)}`);
});

๐Ÿ›ก๏ธ Defense Summary

1
Escape output โ€” Use template engines that auto-escape (EJS <%= %>, Handlebars, Pug)
2
Sanitize input โ€” Use express-validator's .escape() or DOMPurify
3
Set CSP headers โ€” Content Security Policy restricts script sources
4
HttpOnly cookies โ€” Prevents JavaScript access to session cookies
CRITICAL

๐Ÿ’‰ SQL Injection (SQLi)

Attacker inserts malicious SQL code through user input, potentially reading, modifying, or deleting entire databases.

๐ŸŽฏ Attack Example

JavaScript
// โŒ Vulnerable: String concatenation
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;

// Attacker sends email: ' OR '1'='1' --
// Result: SELECT * FROM users WHERE email = '' OR '1'='1' --'
// ๐Ÿ’€ Returns ALL users!

โœ… Fix: Parameterized Queries

JavaScript
// โœ… SAFE: Parameterized query (mysql2)
const [rows] = await db.execute(
  'SELECT * FROM users WHERE email = ?',
  [req.body.email]
);

// โœ… SAFE: Using Prisma ORM
const user = await prisma.user.findUnique({
  where: { email: req.body.email }
});

// โœ… SAFE: MongoDB with Mongoose
const user = await User.findOne({ email: req.body.email });

๐Ÿ›ก๏ธ Defense Summary

1
Use parameterized queries โ€” Never concatenate user input into SQL
2
Use an ORM โ€” Prisma, Sequelize, TypeORM handle escaping for you
3
Validate input types โ€” Ensure numbers are numbers, emails are emails

โš ๏ธ NoSQL Injection Warning! The MongoDB example above is not automatically safe. If req.body.email contains {"$ne": null} instead of a string, User.findOne({email: req.body.email}) would match ALL users! Always validate input types: User.findOne({email: String(req.body.email)}) or use express-validator to ensure inputs are actually strings before querying.

HIGH

๐ŸŽญ Cross-Site Request Forgery (CSRF)

Attacker tricks a logged-in user into making unwanted requests. If you're logged into your bank and visit a malicious site, it could transfer money using your session.

๐ŸŽฏ Attack Example

HTML
<!-- Malicious site embeds this -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />

<!-- Or a hidden form that auto-submits -->
<form action="https://bank.com/transfer" method="POST" id="evil">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('evil').submit();</script>

โœ… Fix: SameSite Cookies + CSRF Tokens

JavaScript
// โœ… SAFE: SameSite cookies
res.cookie('session', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict' // Cookie won't be sent from other sites!
});

๐Ÿ›ก๏ธ Defense Summary

1
CSRF tokens โ€” Unique token per session that attacker can't guess
2
SameSite cookies โ€” SameSite=Strict or Lax prevents cross-site cookie sending
3
Check Origin/Referer โ€” Verify requests come from your domain
HIGH

๐Ÿ”“ Insecure Direct Object Reference (IDOR)

Application exposes internal object IDs (user ID, order ID) without checking if the current user has permission to access them. Attacker just changes the ID in the URL.

๐ŸŽฏ Attack Example

HTTP
// User sees their profile at:
GET /api/users/42/profile

// Attacker changes 42 to 43:
GET /api/users/43/profile  // ๐Ÿ’€ Sees someone else's data!

โœ… Fix: Authorization Checks

JavaScript
// โœ… SAFE: Check ownership before returning data
app.get('/api/users/:id', requireAuth, (req, res) => {
  if (req.user.id !== parseInt(req.params.id)) {
    return res.status(403).json({ error: 'Access denied' });
  }
  // ... return data
});

// โœ… BETTER: Use /me endpoint instead of exposing IDs
app.get('/api/me/profile', requireAuth, async (req, res) => {
  const profile = await db.getProfile(req.user.id);
  res.json(profile);
});

๐Ÿ›ก๏ธ Defense Summary

1
Always check ownership โ€” Verify the logged-in user owns the resource
2
Use indirect references โ€” Map public IDs to internal IDs server-side
3
Use UUIDs โ€” Harder to guess than sequential IDs (but still verify ownership!)
MEDIUM

๐Ÿ”จ Brute Force & DDoS (Missing Rate Limiting)

Without rate limiting, attackers can try thousands of passwords per second, scrape your entire database, or overwhelm your server with requests.

โœ… Fix: Rate Limiting

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

// General rate limit for all requests
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  message: { error: 'Too many requests, slow down!' }
});

// Strict limit for login attempts
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // Only 5 login attempts
  message: { error: 'Too many login attempts, try again later' },
  skipSuccessfulRequests: true
});

app.use(generalLimiter);
app.post('/login', loginLimiter, loginHandler);
app.post('/register', loginLimiter, registerHandler);
IMPORTANT

๐ŸŒ CORS (Cross-Origin Resource Sharing)

CORS isn't a vulnerability itself โ€” it's a browser security mechanism. By default, browsers block JavaScript from making requests to a different domain (the "same-origin policy"). CORS headers tell the browser which cross-origin requests are allowed. Misconfiguring CORS can either block your own frontend or open your API to unauthorized access from any website.

โ“ The Problem

Your React app runs on localhost:3000, your Express API on localhost:5000. Without CORS headers, the browser blocks every API request from your frontend โ€” even though you own both! This is the same-origin policy protecting users, and CORS is the controlled way to relax it.

โœ… Fix: Configure CORS Properly

JavaScript
const cors = require('cors');

// โŒ DANGEROUS: Allows any website to call your API
app.use(cors()); // origin: '*' by default

// โœ… SAFE: Whitelist specific origins
app.use(cors({
  origin: ['https://myapp.com', 'http://localhost:3000'],
  credentials: true, // Allow cookies/auth headers
  methods: ['GET', 'POST', 'PUT', 'DELETE']
}));

๐Ÿ›ก๏ธ Defense Summary

1
Never use origin: '*' with credentials โ€” Browsers actually block this combination, but misconfigurations happen
2
Whitelist specific origins โ€” Only allow domains you control or trust
3
Be explicit about allowed methods and headers โ€” Don't allow more than your API needs

๐Ÿ”ง Essential Security Middleware: Helmet.js

helmet is a must-have middleware that sets various HTTP headers to protect your Express app. One line of code, many protections.

JavaScript
const helmet = require('helmet');

app.use(helmet()); // That's it! Adds 11+ security headers

What helmet sets:

Content-Security-Policy Prevents XSS and data injection
X-Frame-Options Prevents clickjacking
X-Content-Type-Options Prevents MIME-type sniffing
Strict-Transport-Security Forces HTTPS
X-XSS-Protection Legacy XSS filter
Referrer-Policy Controls referer header

โœ… Express Security Checklist

๐Ÿ“ Summary

This module covered the full spectrum of web security: from understanding why HTTP is stateless to implementing three authentication strategies (Sessions, JWT, OAuth), plus the critical vulnerabilities every developer must know. In practice, you'll often combine these approaches โ€” OAuth for login, JWT for API access, and sessions for traditional pages โ€” all protected by input validation, rate limiting, and proper cookie configuration.

๐Ÿช Sessions

Server-side state, cookie with session ID. Simple, server manages everything. Best for traditional web apps.

๐ŸŽŸ๏ธ JWT

Self-contained signed token. Stateless, scales easily. Best for SPAs, mobile, microservices.

๐Ÿ”‘ OAuth 2.0

Delegated auth. "Login with Google." No password management. Often combined with Sessions or JWT.

๐Ÿ”’ Passwords

Always bcrypt. Never store plain text. Salt rounds โ‰ฅ 12.

โš ๏ธ Vulnerabilities

XSS, SQLi, CSRF, IDOR, Brute Force โ€” each has specific defenses. Never trust the client.

๐Ÿ›ก๏ธ Defense

Helmet, rate limiting, input validation, parameterized queries, authorization checks, HTTPS.

๐ŸŽ‰
Course Complete 7 / 7

Congratulations! You've completed the course!

You now have a solid foundation in JavaScript, Node.js, Express, and web security. Keep building projects to solidify your knowledge!

โ† Back to Course Overview