Skip to Content
Lesson 1

Foundations of Web Authentication & Password Security

You have been assigned to a new project at your company, “Innovate Inc.” The project is a new customer-facing portal that will require users to create accounts and log in to access their data. As a backend developer, your first task is to lay the groundwork for a secure authentication system. This involves understanding the core principles of web security and building a robust user model. Your senior developer has emphasized that getting security right from the beginning is critical to the project’s success.


Authentication vs. Authorization

Before we write a single line of code, it is crucial to understand the difference between authentication and authorization.

  • Authentication (AuthN) is the process of verifying who a user is. It’s like showing your ID at a security checkpoint. The system checks your credentials (like a username and password) and confirms that you are the user you claim to be. Authentication is about identity.

  • Authorization (AuthZ) is the process of determining what an authenticated user is allowed to do. Once you are past the security checkpoint, authorization dictates which doors you can open. It involves checking a user’s permissions or roles to grant or deny access to specific resources or actions. Authorization is about permissions.

A simple metaphor: Authentication is getting into the building. Authorization is which rooms you can enter once you’re inside.

In any secure system, authentication always comes before authorization. You can’t determine what a user can do until you know who they are.


Designing a Secure User Model

The foundation of any authentication system is the User model. When using Mongoose, a well-designed schema is your first line of defense. At a minimum, a user schema for an authentication system needs a way to uniquely identify the user and a place to store their credentials securely.

Let’s consider a basic userSchema:

import { Schema, model } from "mongoose"; const userSchema = new Schema({ username: { type: String, required: true, unique: true, trim: true, }, email: { type: String, required: true, unique: true, match: [/.+@.+\..+/, "Must match an email address!"], }, password: { type: String, required: true, minlength: 8, }, }); const User = model("User", userSchema); export default User;

In this schema, we define username and email as unique identifiers. The password field is a simple string, but we will never store the user’s actual password in this field. Doing so would be a massive security vulnerability. If your database were ever compromised, all user passwords would be exposed in plain text.

Instead, we must store a hashed version of the password.


Password Security: Hashing and Salting

A hash function is a one-way mathematical algorithm that takes an input (of any size) and produces a fixed-size string of characters, which is called the “hash” or “digest.” This process is irreversible; you cannot derive the original input from its hash.

However, simply hashing a password isn’t enough. If two users have the same password (“password123”), it will produce the same hash. Attackers can use pre-computed tables of common passwords and their hashes (called “rainbow tables”) to crack them easily.

To prevent this, we use a technique called salting. A “salt” is a random string of data that is added to the password before hashing. This salt is then stored alongside the hashed password in the database.

[Password] + [Random Salt] -> [Hash Function] -> [Secure Hash]

By using a unique salt for every user, we ensure that even identical passwords result in completely different hashes. This makes rainbow table attacks and other bulk password-cracking techniques ineffective.

Implementing Hashing with bcrypt

The bcrypt library is the industry standard for hashing passwords in Node.js applications. It handles both hashing and salting automatically and is designed to be slow, which makes it resistant to brute-force attacks.

First, install it:

npm install bcrypt

Now, let’s modify our userSchema to automatically hash the password before it’s saved to the database. We can use Mongoose “pre-save” middleware (or a “hook”) to execute this logic.

// models/User.js import { Schema, model } from "mongoose"; import bcrypt from "bcrypt"; const userSchema = new Schema({ // ... (username and email fields) password: { type: String, required: true, minlength: 8, }, }); // Set up pre-save middleware to create password userSchema.pre("save", async function (next) { if (this.isNew || this.isModified("password")) { const saltRounds = 10; this.password = await bcrypt.hash(this.password, saltRounds); } next(); }); const User = model("User", userSchema); export default User;

In this code:

  1. We use a pre('save') hook, which tells Mongoose to run our function before any User document is saved.
  2. We check if the document is new (this.isNew) or if the password field has been modified. This prevents us from re-hashing the password on every save (e.g., when a user updates their email).
  3. bcrypt.hash() takes the plain-text password (this.password) and a “salt rounds” value. The salt rounds determine how computationally expensive the hash calculation is. A value of 10 is a good starting point.

Verifying a Password

When a user tries to log in, we need to compare the password they provided with the hashed password stored in our database. We do not hash the incoming password and compare the hashes directly. Instead, bcrypt provides a compare method for this purpose.

Let’s add an instance method to our userSchema to handle this comparison:

// models/User.js // ... (userSchema definition and pre-save hook) // Compare the incoming password with the hashed password userSchema.methods.isCorrectPassword = async function (password) { return bcrypt.compare(password, this.password); }; const User = model("User", userSchema); export default User;

Now, when a user logs in, we can use this method to securely check their password:

// Example usage in a login route const user = await User.findOne({ email: req.body.email }); if (!user) { return res.status(400).json({ message: "Incorrect email or password" }); } const correctPw = await user.isCorrectPassword(req.body.password); if (!correctPw) { return res.status(400).json({ message: "Incorrect email or password" }); } // If both are true, the user is authenticated!

By using bcrypt.compare(), we delegate the complex and critical task of securely checking the password to a battle-tested library, ensuring we never expose or mishandle user credentials.


Activity 1: Hashing and Comparing Passwords

Your task is to practice the core skills of this lesson. In a new Node.js script, perform the following steps:

  1. Define a plain-text password.
  2. Use bcrypt to hash the password asynchronously with 10 salt rounds.
  3. Log the resulting hash to the console.
  4. Use bcrypt’s compare method to check if the original password matches the hash. Log the result (which should be true).
  5. Use the compare method again, but this time with an incorrect password. Log the result (which should be false).

This exercise will solidify your understanding of the two most important functions in the bcrypt library.


Knowledge Check

A user logs into your application. Your system checks their username and password to verify their identity. What is this process called?

  • Select an answer to view feedback.

Why should you never store plain-text passwords in a database?

  • Select an answer to view feedback.

In the context of bcrypt, what is the purpose of 'salt rounds'?

  • Select an answer to view feedback.

Summary

In this lesson, you took the first critical steps into web security. You learned the fundamental difference between authentication (verifying identity) and authorization (granting permissions). We designed a basic but secure Mongoose User model and, most importantly, implemented password hashing using bcrypt. You now know how to take a plain-text password, apply a salt, and create a secure, one-way hash to store in the database. Finally, you learned to use bcrypt.compare() to safely validate a user’s credentials during a login attempt.


References

Additional Resources