CSE 340: Web Backend Development

W05 Learning Activity: Hashing and User Registration

Overview

In this activity, you will learn how to create a user registration system that stores passwords securely. You will build a registration form and then save user information to a database. Instead of storing passwords directly, you will use a hashing process to protect user passwords. This is an important security practice that protects user data even if your database is accessed by unauthorized people.

Understanding the difference between hashing and encryption will help you make better security decisions in your web applications. This activity will give you practical experience implementing secure password storage.

Preparation Material

Hashing vs. Encryption: Understanding the Fundamental Difference

In cybersecurity and data protection, two critical processes often get confused: hashing and encryption. While both transform data into different forms, they serve completely different purposes and have fundamentally different characteristics. Understanding this difference is essential for making informed decisions about data security.

The Lock Box Analogy

Imagine you have two different ways to protect a valuable document:

Encryption: The Secure Lock Box

Encryption is like placing your document in a high-security lock box. You use a key to lock it, and anyone with the correct key can unlock it and retrieve the original document, completely unchanged. The document goes in whole, gets scrambled while locked away, but comes out exactly as it went in when unlocked.

The crucial point: the process is reversible. With the right key, you can always get your original document back in perfect condition.

Hashing: The Paper Shredder with a Unique Serial Number

Hashing is like putting your document through a special paper shredder that creates a unique serial number based on the document's contents. No matter how long your document is — whether it is a single page or a thousand-page book — the shredder always produces a serial number of exactly the same length.

The crucial point: the process is irreversible. Once shredded, you cannot reconstruct the original document from the serial number. However, if you shred the same document again, you will always get the identical serial number.

The Mathematical Impossibility

Consider this example: a 1GB file (approximately 8 billion bits of information) being hashed to produce a 256-bit hash. This demonstrates a fundamental mathematical principle called the pigeonhole principle. The pigeonhole principle states that if you have more items than containers, at least one container must hold more than one item.

Think of it this way: you are trying to fit the contents of an entire library into a single sentence. That sentence might uniquely identify the library's contents, but you cannot reconstruct thousands of books from a single sentence. The information simply is not there.

A 256-bit hash can represent only 2256 possible values, while the original data might have far more possible combinations. Multiple different inputs will inevitably produce the same hash value — these are called hash collisions. However, good hashing algorithms make finding these collisions computationally infeasible. This means it would take an extremely long time, even with powerful computers, to find two different inputs that produce the same hash.

Why This Matters in Practice

This irreversibility is not a limitation — it is a feature. Hashing allows you to verify data integrity and authenticate information without ever storing or transmitting the original sensitive data.

Real-World Applications

Password Storage: Why Hashing Wins

When you create an account on a website, that site should never store your actual password. Instead, it stores a hash of your password. Here is why this approach is superior:

If the website's database gets breached, attackers find only hash values, not actual passwords. They cannot reverse the hash to discover your password. When you log in, the site hashes the password you enter and compares it to the stored hash. If they match, you are authenticated.

Consider what would happen if passwords were encrypted instead: if attackers obtained both the encrypted passwords and the encryption key (which must be stored somewhere for the system to decrypt passwords), they could decrypt everyone's passwords. This is why proper password storage always uses hashing, never encryption.

Data Encryption for Transmission

When you shop online, your credit card information gets encrypted during transmission. The website needs to decrypt this information to process your payment, so encryption (which is reversible) is the appropriate choice. The site needs your actual credit card number, not just a hash of it.

Key Characteristics Compared

Encryption Characteristics
Hashing Characteristics

Common Algorithms in Practice

Popular Hashing Algorithms

SHA-256 (Secure Hash Algorithm) is widely used and produces 256-bit hash values. MD5, while faster, is considered cryptographically broken for security purposes but might still be used for non-security applications like checksums.

For password hashing specifically, algorithms like bcrypt, scrypt, and Argon2 are preferred because they are designed to be slow, making brute-force attacks more difficult.

Popular Encryption Algorithms

AES (Advanced Encryption Standard) is the current standard for symmetric encryption, where the same key encrypts and decrypts data. RSA is commonly used for asymmetric encryption, where different keys handle encryption and decryption.

Choosing the Right Tool

The choice between hashing and encryption depends entirely on your goal:

Use hashing when: You need to verify data integrity, store passwords securely, create digital fingerprints, or confirm that data has not been tampered with. Remember: you should never need to recover the original data.

Use encryption when: You need to protect data confidentiality but still access the original data later. This includes secure communication, database protection, and file storage where you need to retrieve the actual content.

A Critical Security Principle

Never use encryption where hashing is appropriate. If your system encrypts passwords, it means someone could potentially decrypt them. If your system hashes passwords properly, even a complete database breach cannot expose actual passwords.

Activity Instructions

In this activity you will create a register user page that will hash the users password and then store the user in the database.

Install the bcrypt Package

Complete the following:

  1. Open your terminal or command prompt.
  2. Navigate to your project directory.
  3. Install the bcrypt package by running: npm install bcrypt

Create the User Model

Complete the following:

  1. In your src/models folder, create a new file named users.js .
  2. In your src/models/users.js file, create a function to insert a new user into the database.
  3. This function should accept name, email, and password hash as parameters. It should assign the new user to the "user" role.
  4. Export the function so it can be used in other parts of your application.
Sample Code (click to expand)
import db from './db.js'

const createUser = async (name, email, passwordHash) => {
    const default_role = 'user';
    const query = `
        INSERT INTO users (name, email, password_hash, role_id) 
        VALUES ($1, $2, $3, (SELECT role_id FROM roles WHERE role_name = $4)) 
        RETURNING user_id
    `;
    const query_params = [name, email, passwordHash, default_role];
    
    const result = await db.query(query, query_params);

    if (result.rows.length === 0) {
        throw new Error('Failed to create user');
    }

    if (process.env.ENABLE_SQL_LOGGING === 'true') {
        console.log('Created new user with ID:', result.rows[0].user_id);
    }

    return result.rows[0].user_id;
};

export { createUser };

Create the Registration Controller

In this step you will use the bcrypt library to hash user passwords before storing them in the database.

The bcrypt library provides two main functions: hash() for creating password hashes and compare() for verifying passwords (which you will use in a future login assignment).

To hash a password, you call bcrypt.hash(password, saltRounds). The salt rounds parameter (typically 10) determines how many times the hashing algorithm runs. Higher numbers are more secure but slower. The function returns a promise that resolves to the hashed password string.

Here is an example of basic usage:

import bcrypt from 'bcrypt';
const saltRounds = 10;
const password = 'user-password';
const passwordHash = await bcrypt.hash(password, saltRounds);

// The hash looks like: $2b$10$N9qo8uLOickgx2ZMRZoMye...
console.log(passwordHash);

Complete the following:

  1. In your src/controllers folder, create a new file named users.js .
  2. Import the bcrypt library using import bcrypt from 'bcrypt';.
  3. Import the createUser function from the user model using import { createUser } from '../models/users.js';.
  4. Create a showUserRegistrationForm controller function that renders the registration form view register (which you will create in a future step).
  5. Create a processUserRegistrationForm controller function to handle the registration logic of creating the new user, including hashing the password and saving the user.
Sample Code (click to expand)
import bcrypt from 'bcrypt';
import { createUser } from '../models/users.js';

const showUserRegistrationForm = (req, res) => {
    res.render('register', { title: 'Register' });
};

const processUserRegistrationForm = async (req, res) => {
    const { name, email, password } = req.body;

    try {
        // Hash the password before storing it
        const salt = await bcrypt.genSalt(10);
        const passwordHash = await bcrypt.hash(password, salt);

        // Create the user in the database
        const userId = await createUser(name, email, passwordHash);

        // Redirect to the home page after successful registration
        req.flash('success', 'Registration successful! Please log in.');
        res.redirect('/');
    } catch (error) {
        console.error('Error registering user:', error);
        req.flash('error', 'An error occurred during registration. Please try again.');
        res.redirect('/register');
    }
};

export { showUserRegistrationForm, processUserRegistrationForm };

Create the Registration Routes

Complete the following:

  1. Open your routes file src/controllers/routes.js .
  2. Import the user controller at the top of the file.
  3. Create a GET route for /register to call the showUserRegistrationForm controller function.
  4. Create a POST route for /register to call the processUserRegistrationForm controller function.
Sample Code (click to expand)
// User registration routes
router.get('/register', showUserRegistrationForm);
router.post('/register', processUserRegistrationForm);

Create the Registration Form View

Complete the following:

  1. In your src/views folder, create a new file called register.ejs .
  2. Add a form with a field for name, email, and password.
  3. Set the form method to POST and the action to /register.
  4. Add a submit button with appropriate text.
  5. Include basic validation attributes such as required on each input field, and email format validation on the email field.
Sample Code (click to expand)
<%- include('partials/header') %>
<main>
    <h1><%= title %></h1>

    <form action="/register" method="POST">
        <div class="form-group">
            <label for="name">Name</label>
            <input
                type="text"
                id="name"
                name="name"
                required
            />
        </div>
        
        <div class="form-group">
            <label for="email">Email (Username)</label>
            <input
                type="email"
                id="email"
                name="email"
                required
            />
        </div>

        <div class="form-group">
            <label for="password">Password</label>
            <input
                type="password"
                id="password"
                name="password"
                required
            />
        </div>

        <button type="submit">Register</button>
    </form>

</main>
<%- include('partials/footer') %>

Add a Link to your new registration page

Complete the following:

  1. Open your header partial view file src/views/partials/header.ejs .
  2. Add a link to the registration page in the navigation menu.
Sample Code (click to expand)

    <nav>
        <ul>
            <li><a href="/">Home</a></li>
            <li><a href="/organizations">Organizationss</a></li>
            <li><a href="/projects">Projects</a></li>
            <li><a href="/categories">Categories</a></li>
            <li><a href="/register">Register</a></li>
        </ul>
    </nav>

Test Your Registration System

Complete the following:

  1. Start your server and navigate to the registration page in your browser.
  2. Fill out the registration form with test data.
  3. Submit the form and verify that you are redirected correctly.
  4. Check your database to confirm the user was created with a hashed password.
  5. Verify that the password in the database is not readable (it should look like a random string of characters).
  6. Try registering with the same email again to test your error handling.

Testing Reminder

Always test your application with various inputs, including invalid data, to make sure your error handling works correctly. Try leaving fields empty, using invalid email formats, and attempting to create duplicate accounts.

Next Step

Complete the other Week 05 Learning Activities

After you have completed all the learning activities for this lesson, return to Canvas to submit a quiz.

Other Links: