Password Hashing with bcrypt
Never store passwords in plain text. The bcrypt library hashes passwords with a salt, making them extremely difficult to crack even if your database is compromised.
bcrypt uses a cost factor (salt rounds) that controls how computationally expensive the hashing is. Higher rounds mean more security but slower hashing. A value of 10-12 is standard for most applications.
// npm install bcrypt jsonwebtoken
const bcrypt = require('bcrypt');
// Registration: Hash the password before storing
async function registerUser(name, email, password) {
// Hash password with 10 salt rounds
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Save to database (password is now hashed)
const user = {
name,
email,
password: hashedPassword // $2b$10$X7Q3...
};
// Save user to DB...
return user;
}
// Login: Compare submitted password with stored hash
async function loginUser(email, password) {
// Find user in database
const user = await findUserByEmail(email);
if (!user) {
throw new Error('Invalid email or password');
}
// Compare password with hash
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('Invalid email or password');
}
return user;
} - bcrypt.hash(password, rounds) — Hash a password with salt rounds
- bcrypt.compare(password, hash) — Compare a password to a hash
- Salt rounds — Number of hashing iterations (10-12 recommended)
- Never store plain text passwords — always hash first
- Use the same error message for wrong email and wrong password (security)
- Use the same error message ('Invalid email or password') for both wrong email and wrong password. This prevents attackers from discovering valid email addresses.
JWT Authentication and Route Protection
JWT (JSON Web Token) is the most common authentication method for REST APIs. After login, the server creates a token containing user data and signs it with a secret key. The client sends this token with every request to prove their identity.
JWTs are stateless — the server does not need to store sessions. The token itself contains all the information needed to verify the user.
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const express = require('express');
const app = express();
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Generate a token
function generateToken(user) {
return jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '24h' }
);
}
// Login route
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await findUserByEmail(email); // DB lookup
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = generateToken(user);
res.json({ token, user: { id: user.id, name: user.name } });
} catch (err) {
res.status(500).json({ error: 'Server error' });
}
});
// Auth middleware — protects routes
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded; // Attach user data to request
next();
} catch (err) {
res.status(401).json({ error: 'Invalid or expired token' });
}
}
// Protected route
app.get('/api/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
// Admin-only middleware
function requireAdmin(req, res, next) {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
app.get('/api/admin/users', authenticate, requireAdmin, (req, res) => {
res.json({ message: 'Admin dashboard' });
}); - Always store your JWT secret in an environment variable, never hardcode it. The token should be sent in the Authorization header as 'Bearer
'.
