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!