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.
<aside> 🔄
Authentication Flow:
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> 🔍
Let's break down a typical bcrypt hash string:
$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
This string is made of three distinct parts:
$2b$10$
$2b$: This is the bcrypt algorithm version.$10$: This is the cost factor, which determines how computationally "expensive" (and thus, how slow) the hashing is. A higher number means more security against brute-force attacks.N9qo8uLOickgx2ZMRZoMye (the next 22 characters)
IjZAgcfl7p92ldGxad68LJZdL17lhWy (the remaining characters)
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.
<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' })
}
}