Security Fundamentals
Authentication & Authorization
Understand the difference between authentication and authorization, and implement both securely.
Authentication vs Authorization
Two concepts that are often confused:
Authentication — "Who are you?" Verifying the identity of a user. Username/password, tokens, biometrics.
Authorization — "What can you do?" Determining what an authenticated user is allowed to access. Roles, permissions, resource ownership.
Both are required. Authentication without authorization means anyone can access everything after logging in. Authorization without authentication means you don't know who you're authorizing.
Password Security
Never store passwords in plain text. Use a slow hashing algorithm designed for passwords:
import bcrypt from 'bcryptjs';
// When creating an account
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(plainTextPassword, saltRounds);
await db.users.create({ email, password: hashedPassword });
// When logging in
const isValid = await bcrypt.compare(plainTextPassword, storedHash);Why bcrypt? It is intentionally slow (configurable with salt rounds). An attacker who steals your database still needs years to crack each password. bcrypt also handles salting automatically — each hash is unique even for identical passwords.
JWT: Structure and Usage
JSON Web Tokens (JWTs) are the most common mechanism for stateless authentication in modern applications.
A JWT has three parts separated by dots:
eyJhbGciOiJIUzI1NiJ9 <- Header (algorithm)
.eyJ1c2VySWQiOiIxMjMifQ <- Payload (claims)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c <- SignatureCritical JWT rules:
- Always validate the signature on every request
- Always check the expiry (exp claim)
- Store in HttpOnly cookies, not localStorage (prevents XSS theft)
- Use short expiry (15 minutes to 1 hour) with refresh tokens
import { SignJWT, jwtVerify } from 'jose';
// Sign a JWT
const token = await new SignJWT({ userId: user.id, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('1h')
.sign(new TextEncoder().encode(process.env.JWT_SECRET));
// Verify a JWT
const { payload } = await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET)
);Role-Based Access Control (RBAC)
RBAC assigns permissions to roles, then assigns roles to users:
const permissions = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read'],
};
function hasPermission(user: User, permission: string): boolean {
return permissions[user.role]?.includes(permission) ?? false;
}
// In API route
if (!hasPermission(currentUser, 'delete')) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}Multi-Factor Authentication
MFA adds a second verification step after password entry. Common second factors:
- TOTP (Time-based One-Time Password) — apps like Authenticator generate a 6-digit code that changes every 30 seconds. Most secure and widely supported.
- SMS OTP — a code sent via text message. Convenient but vulnerable to SIM-swapping attacks.
- Hardware keys — physical devices (YubiKey). Most secure, least convenient.
Always offer MFA as an option. Make it required for admin accounts.
Key Takeaways
- Authentication proves identity; authorization controls access — both are always required
- Never store plain text passwords — use bcrypt or argon2 with appropriate cost factors
- JWTs must always be signature-validated and expiry-checked — never trust the payload without verification
- Store JWTs in HttpOnly cookies, not localStorage, to prevent XSS theft
- RBAC assigns permissions to roles — users inherit permissions from their assigned roles
Example
// Complete auth flow example
import bcrypt from 'bcryptjs';
import { SignJWT, jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
// Registration
export async function register(email: string, password: string) {
const hashedPassword = await bcrypt.hash(password, 12);
return db.users.create({ email, password: hashedPassword });
}
// Login
export async function login(email: string, password: string) {
const user = await db.users.findByEmail(email);
if (!user) throw new Error('Invalid credentials');
const valid = await bcrypt.compare(password, user.password);
if (!valid) throw new Error('Invalid credentials');
return new SignJWT({ userId: user.id, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('1h')
.sign(secret);
}
// Verify
export async function verifyToken(token: string) {
const { payload } = await jwtVerify(token, secret);
return payload;
}