Security is the most critical part of any backend. Authentication verifies WHO a user is, while Authorization verifies WHAT they are allowed to do. Let's explore how modern systems handle both securely.
Step 1 — The Fundamentals
Step 2 — Hashing Passwords securely
You must NEVER store plain-text passwords in your database. If the DB is compromised, hackers have your users' passwords. Instead, use a cryptographic hash function like bcrypt.
const bcrypt = require('bcrypt');
// Registration (Hashing)
async function registerUser(email, plainTextPassword) {
const saltRounds = 10; // Controls hashing complexity
const hashedPassword = await bcrypt.hash(plainTextPassword, saltRounds);
// Store email and hashedPassword in database
await db.users.insert({ email, password: hashedPassword });
}
// Login (Verifying)
async function loginUser(email, plainTextPassword) {
const user = await db.users.findByEmail(email);
if (!user) return false;
// bcrypt.compare hashes the provided password and compares it to the stored hash
const match = await bcrypt.compare(plainTextPassword, user.password);
return match; // true if success
}Step 3 — Session-based Authentication (Stateful)
In traditional auth, the server creates a 'session' in its memory or database when a user logs in, and gives the browser a Cookie containing the Session ID.
Pros & Cons of Sessions
- Pros: Highly secure. Easy to instantly revoke access (just delete the session from the DB).
- Cons: Requires server storage (Redis is often used). Harder to scale across multiple microservices.
Step 4 — Token-based Auth with JWT (Stateless)
JSON Web Tokens (JWT) are signed tokens that contain the user's data payload directly. The server doesn't need to store a session; it just cryptographically verifies the token's signature.
const jwt = require('jsonwebtoken');
const SECRET_KEY = process.env.JWT_SECRET;
// 1. Generate Token (On Login)
const token = jwt.sign(
{ userId: 123, role: 'admin' }, // Payload
SECRET_KEY,
{ expiresIn: '1h' }
);
// 2. Verify Token (Middleware for protected routes)
function authenticateToken(req, res, next) {
// Token is usually sent in the Authorization header: "Bearer <token>"
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.sendStatus(401);
jwt.verify(token, SECRET_KEY, (err, userPayload) => {
if (err) return res.sendStatus(403); // Token invalid or expired
req.user = userPayload;
next();
});
}Step 5 — Role-Based Access Control (RBAC)
Once authenticated, you need Authorization. RBAC assigns roles to users, and grants permissions to roles.
// Authorization Middleware
function requireRole(role) {
return (req, res, next) => {
// req.user was populated by authenticateToken middleware
if (req.user && req.user.role === role) {
next(); // Authorized!
} else {
res.status(403).json({ error: 'Access denied. Insufficient permissions.' });
}
};
}
// Using multiple middlewares on a route
app.delete('/api/users/:id', authenticateToken, requireRole('admin'), (req, res) => {
// Only logged-in Admins reach this code
db.users.delete(req.params.id);
res.sendStatus(204);
});Step 6 — OAuth 2.0 (Social Login)
OAuth is an authorization protocol that lets users grant your app access to their data on another service (like Google or GitHub) without giving you their password. It's the engine behind "Log in with Google".
- User clicks 'Log in with Google' and is redirected to Google's auth page.
- User logs into Google and consents to share their profile.
- Google redirects back to your server with an Authorization Code.
- Your server securely swaps that Code for an Access Token from Google.
- Your server uses the Access Token to fetch the user's email, creates an account in your DB, and logs them in via JWT/Session.