01How The Internet Works 02Your First Node.js Server 03HTTP Methods and JSON 04Express.js Framework 05Building REST APIs 06MongoDB Fundamentals 07Mongoose ODM 08Data Modeling 09Authentication Basics 10JWT Implementation

Settings

Theme
Sand
Cloud
Midnight
Forest
Sunset
Purple
Ocean
Crimson
Font
Merriweather
Inter
JetBrains
Space Grotesk
Fira Code
Playfair
Font Size
100%
Bookmark
No bookmark set
Day 9

Authentication Basics

Security is paramount. Today you learn to register users safely, hash passwords with bcrypt, and implement secure login systems that protect user credentials.

Why Password Security Matters

Storing plain text passwords is catastrophic. When databases get breached (and they do), attackers gain immediate access to all user accounts. Password hashing makes stolen data useless.

Security Rule

Never store plain text passwords. Never. Ever. Always hash passwords before storing them. This is non-negotiable in professional development.

Understanding Bcrypt

Bcrypt is a password hashing algorithm designed to be slow and computationally expensive, making brute-force attacks impractical:

How Bcrypt Works

  • Salting - Adds random data to each password before hashing, preventing rainbow table attacks
  • Cost Factor - Controls how slow the hashing is (higher = more secure but slower)
  • One-Way - Cannot reverse a hash back to the original password
  • Built-in Salt - Salt is stored with the hash, no separate storage needed
Terminal
npm install bcrypt

Hashing Passwords

Basic Bcrypt Usage
const bcrypt = require('bcrypt');

// Salt rounds (cost factor) - 10-12 is recommended
// Higher = more secure but slower
const SALT_ROUNDS = 10;

// Hash a password (async recommended)
async function hashPassword(plainPassword) {
  // bcrypt.hash generates salt and hashes in one step
  const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);
  return hashedPassword;
}

// Example output (will be different each time due to random salt):
// $2b$10$N9qo8uLOickgx2ZMRZoMye.IjqR5rKP5VrVYJE4JkFd6HQG3N9O.i
// ^   ^  ^                      ^
// |   |  |                      |
// |   |  22-char salt           31-char hash
// |   cost factor (10)
// bcrypt identifier

// Verify a password against stored hash
async function verifyPassword(plainPassword, hashedPassword) {
  // bcrypt.compare extracts salt from hash automatically
  const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
  return isMatch;  // true if password matches, false otherwise
}

User Model with Password Hashing

models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  // Email as unique identifier for login
  email: {
    type: String,
    required: [true, 'Email is required'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
  },
  // Password with minimum length requirement
  password: {
    type: String,
    required: [true, 'Password is required'],
    minlength: [8, 'Password must be at least 8 characters'],
    select: false  // Never include password in queries by default
  },
  // Display name
  name: {
    type: String,
    required: [true, 'Name is required'],
    trim: true
  },
  // Account creation timestamp
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// Pre-save middleware to hash password before saving
userSchema.pre('save', async function(next) {
  // Only hash if password was modified (or new)
  // This prevents re-hashing on unrelated updates
  if (!this.isModified('password')) {
    return next();
  }

  try {
    // Hash password with cost factor 10
    this.password = await bcrypt.hash(this.password, 10);
    next();
  } catch (err) {
    next(err);
  }
});

// Instance method to compare passwords
userSchema.methods.comparePassword = async function(candidatePassword) {
  // 'this.password' is the hashed password from database
  return bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model('User', userSchema);

Registration Endpoint

routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/User');

// POST /api/auth/register - Create new user account
router.post('/register', async (req, res) => {
  try {
    // Extract registration data from request body
    const { email, password, name } = req.body;

    // Check if user with this email already exists
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(400).json({
        error: 'Email already registered'
      });
    }

    // Create new user - password is hashed automatically by pre-save middleware
    const user = await User.create({
      email,
      password,  // Plain password - will be hashed by middleware
      name
    });

    // Return success without exposing password
    res.status(201).json({
      message: 'User registered successfully',
      user: {
        id: user._id,
        email: user.email,
        name: user.name
      }
    });

  } catch (err) {
    // Handle validation errors
    if (err.name === 'ValidationError') {
      const messages = Object.values(err.errors).map(e => e.message);
      return res.status(400).json({ error: messages.join(', ') });
    }
    res.status(500).json({ error: 'Registration failed' });
  }
});

module.exports = router;

Login Endpoint

routes/auth.js (continued)
// POST /api/auth/login - Authenticate user
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    // Validate input exists
    if (!email || !password) {
      return res.status(400).json({
        error: 'Email and password are required'
      });
    }

    // Find user by email, explicitly include password field
    // (+password overrides select: false in schema)
    const user = await User.findOne({ email }).select('+password');

    // Check if user exists
    if (!user) {
      // Use generic message to prevent email enumeration
      return res.status(401).json({
        error: 'Invalid email or password'
      });
    }

    // Compare provided password with stored hash
    const isValidPassword = await user.comparePassword(password);

    if (!isValidPassword) {
      // Same generic message - don't reveal which field is wrong
      return res.status(401).json({
        error: 'Invalid email or password'
      });
    }

    // Login successful - return user info
    // In Day 10, we'll add JWT token here
    res.json({
      message: 'Login successful',
      user: {
        id: user._id,
        email: user.email,
        name: user.name
      }
    });

  } catch (err) {
    res.status(500).json({ error: 'Login failed' });
  }
});
Security Best Practice

Always return the same error message for invalid email AND invalid password. If you say "Email not found" vs "Wrong password", attackers can enumerate which emails exist in your system.

Complete Authentication Server

server.js
const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');

const app = express();
app.use(express.json());

// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/authapp')
  .then(() => console.log('MongoDB connected'))
  .catch(err => console.error(err));

// User Schema with password hashing
const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true, lowercase: true },
  password: { type: String, required: true, minlength: 8, select: false },
  name: { type: String, required: true },
  createdAt: { type: Date, default: Date.now }
});

// Hash password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// Compare password method
userSchema.methods.comparePassword = function(candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password);
};

const User = mongoose.model('User', userSchema);

// Register endpoint
app.post('/api/auth/register', async (req, res) => {
  try {
    const { email, password, name } = req.body;

    // Check existing user
    const exists = await User.findOne({ email });
    if (exists) {
      return res.status(400).json({ error: 'Email already registered' });
    }

    // Create user
    const user = await User.create({ email, password, name });

    res.status(201).json({
      message: 'Registration successful',
      user: { id: user._id, email: user.email, name: user.name }
    });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// Login endpoint
app.post('/api/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    // Find user with password included
    const user = await User.findOne({ email }).select('+password');

    // Verify user exists and password matches
    if (!user || !(await user.comparePassword(password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    res.json({
      message: 'Login successful',
      user: { id: user._id, email: user.email, name: user.name }
    });
  } catch (err) {
    res.status(500).json({ error: 'Login failed' });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

Video Resources

Knowledge Check

1. Why should you use the same error message for invalid email and invalid password?

AIt makes debugging easier
BIt improves user experience
CIt prevents attackers from discovering which emails exist
DIt reduces server load

2. What does `select: false` do in a Mongoose schema field?

AMakes the field read-only
BExcludes the field from query results by default
CPrevents the field from being saved
DMarks the field as optional

3. What does the salt in bcrypt protect against?

ARainbow table attacks and identical password detection
BSQL injection attacks
CCross-site scripting
DBrute force attacks
Practice Project

Build a Complete Auth System

Extend the authentication system with additional features:

  • Add password confirmation field during registration (password must match confirmPassword)
  • Implement password strength validation (must include uppercase, lowercase, number, special char)
  • Add a "forgot password" endpoint that generates a reset token
  • Create a profile endpoint that returns user data (excluding password)
  • Implement rate limiting on login attempts (max 5 per minute)