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
- Reversible: With the correct key, you can always recover the original data
- Variable Output Size: Encrypted data is usually similar in size to the original data
- Key Dependent: Requires keys for both encryption and decryption processes
- Purpose: Protects data confidentiality during storage or transmission
Hashing Characteristics
- Irreversible: Cannot recover original data from the hash value
- Fixed Output Size: Always produces the same length output regardless of input size
- No Keys Required: The same input always produces the same hash
- Purpose: Verifies data integrity and enables secure authentication
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:
- Open your terminal or command prompt.
- Navigate to your project directory.
- Install the bcrypt package by running:
npm install bcrypt
Create the User Model
Complete the following:
- In your
src/modelsfolder, create a new file namedusers.js. - In your
src/models/users.jsfile, create a function to insert a new user into the database. - This function should accept name, email, and password hash as parameters. It should assign the new user to the "user" role.
- 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:
- In your
src/controllersfolder, create a new file namedusers.js. - Import the bcrypt library using
import bcrypt from 'bcrypt';. - Import the createUser function from the user model using
import { createUser } from '../models/users.js';. - Create a
showUserRegistrationFormcontroller function that renders the registration form viewregister(which you will create in a future step). - Create a
processUserRegistrationFormcontroller 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:
- Open your routes file
src/controllers/routes.js. - Import the user controller at the top of the file.
- Create a GET route for
/registerto call theshowUserRegistrationFormcontroller function. - Create a POST route for
/registerto call theprocessUserRegistrationFormcontroller 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:
- In your
src/viewsfolder, create a new file calledregister.ejs. - Add a form with a field for name, email, and password.
- Set the form method to POST and the action to
/register. - Add a submit button with appropriate text.
- Include basic validation attributes such as
requiredon 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:
- Open your header partial view file
src/views/partials/header.ejs. - 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:
- Start your server and navigate to the registration page in your browser.
- Fill out the registration form with test data.
- Submit the form and verify that you are redirected correctly.
- Check your database to confirm the user was created with a hashed password.
- Verify that the password in the database is not readable (it should look like a random string of characters).
- 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:
- Return to: Week Overview | Course Home