Authentication, JWT & Cookies
Episode 17 - Authentication, JWT & Cookies
Hey everyone! Welcome back to the Node.js tutorial series. Today we're going to learn about Authentication - one of the MOST important concepts in backend development!
How do websites know you're logged in? How do they keep you logged in across pages? Let's find out!
What we will cover:
- Why We Need Authentication
- How Server Validates Users
- What is JWT?
- Structure of JWT
- Creating & Verifying JWT Tokens
- Working with Cookies
- Auth Middleware
- Schema Methods
- Interview Questions
1) Why Do We Need Authentication?
Whenever a client requests data from the server, a TCP/IP connection is made. But how does the server know WHO is making the request?
The Problem: ============ Request 1: "Give me my profile data" Server: "Who are you? 🤔" Request 2: "Show me my orders" Server: "Who are you? 🤔" Every request is INDEPENDENT! Server doesn't remember previous requests! This is the "stateless" nature of HTTP.
The Solution - Authentication: ============================== 1. User logs in with credentials (email/password) 2. Server validates credentials 3. Server creates a TOKEN and sends to client 4. Client stores token and sends with EVERY request 5. Server validates token and responds Now server knows WHO is making each request!
2) How Server Validates the User?
Authentication Flow:
====================
CLIENT SERVER
====== ======
1. Login Request
(email, password)
│
└─────────────────────────────→ Validate credentials
Create JWT token
│
←─────────────────────────────────────┘
Receive JWT token Send JWT in response
Store in cookies
│
▼
2. API Request + JWT token
│
└─────────────────────────────→ Validate JWT token
Is token valid?
Is token expired?
│
←─────────────────────────────────────┘
Receive data Send response data
│
▼
3. Another Request + JWT token
│
└─────────────────────────────→ Validate JWT again
│
←─────────────────────────────────────┘
Receive data Send response
Every request sends the JWT token!
Server validates token EVERY time!
3) Where are JWT Tokens Stored?
JWT tokens are stored in COOKIES in the browser! Browser Storage Options: ======================== 1. Cookies (Recommended for auth!) ✅ Automatically sent with every request ✅ Can be HTTP-only (prevents XSS) ✅ Can set expiry 2. Local Storage ❌ Not automatically sent ❌ Vulnerable to XSS attacks ❌ Manual handling required 3. Session Storage ❌ Cleared when tab closes ❌ Not ideal for auth
4) When Will Cookies Not Work?
Cookies Won't Work When: ======================== 1. Cookie is EXPIRED - Every cookie has an expiry time - After expiry, browser deletes it 2. JWT token is EXPIRED - Token itself has expiry (set by server) - Even if cookie exists, token is invalid 3. Cookie is DELETED - User clears browser data - Logout clears cookies 4. Different domain - Cookies are domain-specific - Can't access across domains (security!)
5) How to Create & Access Cookies
Creating a Cookie
// Express provides res.cookie() method
app.post("/login", async (req, res) => {
// ... validate user, create token ...
// Create cookie
res.cookie("token", jwtToken);
// With options
res.cookie("token", jwtToken, {
expires: new Date(Date.now() + 8 * 3600000), // 8 hours
httpOnly: true, // Can't be accessed by JavaScript
secure: true, // Only sent over HTTPS
sameSite: "strict" // CSRF protection
});
res.send("Login successful!");
});
Accessing Cookies
Step 1: Install cookie-parser
=============================
npm i cookie-parser
Step 2: Use middleware
======================
const cookieParser = require("cookie-parser");
app.use(cookieParser());
Step 3: Access cookies
======================
app.get("/profile", (req, res) => {
// req.cookies is an object with all cookies
console.log(req.cookies);
// { token: "eyJhbGciOiJIUzI1NiIs..." }
const token = req.cookies.token;
// Now validate this token!
});
6) What is JWT?
JWT stands for JSON Web Token. It's a way of securely sharing information between client and server.
JWT = JSON Web Token ==================== - Compact, URL-safe token - Contains encoded JSON data - Digitally signed (can't be tampered) - Self-contained (contains all user info) Used for: - Authentication (who are you?) - Authorization (what can you access?) - Information exchange (secure data sharing)
7) Structure of JWT
A JWT token consists of three parts separated by dots (.)
JWT Structure:
==============
xxxxx.yyyyy.zzzzz
│ │ │
│ │ └── SIGNATURE
│ └──────── PAYLOAD
└─────────────── HEADER
Example JWT:
============
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJfaWQiOiIxMjM0NTYiLCJpYXQiOjE2MTYxNjE2MTZ9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
↑ ↑ ↑
HEADER PAYLOAD SIGNATURE
The Three Parts Explained
1. HEADER
=========
Contains metadata about the token:
- Token type (JWT)
- Hashing algorithm (HS256, RS256)
{
"alg": "HS256", // Algorithm
"typ": "JWT" // Token type
}
This is Base64 encoded → eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2. PAYLOAD
==========
Contains the data we want to send:
- User ID
- Role
- Any custom data
{
"_id": "123456",
"email": "john@email.com",
"role": "admin",
"iat": 1616161616, // Issued at
"exp": 1616165216 // Expiry
}
This is Base64 encoded → eyJfaWQiOiIxMjM0NTYiLCJlbWFpbCI6...
⚠️ WARNING: Payload is NOT encrypted!
Anyone can decode it!
Never put sensitive data (passwords) here!
3. SIGNATURE
============
Used to verify the token wasn't tampered with:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECRET_KEY
)
- Combines header + payload + secret
- If anyone changes payload, signature won't match
- Only server knows the SECRET_KEY!
Visual Representation:
======================
┌─────────────────────────────────────────────────────┐
│ JWT TOKEN │
├─────────────────────────────────────────────────────┤
│ HEADER │ PAYLOAD │ SIGNATURE │
│ ─────── │ ──────── │ ────────── │
│ { │ { │ │
│ "alg":"HS256" │ "_id":"123" │ HMACSHA256( │
│ "typ":"JWT" │ "role":"user" │ header + │
│ } │ "exp":1234 │ payload + │
│ │ } │ secret │
│ │ │ ) │
├─────────────────────────────────────────────────────┤
│ xxxxx.yyyyy.zzzzz │
└─────────────────────────────────────────────────────┘
8) How to Create a JWT Token
Step 1: Install jsonwebtoken
============================
npm i jsonwebtoken
Step 2: Create token using sign()
=================================
const jwt = require("jsonwebtoken");
app.post("/login", async (req, res) => {
try {
const { email, password } = req.body;
// Find user in database
const user = await User.findOne({ email: email });
if (!user) {
throw new Error("User not found");
}
// Compare password (using bcrypt)
const isPasswordValid = await bcrypt.compare(
password,
user.password
);
if (isPasswordValid) {
// CREATE JWT TOKEN
const token = await jwt.sign(
{ _id: user._id }, // Payload (data to encode)
"YOUR_SECRET_KEY" // Secret key
);
// Add token to cookie
res.cookie("token", token);
res.send("Login successful!");
} else {
throw new Error("Invalid credentials");
}
} catch (err) {
res.status(400).send("Error: " + err.message);
}
});
jwt.sign() Parameters:
======================
jwt.sign(payload, secretKey, options)
1. payload: Object with data to encode
{ _id: user._id, role: "admin" }
2. secretKey: Your secret string
Store in .env file!
3. options: Optional settings
{ expiresIn: "1h" }
9) How to Verify JWT Token
Verifying Token on Subsequent Requests:
=======================================
const jwt = require("jsonwebtoken");
app.get("/profile", async (req, res) => {
try {
// Get cookies
const cookies = req.cookies;
const { token } = cookies;
if (!token) {
throw new Error("Invalid token");
}
// VERIFY & DECODE the token
const decodedToken = await jwt.verify(
token,
"YOUR_SECRET_KEY"
);
// decodedToken = { _id: "123456", iat: 1616..., exp: 1616... }
const { _id } = decodedToken;
// Find user by ID from token
const user = await User.findById(_id);
if (!user) {
throw new Error("User does not exist");
}
res.send(user);
} catch (err) {
res.status(400).send("Error: " + err.message);
}
});
jwt.verify() does TWO things:
=============================
1. VALIDATES the signature
- Checks if token was tampered
- Uses secret key to verify
2. DECODES the payload
- Returns the original data
- { _id, iat, exp, ... }
If token is invalid or expired:
- Throws an error!
- Caught by catch block
10) How to Create Auth Middleware
We don't want to repeat token validation in every route! Let's create a middleware.
Why Middleware?
===============
Without middleware:
app.get("/profile", validateToken, ...)
app.get("/orders", validateToken, ...)
app.get("/settings", validateToken, ...)
app.post("/update", validateToken, ...)
// Same code repeated everywhere! 😫
With middleware:
app.get("/profile", userAuth, ...)
app.get("/orders", userAuth, ...)
// Clean, reusable! 😎
Creating Auth Middleware:
=========================
// middleware/auth.js
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const userAuth = async (req, res, next) => {
try {
// Get token from cookies
const { token } = req.cookies;
if (!token) {
throw new Error("Invalid token");
}
// Verify and decode token
const decodedToken = await jwt.verify(
token,
process.env.JWT_SECRET
);
const { _id } = decodedToken;
// Find user
const user = await User.findById(_id);
if (!user) {
throw new Error("User not found");
}
// ATTACH user to request object
req.user = user;
// Call next middleware/handler
next();
} catch (err) {
res.status(401).send("Error: " + err.message);
}
};
module.exports = { userAuth };
Using the Middleware:
=====================
const { userAuth } = require("./middleware/auth");
// Protected routes - require authentication
app.get("/profile", userAuth, async (req, res) => {
// req.user is available here!
res.send(req.user);
});
app.get("/orders", userAuth, async (req, res) => {
const orders = await Order.find({ userId: req.user._id });
res.send(orders);
});
// Public routes - no authentication needed
app.post("/signup", async (req, res) => { ... });
app.post("/login", async (req, res) => { ... });
Middleware Flow:
================
Request → userAuth middleware → Route Handler → Response
│
├── Token valid? → next() → Handler executes
│
└── Token invalid? → Send 401 error
11) Setting Expiry Time for JWT Token
JWT Expiry:
===========
const token = await jwt.sign(
{ _id: user._id },
"SECRET_KEY",
{ expiresIn: "1h" } // Token expires in 1 hour
);
Expiry Options:
- "1h" → 1 hour
- "7d" → 7 days
- "30d" → 30 days
- 3600 → 3600 seconds (1 hour)
12) Setting Expiry Time for Cookies
Cookie Expiry:
==============
res.cookie("token", token, {
expires: new Date(Date.now() + 8 * 3600000) // 8 hours
});
// OR using maxAge (milliseconds)
res.cookie("token", token, {
maxAge: 8 * 60 * 60 * 1000 // 8 hours in ms
});
Best Practice:
==============
Set cookie expiry SAME as JWT expiry!
const token = jwt.sign(payload, secret, { expiresIn: "7d" });
res.cookie("token", token, {
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
httpOnly: true
});
13) What are Schema Methods?
Schema methods are custom functions attached to your Mongoose schema. They're available on all documents created from that schema.
Schema Methods: =============== - Custom functions on schema - Available on ALL documents - Access document data with "this" - Arrow functions NOT allowed (this is undefined!) Why use them? - Cleaner code - Reusable logic - Keep related code together
14) How to Create Schema Methods
Creating Schema Method:
=======================
// models/User.js
const userSchema = new mongoose.Schema({
email: String,
password: String,
// ... other fields
});
// Add custom method
userSchema.methods.validatePassword = async function(inputPassword) {
// "this" refers to the current user document
const user = this;
const isPasswordValid = await bcrypt.compare(
inputPassword, // Password from login request
user.password // Hashed password in database
);
return isPasswordValid;
};
// Another method - generate JWT
userSchema.methods.getJWT = function() {
const user = this;
const token = jwt.sign(
{ _id: user._id },
process.env.JWT_SECRET,
{ expiresIn: "7d" }
);
return token;
};
module.exports = mongoose.model("User", userSchema);
Using Schema Methods:
=====================
app.post("/login", async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
throw new Error("User not found");
}
// Using schema method!
const isPasswordValid = await user.validatePassword(password);
if (!isPasswordValid) {
throw new Error("Invalid credentials");
}
// Using another schema method!
const token = user.getJWT();
res.cookie("token", token);
res.send("Login successful!");
});
Benefits:
=========
- Code is CLEANER
- Logic is ENCAPSULATED
- Methods are REUSABLE
- Easy to MAINTAIN
⚠️ Important: No Arrow Functions!
=================================
// ❌ WRONG - Arrow function
userSchema.methods.getJWT = () => {
console.log(this); // undefined!
};
// ✅ CORRECT - Regular function
userSchema.methods.getJWT = function() {
console.log(this); // User document!
};
Arrow functions don't have their own "this"!
Quick Recap
| Concept | Description |
|---|---|
| Authentication | Verifying WHO the user is |
| JWT | JSON Web Token - secure token for auth |
| JWT Structure | Header.Payload.Signature |
| Cookies | Store JWT on client side |
| jwt.sign() | Create a new JWT token |
| jwt.verify() | Validate and decode JWT token |
| cookie-parser | Middleware to parse cookies |
| Auth Middleware | Reusable token validation |
| Schema Methods | Custom functions on Mongoose schema |
Interview Questions
Q: What is JWT and why is it used?
"JWT (JSON Web Token) is a compact, URL-safe way of securely transmitting information between parties as a JSON object. It's commonly used for authentication - after login, the server creates a JWT containing user info, and the client sends this token with every request to prove identity."
Q: What are the three parts of a JWT?
"JWT has three parts separated by dots: Header (contains algorithm and token type), Payload (contains the data like user ID), and Signature (created by hashing header + payload + secret key). The signature ensures the token hasn't been tampered with."
Q: Where should JWT tokens be stored on the client?
"JWT tokens should be stored in HTTP-only cookies. This is more secure than localStorage because HTTP-only cookies can't be accessed by JavaScript, protecting against XSS attacks. Cookies are also automatically sent with every request."
Q: What is the difference between authentication and authorization?
"Authentication verifies WHO the user is (identity verification through login). Authorization determines WHAT the user can access (permissions and roles). First you authenticate, then you authorize."
Q: Why can't we use arrow functions for Schema methods?
"Arrow functions don't have their own 'this' binding - they inherit 'this' from the surrounding scope. Schema methods need 'this' to refer to the current document. With arrow functions, 'this' would be undefined, so we must use regular functions."
Key Points to Remember
- JWT = JSON Web Token for secure authentication
- JWT has 3 parts: Header, Payload, Signature
- Store JWT in HTTP-only cookies
- jwt.sign() creates token
- jwt.verify() validates & decodes token
- Use cookie-parser middleware
- Create auth middleware for protected routes
- Set expiry for both JWT and cookies
- Schema methods for cleaner code
- No arrow functions in schema methods
- Never store sensitive data in JWT payload
- Keep secret key in .env file
What's Next?
Now you understand authentication with JWT and Cookies! In the upcoming episodes, we will:
- Implement password reset functionality
- Add OAuth (Google/Facebook login)
- Implement refresh tokens
Keep coding, keep learning! See you in the next one!
Post a Comment