Lesson 43 of 50 – Authentication in Node.js
86% Completed
86%

Authentication in Node.js

Learn how to register users, securely store passwords, log users in, manage sessions, and protect private routes in a Node.js application.

What is Authentication? Authentication is the process of verifying a user's identity. For example, a student enters an email address and password to log in to a student dashboard.

1. Authentication vs Authorization

Authentication Authorization
Verifies who the user is. Checks what the user is allowed to do.
Usually happens during login. Usually happens when accessing a protected resource.
Example: Verify a student's password. Example: Allow only an admin to delete students.
Remember: Authentication asks, "Who are you?" Authorization asks, "What are you allowed to access?"

2. How Authentication Works

Authentication Process
  1. The user registers with an email and password.
  2. The server validates the submitted information.
  3. The server securely hashes the password.
  4. The password hash is stored in the database.
  5. The user submits their credentials during login.
  6. The server verifies the password.
  7. The server creates a session or issues a token.
  8. The user can access authorized private resources.

3. Create a Node.js Project

Create a folder named node-auth-app.

mkdir node-auth-app
cd node-auth-app
npm init -y

Install the required packages:

npm install express bcryptjs express-session dotenv
npm install --save-dev nodemon
Package Explanation
Package Purpose
express Creates the web server and routes.
bcryptjs Hashes passwords and verifies them.
express-session Manages login sessions.
dotenv Loads environment variables from a file.
nodemon Restarts the server when files change during development.

4. Why Should We Hash Passwords?

Passwords should never be stored as plain text in a database. A password hash is a one-way transformation used to verify a password without storing the original password.

Insecure Example
{
    "email": "student@example.com",
    "password": "mypassword123"
}

Storing the original password like this exposes it if the database is accessed by an unauthorized person.

Safer Approach

Use a password-hashing algorithm such as bcrypt or Argon2. Store the resulting hash, not the original password.

const bcrypt = require("bcryptjs");

async function hashPassword(password) {
    const hash = await bcrypt.hash(password, 12);
    return hash;
}

The work factor controls the computational cost of hashing. Choose it based on current security guidance and your server's capacity.

5. Create a User Store for Practice

To keep this first example easy to run, we will use an in-memory array instead of a database. The users will disappear when the server restarts.

Create a file named users.js.

const users = [];

module.exports = users;
This array is only for learning. Do not use in-memory arrays as the user database for a production application.

6. Create the Express Authentication Server

Create a file named server.js. This example uses server-side sessions and bcrypt password hashing.

server.js
require("dotenv").config();

const express = require("express");
const bcrypt = require("bcryptjs");
const session = require("express-session");
const users = require("./users");

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

if (!process.env.SESSION_SECRET) {
    throw new Error("SESSION_SECRET must be configured");
}

app.use(session({
    name: "auth.sid",
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "lax",
        maxAge: 1000 * 60 * 60
    }
}));

// Register a user
app.post("/register", async (req, res) => {
    try {
        const { name, email, password } = req.body;

        if (
            typeof name !== "string" ||
            !name.trim() ||
            typeof email !== "string" ||
            !email.trim() ||
            typeof password !== "string" ||
            password.length < 8 ||
            password.length > 72
        ) {
            return res.status(400).json({
                message: "Provide a name, email, and password of 8 to 72 characters"
            });
        }

        const normalizedEmail = email.trim().toLowerCase();

        const existingUser = users.find(
            user => user.email === normalizedEmail
        );

        if (existingUser) {
            return res.status(409).json({
                message: "Email already registered"
            });
        }

        const passwordHash = await bcrypt.hash(password, 12);

        const user = {
            id: users.length + 1,
            name: name.trim(),
            email: normalizedEmail,
            passwordHash: passwordHash
        };

        users.push(user);

        res.status(201).json({
            message: "Registration successful",
            user: {
                id: user.id,
                name: user.name,
                email: user.email
            }
        });
    } catch (error) {
        console.error("Registration error:", error.message);

        res.status(500).json({
            message: "Unable to register user"
        });
    }
});

// Login a user
app.post("/login", async (req, res, next) => {
    try {
        const { email, password } = req.body;

        if (
            typeof email !== "string" ||
            typeof password !== "string"
        ) {
            return res.status(400).json({
                message: "Email and password are required"
            });
        }

        const normalizedEmail = email.trim().toLowerCase();

        const user = users.find(
            item => item.email === normalizedEmail
        );

        if (!user) {
            return res.status(401).json({
                message: "Invalid email or password"
            });
        }

        const passwordMatches = await bcrypt.compare(
            password,
            user.passwordHash
        );

        if (!passwordMatches) {
            return res.status(401).json({
                message: "Invalid email or password"
            });
        }

        // Regenerate the session after successful authentication.
        req.session.regenerate((error) => {
            if (error) {
                return next(error);
            }

            req.session.userId = user.id;

            req.session.save((saveError) => {
                if (saveError) {
                    return next(saveError);
                }

                res.json({
                    message: "Login successful",
                    user: {
                        id: user.id,
                        name: user.name,
                        email: user.email
                    }
                });
            });
        });
    } catch (error) {
        next(error);
    }
});

// Authentication middleware
function requireLogin(req, res, next) {
    if (!req.session.userId) {
        return res.status(401).json({
            message: "Please log in first"
        });
    }

    next();
}

// Protected profile route
app.get("/profile", requireLogin, (req, res) => {
    const user = users.find(
        item => item.id === req.session.userId
    );

    if (!user) {
        return res.status(401).json({
            message: "User session is no longer valid"
        });
    }

    res.json({
        id: user.id,
        name: user.name,
        email: user.email
    });
});

// Logout
app.post("/logout", requireLogin, (req, res, next) => {
    req.session.destroy((error) => {
        if (error) {
            return next(error);
        }

        res.clearCookie("auth.sid", {
            httpOnly: true,
            secure: process.env.NODE_ENV === "production",
            sameSite: "lax"
        });

        res.json({
            message: "Logout successful"
        });
    });
});

// Basic error handler
app.use((error, req, res, next) => {
    console.error("Server error:", error.message);

    if (res.headersSent) {
        return next(error);
    }

    res.status(500).json({
        message: "Internal server error"
    });
});

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

7. Configure Environment Variables

Create a file named .env in your project directory. Generate a long, random secret for your session.

SESSION_SECRET=replace_with_a_long_random_secret
PORT=5000
NODE_ENV=development

For local development, you can generate a secret using Node.js:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Replace the placeholder with the generated value. Keep the .env file private and add it to .gitignore.

.env
node_modules/
Production warning: The default session MemoryStore is for development only. Use a suitable persistent session store in production, deploy over HTTPS, and configure secure cookies.

8. Run the Application

Start the server:

node server.js
Expected output:
Server running on port 5000

9. Register a User Using Postman

Send a POST request to:

http://localhost:5000/register

Choose Body → raw → JSON and enter:

{
    "name": "Rahul Kumar",
    "email": "rahul@example.com",
    "password": "ExamplePass123"
}
Expected Response
{
    "message": "Registration successful",
    "user": {
        "id": 1,
        "name": "Rahul Kumar",
        "email": "rahul@example.com"
    }
}

Status code: 201 Created

The response does not include the password or password hash. The hash is kept on the server for password verification.

10. Login Using Postman

Send a POST request to:

http://localhost:5000/login

Enter the registered credentials:

{
    "email": "rahul@example.com",
    "password": "ExamplePass123"
}
Expected Response
{
    "message": "Login successful",
    "user": {
        "id": 1,
        "name": "Rahul Kumar",
        "email": "rahul@example.com"
    }
}

The server also sends a session cookie. Postman can store this cookie and send it with later requests to the same server.

11. Access the Protected Profile

After logging in, send a GET request to:

http://localhost:5000/profile

Make sure your API client sends the session cookie received during login.

Expected Response
{
    "id": 1,
    "name": "Rahul Kumar",
    "email": "rahul@example.com"
}

If you access the route without a valid session, the API returns 401 Unauthorized.

12. Logout

Send a POST request to:

http://localhost:5000/logout

Send the session cookie with the request. The server destroys the session and clears the cookie.

Expected Response
{
    "message": "Logout successful"
}

Try opening /profile again. The old session should no longer grant access.

13. Authentication Request Flow

Registration and Login
  1. User registers with an email and password.
  2. Server hashes the password and stores the hash.
  3. User logs in with the same credentials.
  4. Server compares the submitted password with the hash.
  5. Server creates a new session after successful login.
  6. Browser or API client stores the session cookie.
  7. Protected routes verify the session before responding.
  8. Logout destroys the session.

14. Common Authentication Errors

Problem Possible Solution
Invalid email or password Check the submitted credentials and registration data.
401 Unauthorized Log in and send the session cookie with the protected request.
Session secret missing Configure SESSION_SECRET in the environment.
Session disappears after restart The example uses in-memory users and the default session store.
Cookie not sent Check cookie settings, client behavior, HTTPS, and origin configuration.
Email already registered Use a different email or implement a database-backed account recovery flow.

15. Authentication Security Best Practices

  • Never store plain-text passwords.
  • Use bcrypt or Argon2 with an appropriate work factor.
  • Use HTTPS in production.
  • Keep session secrets and credentials outside source code.
  • Regenerate the session ID after successful login.
  • Use a persistent session store in production.
  • Set cookies with appropriate HttpOnly, Secure, and SameSite attributes.
  • Use rate limiting and other protections against repeated login attempts.
  • Validate all user input on the server.
  • Implement authorization checks for protected operations.
  • Use CSRF protection where required by your cookie-based authentication design.

16. Practical Assignment

User Authentication System

Create a simple authentication API using Node.js and Express.

  1. Create a registration endpoint.
  2. Validate the name, email, and password.
  3. Hash passwords using bcrypt.
  4. Create a login endpoint.
  5. Use sessions to track authenticated users.
  6. Create a protected profile endpoint.
  7. Create a logout endpoint.
  8. Test successful and unsuccessful logins using Postman.

Bonus Task: Replace the in-memory user array with a MySQL or MongoDB database and add unique email constraints at the database level.

17. Lesson Summary

In this lesson, you learned the difference between authentication and authorization, password hashing with bcrypt, user registration, login sessions, protected routes, logout, and basic authentication security practices.

Test Your Knowledge – Authentication

Select the correct answer for each question.

1. What is authentication?
2. Which package is used in this lesson to hash passwords?
3. What should happen when an unauthenticated user accesses a protected route?