Month 2, Week 4

Authentication & Security in Express.js

Securing Your In-Memory Application

Module 1: The First Rule of Security

Never, Ever, Store Plain-Text Passwords

The Catastrophe of a Data Breach

If an attacker gains access to your database and you have stored passwords in plain text, you have failed. The attacker now has the keys to your users' digital lives, as people often reuse passwords.

Storing plain-text passwords is the single greatest sign of an amateur developer. We are architects.

The Solution: Cryptographic Hashing

A hash function is a one-way street. It's a mathematical algorithm that takes an input (the password) and produces a unique, fixed-length string of characters (the hash).

  • One-Way: You can easily compute a hash from a password, but it is computationally impossible to go from the hash back to the original password.
  • Deterministic: The same input will always produce the same output.

We store the hash, not the password.

Hashing + Salting with bcrypt

Simple hashing isn't enough (due to "rainbow table" attacks). We need to add a salt—a random string of characters unique to each user—to the password before hashing it.

The `bcrypt` library handles all of this for us automatically and securely. It is the industry standard.


                        npm install bcrypt
                    

Using `bcrypt`

It provides two crucial functions:

  1. `bcrypt.hash(plainTextPassword, saltRounds)`: Hashes the password.
  2. `bcrypt.compare(plainTextPassword, hashFromDB)`: Compares a submitted password to a stored hash.

                        const bcrypt = require('bcrypt');
                        const saltRounds = 10; // The "cost factor". Higher is slower but more secure.

                        async function registerUser(password) {
                            const hashedPassword = await bcrypt.hash(password, saltRounds);
                            // Store `hashedPassword` in your database
                            console.log('Stored Hash:', hashedPassword);
                            return hashedPassword;
                        }

                        async function loginUser(submittedPassword, storedHash) {
                            const isMatch = await bcrypt.compare(submittedPassword, storedHash);
                            // isMatch will be true or false
                            console.log('Passwords match:', isMatch);
                        }
                     

Module 2: Stateless Authentication with JWT

The Modern Standard for APIs

The Problem with Sessions

Traditional web apps use sessions, where the server stores a user's login state in memory. This violates the REST principle of statelessness.

In a large-scale application with multiple servers, how does Server B know that the user logged in on Server A? This creates a scaling bottleneck.

The Solution: Tokens

With token-based authentication, the server does not store anything. After a user logs in, the server gives them a token (a signed piece of data).

For every subsequent request to a protected resource, the client must include this token in the request header. The server can then verify the token's authenticity without needing to look up a session store.

This is truly stateless and infinitely scalable.

JSON Web Tokens (JWT)

JWT (pronounced "jot") is an open standard for creating compact and self-contained tokens for securely transmitting information between parties as a JSON object.

A JWT consists of three parts, separated by dots:

Header.Payload.Signature


                        eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTYxNjQ0NjE5N30.c_p_4a...
                    

Anatomy of a JWT

  • Header (Base64Url Encoded): Contains metadata about the token, like the hashing algorithm used.
    { "alg": "HS256", "typ": "JWT" }
  • Payload (Base64Url Encoded): Contains the claims or data. This is the information we want to transmit, like the user's ID. **Important: This data is encoded, not encrypted. Anyone can read it. Do not put sensitive information here.**
    { "userId": 1, "role": "admin", "iat": 1616446197 }
  • Signature: The most critical part. It's created by taking the encoded header, the encoded payload, a secret key known only to the server, and signing them with the algorithm specified in the header.

The Power of the Signature

The signature is what makes a JWT secure.

When the server receives a token, it re-calculates the signature using the header, the payload, and its secret key. If the re-calculated signature matches the signature on the token, the server knows that the token is authentic and has not been tampered with.

If an attacker changes the payload (e.g., `"role": "user"` to `"role": "admin"`), the signature will no longer match, and the server will reject the token.

Mid-Lecture Knowledge Check

Module 3: Implementing JWT in Express

Creating and Verifying Tokens

Installation

We will use the standard `jsonwebtoken` library.


                        npm install jsonwebtoken bcrypt
                    

We also need a secret key. This should be a long, random, and private string stored securely as an environment variable (we'll learn more about this later). For now, we'll define it in our code.

User Registration (Hashing the Password)

When a new user registers, we hash their password before storing it.


                        const bcrypt = require('bcrypt');
                        // ...
                        
                        router.post('/register', async (req, res) => {
                            try {
                                const { name, email, password } = req.body;
                                // In a real app, you'd check if the user already exists

                                const hashedPassword = await bcrypt.hash(password, 10);
                                
                                const newUser = {
                                    id: nextId++,
                                    name,
                                    email,
                                    password: hashedPassword // Store the hash!
                                };
                                users.push(newUser);

                                res.status(201).send('User registered successfully');
                            } catch {
                                res.status(500).send();
                            }
                        });
                    

User Login (Generating the Token)

When a user logs in, we compare their submitted password to the stored hash. If they match, we generate and send back a JWT.


                        const jwt = require('jsonwebtoken');
                        const JWT_SECRET = 'your_super_secret_key_that_is_very_long';
                        // ...

                        router.post('/login', async (req, res) => {
                            const user = users.find(u => u.email === req.body.email);
                            if (!user) return res.status(400).send('Invalid email or password.');

                            const validPassword = await bcrypt.compare(req.body.password, user.password);
                            if (!validPassword) return res.status(400).send('Invalid email or password.');

                            // Create the token payload
                            const payload = { userId: user.id, name: user.name };

                            const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });

                            res.json({ token });
                        });
                     

Module 4: Protecting Routes with Middleware

The Authentication Gatekeeper

The Strategy

We will write a custom middleware function that will act as a guard for our protected routes.

The flow for a protected request:

  1. Client makes a request to `/users/5` with an `Authorization` header containing the JWT.
  2. Our auth middleware intercepts the request.
  3. It extracts and verifies the token.
  4. If the token is valid, it attaches the user payload to the `req` object (`req.user`) and calls `next()`.
  5. The actual route handler for `/users/5` then runs, and it now knows which user is making the request.

Writing the Authentication Middleware


                        function authMiddleware(req, res, next) {
                            // The header format is "Bearer TOKEN"
                            const authHeader = req.header('Authorization');
                            if (!authHeader) return res.status(401).send('Access denied. No token provided.');

                            const token = authHeader.split(' ')[1];
                            if (!token) return res.status(401).send('Access denied. Token format is invalid.');

                            try {
                                // jwt.verify will throw an error if the token is invalid or expired
                                const decodedPayload = jwt.verify(token, JWT_SECRET);
                                
                                // Add the payload to the request object
                                req.user = decodedPayload;
                                next();
                            } catch (ex) {
                                res.status(400).send('Invalid token.');
                            }
                        }
                    

Applying the Middleware

You can apply the middleware to any route that needs to be protected. Only authenticated users will be able to access it.


                        // In userRoutes.js

                        // This route is public
                        router.get('/', (req, res) => { /* get all users */ });

                        // This route is now protected. The authMiddleware runs first.
                        router.get('/:id', authMiddleware, (req, res) => {
                            // Because of the middleware, we now have access to req.user
                            console.log('Authenticated user:', req.user);
                            
                            const user = users.find(u => u.id === parseInt(req.params.id));
                            if (!user) return res.status(404).send('User not found');
                            res.json(user);
                        });
                        
                        // We can also protect our DELETE route
                        router.delete('/:id', authMiddleware, deleteUser);
                    

In-Class Practical Exercise

Securing the CRUD API

You will be given the complete, working CRUD API for `users` from last week. Your task is to implement a full authentication system from scratch.

  1. Install `bcrypt` and `jsonwebtoken`.
  2. Create a new router file `auth.js` for your authentication routes.
  3. In an `authController.js`, implement a `register` function that hashes the user's password and adds them to the in-memory array.
  4. Implement a `login` function that validates credentials and returns a JWT.
  5. Create an `authMiddleware.js` file and write the middleware to verify the JWT from the `Authorization` header.
  6. In `app.js`, mount your new `authRouter` at `/auth`.
  7. In `userRoutes.js`, apply your `authMiddleware` to the `PATCH /:id` and `DELETE /:id` routes to protect them. The `GET` routes should remain public.
  8. Test the entire flow with Postman or Insomnia.

Final Knowledge Check