Back to List

Implementing Multi-User Authentication

Learn step-by-step how to implement registration, password hashing (bcrypt), and role-based access control.

Intermediate
|
12min
|
Verified (2026-07)
multi-userregistrationbcryptpassword hashingauthentication implementation
Progress0/55 (0%)

Implementing Multi-User Authentication

After completing this topic:

You will be able to securely hash passwords during user registration, understand the principles of bcrypt, and implement role-based access control.


Why Storing Passwords in Plain Text is a Bad Idea

javascript
// NEVER do this
const users = [
  { username: 'alice', password: 'mySecret123' }
];

If the database is compromised, all users' passwords will be exposed. The 2012 LinkedIn hack and the 2013 Adobe hack exposed hundreds of millions of passwords stored in plain text (or with weak hashing).

Never store passwords in plain text. Store them using hashing.


What is Hashing?

Hashing is a one-way transformation. It's possible to go from the original to the hash, but not from the hash back to the original.

text
"mySecret123" β†’ hashing β†’ "$2b$10$X7kG..."  (possible)
"$2b$10$X7kG..." β†’ ???  β†’ "mySecret123"   (impossible)

During login, the entered password is hashed and compared to the stored hash.

text
User input: "mySecret123"
Stored hash: "$2b$10$X7kG..."

"mySecret123" β†’ hashing β†’ "$2b$10$X7kG..."  (match β†’ authentication successful)
"wrongPass"   β†’ hashing β†’ "$2b$10$Qm1Y..."  (no match β†’ authentication failed)

bcrypt β€” The Standard for Password Hashing

bash
npm install bcrypt

Why bcrypt is better than general hash functions like SHA-256 for passwords:

  1. Designed to be slow: Prevents hackers from trying billions of hashes per second.
  2. Automatically includes salt: Generates a different hash each time, even for the same password.
  3. Configurable cost: Adjust the speed/security trade-off with the rounds value.
javascript
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;

// Hashing (during registration)
const hash = await bcrypt.hash('mySecret123', SALT_ROUNDS);
console.log(hash);
// $2b$10$X7kG.fDlUqVnPq5RvWEYq.ZM3Y4Fs8qD1vSrKLhwEcFjnp2fW9Xm.

// Comparison (during login)
const isMatch = await bcrypt.compare('mySecret123', hash);
console.log(isMatch);  // true

const isWrong = await bcrypt.compare('wrongPass', hash);
console.log(isWrong);  // false

SALT_ROUNDS = 10 means 2^10 = 1,024 iterations. A higher value is more secure but slower. 10-12 is the current recommended range.


Implementing Registration

javascript
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());

const users = [];  // In a real application, use a database.

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

  // Input validation
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password required' });
  }
  if (password.length < 8) {
    return res.status(400).json({ error: 'Password must be at least 8 characters' });
  }

  // Check for duplicates
  if (users.find(u => u.username === username)) {
    return res.status(409).json({ error: 'Username already exists' });
  }

  // Hash the password
  const hashedPassword = await bcrypt.hash(password, 10);

  // Store the user
  const newUser = {
    id: users.length + 1,
    username,
    password: hashedPassword,
    displayName: displayName || username,
    role: 'user',
    createdAt: new Date().toISOString()
  };
  users.push(newUser);

  res.status(201).json({
    message: 'Registration successful',
    user: { id: newUser.id, username: newUser.username }
  });
});

Never include the password (including the hash) in the response.


Integrating with Passport

javascript
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy(
  async (username, password, done) => {
    const user = users.find(u => u.username === username);
    if (!user) {
      return done(null, false, { message: 'User not found' });
    }

    // Compare using bcrypt
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) {
      return done(null, false, { message: 'Wrong password' });
    }

    return done(null, user);
  }
));

passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => {
  const user = users.find(u => u.id === id);
  done(null, user);
});

The only difference from the Strategy in the previous topic is using bcrypt.compare() instead of user.password !== password.


Role-Based Access Control (RBAC)

javascript
function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.isAuthenticated()) {
      return res.status(401).json({ error: 'Login required' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// All logged-in users
app.get('/profile', requireRole('user', 'admin'), (req, res) => {
  res.json({ user: req.user.displayName });
});

// Only administrators
app.get('/admin/users', requireRole('admin'), (req, res) => {
  const safeUsers = users.map(u => ({
    id: u.id, username: u.username, role: u.role
  }));
  res.json(safeUsers);
});

// Administrator - change role
app.patch('/admin/users/:id/role', requireRole('admin'), (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'User not found' });

  user.role = req.body.role;
  res.json({ message: `${user.username} role changed to ${user.role}` });
});

requireRole(...roles) is a higher-order function (a function that returns a function). It takes the allowed roles as arguments and creates a middleware that returns 403 if the user does not have the required role.


Security Checklist

ItemDescription
Password Hashingbcrypt, SALT_ROUNDS 10+
Minimum Length8 characters or more (NIST recommendation)
Do not include hash in responseNever include the password field (including the hash) in the API response
Generalize error messages"Invalid username or password" (do not indicate which one is incorrect)
Rate limitingLimit the number of login attempts (prevent brute force)
HTTPSEnsure that passwords are not transmitted in plain text over the network

The reason for generalizing error messages: if you say "username not found", an attacker can determine the existence of accounts. This is called a user enumeration attack.


Real-World Pattern β€” Account Deletion

javascript
app.delete('/account', requireRole('user', 'admin'), async (req, res) => {
  const valid = await bcrypt.compare(req.body.password, req.user.password);
  if (!valid) {
    return res.status(401).json({ error: 'Password confirmation failed' });
  }

  const index = users.findIndex(u => u.id === req.user.id);
  users.splice(index, 1);

  req.logout((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.json({ message: 'Account deleted' });
  });
});

Account deletion is a risky operation, so it requires password re-confirmation. After deletion, be sure to clear the session with req.logout().


Key Takeaways

ConceptSummary
HashingOne-way transformation. Possible to go from original to hash, but not from hash to original
bcryptPassword-specific hash. Slow and includes salt
SaltRandom value that generates a different hash, even for the same password
RBACRole-based access control
bcrypt.hash()Hashing password during registration
bcrypt.compare()Comparing input with stored hash during login

Authentication is about more than just "is the password correct"; it's about how to handle passwords securely. Storing passwords in plain text is a disaster, weak hashes (MD5, SHA-1) are meaningless, and bcrypt is the current standard.

πŸ’¬ Questions & Comments

0 comments

You can post without signing in. Guest comments cannot be edited or deleted by their author.

0/2000

Loading...