🔐 User Sign In with Password

Authentication is the gateway to your API. In this lesson, we'll implement secure user sign-in using password verification with bcrypt and JWT token generation for maintaining authenticated sessions.


The Sign-In Flow

<aside> 🔄

Authentication Flow:

  1. User provides email and password
  2. Find user by email in database
  3. Compare provided password with stored hash
  4. If valid, generate JWT token
  5. Return token for future requests </aside>

Understanding Password Verification

How Bcrypt Compare Works

Bcrypt's compare function is fascinating - it can verify a password against a hash without knowing the original password:

// During signup (previous lesson)
const hash = await bcrypt.hash("myPassword123", 10)
// Stored: $2b$10$N9qo8uLOickgx2ZMRZoMye.IjQ0JwF2cJX8nnYA5VYA.KQIeqHLWa

// During login (this lesson)
const isValid = await [bcrypt.compare](<http://bcrypt.compare>)("myPassword123", hash)
// Returns: true if password matches, false otherwise

<aside> 🔍

Anatomy of a Bcrypt Hash

Let's break down a typical bcrypt hash string: $2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

This string is made of three distinct parts:

  1. Parameters: $2b$10$
  2. Salt: N9qo8uLOickgx2ZMRZoMye (the next 22 characters)
  3. Hashed Password: IjZAgcfl7p92ldGxad68LJZdL17lhWy (the remaining characters)

Timing Attack Protection

Bcrypt's compare is constant-time, meaning it takes the same amount of time regardless of how many characters match:

// Both take the same time to return false
await [bcrypt.compare](<http://bcrypt.compare>)("a", hash)        // Wrong from first character
await [bcrypt.compare](<http://bcrypt.compare>)("myPassword12", hash) // Wrong at last character

This prevents attackers from using timing differences to guess passwords character by character.


Implementing Sign-In

The Login Controller

<aside> 📝

File: src/controllers/authController.ts

</aside>

import type { Request, Response } from 'express'
import bcrypt from 'bcrypt'
import { generateToken } from '../utils/jwt.ts'
import { db } from '../db/connection.ts'
import { users } from '../db/schema.ts'
import { eq } from 'drizzle-orm'

export const login = async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body

    // Step 1: Find user by email
    const [user] = await [db.select](<http://db.select>)().from(users).where(eq([users.email](<http://users.email>), email))

    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' })
    }

    // Step 2: Verify password
    const isValidPassword = await [bcrypt.compare](<http://bcrypt.compare>)(password, user.password)

    if (!isValidPassword) {
      return res.status(401).json({ error: 'Invalid credentials' })
    }

    // Step 3: Generate JWT token
    const token = await generateToken({
      id: [user.id](<http://user.id>),
      email: [user.email](<http://user.email>),
      username: user.username,
    })

    // Step 4: Return user data and token
    res.json({
      message: 'Login successful',
      user: {
        id: [user.id](<http://user.id>),
        email: [user.email](<http://user.email>),
        username: user.username,
        firstName: user.firstName,
        lastName: user.lastName,
      },
      token,
    })
  } catch (error) {
    console.error('Login error:', error)
    res.status(500).json({ error: 'Failed to login' })
  }
}