Lesson 45 of 50 – Password Security
90% Completed
90%

Node.js Password Security

Learn how to securely store passwords, hash passwords using bcrypt, validate passwords, and protect user accounts in Node.js applications.

Important: Never store user passwords as plain text. Always use a suitable password-hashing algorithm such as Argon2id or bcrypt.

1. What Is Password Security?

Password security means protecting users' passwords from unauthorized access. If a database is leaked, securely hashed passwords are much harder to recover than plain-text passwords.

Example: Plain-Text Password
{
    "username": "rahul",
    "password": "rahul123"
}
This is unsafe because anyone who can read the database can directly see the password.
Example: Hashed Password
{
    "username": "rahul",
    "password": "$2b$12$exampleHashValue..."
}

A real bcrypt hash includes algorithm and cost information. The example above is illustrative, not a valid hash.

2. What Is Password Hashing?

Password hashing converts a password into a derived value using a one-way password-hashing algorithm. The original password should not be recoverable from the stored hash.

Password
   |
   v
Password Hashing Algorithm
   |
   v
Password Hash Stored in Database
Hashing is different from encryption. Encryption is designed to be reversible with a key; password hashing is designed for secure password verification.

3. Install bcryptjs

In this lesson, we will use bcryptjs to hash and verify passwords. Argon2id is also a strong option for new applications.

Terminal Command
npm init -y
npm install bcryptjs

4. Hash a Password in Node.js

Create a file named hash_password.js.

Code Example
const bcrypt = require('bcryptjs');

async function hashPassword() {
    const password = 'Rahul@123';

    const saltRounds = 12;

    const hashedPassword = await bcrypt.hash(
        password,
        saltRounds
    );

    console.log('Original Password:', password);
    console.log('Hashed Password:', hashedPassword);
}

hashPassword().catch(console.error);

Output

Original Password: Rahul@123
Hashed Password: $2b$12$...

The actual hash will be longer and will differ between executions because bcrypt generates a random salt.

Remember: Store the hash in your database, not the original password. Do not print real passwords in production logs.

5. Verify a Password

When a user logs in, compare the submitted password with the stored hash using bcrypt.compare(). Never compare the submitted password directly with the hash as a string.

const bcrypt = require('bcryptjs');

async function verifyPassword() {
    const password = 'Rahul@123';

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

    const enteredPassword = 'Rahul@123';

    const isMatch = await bcrypt.compare(
        enteredPassword,
        storedHash
    );

    if (isMatch) {
        console.log('Password is correct');
    } else {
        console.log('Invalid password');
    }
}

verifyPassword().catch(console.error);

Output

Password is correct

6. Create a Simple Registration and Login Example

The following Express example demonstrates registration and login using bcrypt. It stores users in memory for learning purposes only. Users disappear when the server restarts.

This example is not production-ready. A real application needs a database, unique account identifiers, rate limiting, secure session or token handling, and appropriate validation.

Step 1: Install Required Packages

npm install express bcryptjs

Step 2: Create password_app.js

const express = require('express');
const bcrypt = require('bcryptjs');

const app = express();

app.use(express.json());

// In-memory storage for learning only
const users = [];

app.post('/register', async (req, res) => {
    try {
        const { username, password } = req.body;

        if (
            typeof username !== 'string' ||
            typeof password !== 'string' ||
            !username.trim() ||
            password.length < 12
        ) {
            return res.status(400).json({
                message: 'Enter a username and a password of at least 12 characters.'
            });
        }

        const normalizedUsername = username.trim().toLowerCase();

        const existingUser = users.find(
            user => user.username === normalizedUsername
        );

        if (existingUser) {
            return res.status(409).json({
                message: 'Unable to create this account.'
            });
        }

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

        users.push({
            username: normalizedUsername,
            password: hashedPassword
        });

        res.status(201).json({
            message: 'Registration successful'
        });
    } catch (error) {
        console.error('Registration error');
        res.status(500).json({
            message: 'Something went wrong'
        });
    }
});

app.post('/login', async (req, res) => {
    try {
        const { username, password } = req.body;

        if (
            typeof username !== 'string' ||
            typeof password !== 'string'
        ) {
            return res.status(400).json({
                message: 'Invalid username or password'
            });
        }

        const normalizedUsername = username.trim().toLowerCase();

        const user = users.find(
            user => user.username === normalizedUsername
        );

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

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

        if (!isMatch) {
            return res.status(401).json({
                message: 'Invalid username or password'
            });
        }

        res.json({
            message: 'Login successful'
        });
    } catch (error) {
        console.error('Login error');
        res.status(500).json({
            message: 'Something went wrong'
        });
    }
});

app.listen(3000, () => {
    console.log('Server running at http://localhost:3000');
});

Step 3: Run the Application

node password_app.js

Step 4: Test Registration

Send a POST request to http://localhost:3000/register with a JSON body:

{
    "username": "rahul",
    "password": "MyStrongPassword@123"
}

Successful response:

{
    "message": "Registration successful"
}

Step 5: Test Login

Send a POST request to http://localhost:3000/login with the same username and password.

{
    "username": "rahul",
    "password": "MyStrongPassword@123"
}

Successful response:

{
    "message": "Login successful"
}
You can test these requests using Postman or another API client. For a production app, use a persistent database and enforce a unique constraint on the normalized username.

7. Important Password Security Rules

Rule Explanation
Hash passwords Use Argon2id or bcrypt with an appropriate work factor.
Never store plain text Save only the password hash.
Use unique salts Modern password-hashing libraries generate salts automatically.
Use HTTPS Protect passwords while they travel between the client and server.
Validate inputs Check types, lengths, and reasonable input limits.
Limit login attempts Use rate limiting and monitoring to reduce brute-force attacks.
Use generic login errors Avoid revealing whether a username or email exists.
Keep secrets private Do not commit database credentials or signing secrets to source control.
Support secure recovery Use short-lived, single-use password reset tokens.

8. Why Should You Avoid MD5 and SHA-256 for Passwords?

MD5 and ordinary SHA-256 hashing are designed to be fast. That makes them unsuitable for directly storing passwords because attackers can test many guesses quickly.

// Do not use this for password storage:
const crypto = require('crypto');

const hash = crypto
    .createHash('sha256')
    .update('Rahul@123')
    .digest('hex');
Do not use plain MD5 or SHA-256 for password storage, even if you add a simple salt. Prefer a dedicated password-hashing algorithm such as Argon2id or bcrypt.

9. Common Password Security Mistakes

Avoid These Mistakes
  • Saving passwords directly in the database.
  • Using the same hardcoded password for every user.
  • Logging passwords or authentication secrets.
  • Using fast general-purpose hashes for password storage.
  • Allowing unlimited login attempts.
  • Sending passwords over unencrypted HTTP.
  • Keeping reset tokens valid indefinitely.
  • Returning detailed account-existence errors during login.

10. Practical Assignment

Assignment: Secure User Registration

Complete the following tasks:

  1. Create a Node.js project and install Express and bcryptjs.
  2. Create a POST /register route.
  3. Validate the username and password.
  4. Hash the password before storing it.
  5. Create a POST /login route.
  6. Verify the password using bcrypt.compare().
  7. Return an error for invalid credentials.
  8. Explain why plain-text passwords are unsafe.

11. Summary

  • Password hashing protects stored passwords.
  • Argon2id and bcrypt are designed for password hashing.
  • bcrypt.hash() creates a password hash.
  • bcrypt.compare() verifies a submitted password.
  • Never store or log plain-text passwords.
  • Use HTTPS, input validation, rate limiting, and secure recovery flows.

Test Your Knowledge – Password Security

Choose the correct answer for each question.

1. Which function hashes a password using bcryptjs?

2. Which function checks a password against a stored bcrypt hash?

3. Which is the safest password-storage approach?