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:

typescript
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:

text
eyJhbGciOiJIUzI1NiJ9    <- Header (algorithm)
.eyJ1c2VySWQiOiIxMjMifQ  <- Payload (claims)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  <- Signature

Critical 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
typescript
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:

typescript
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

typescript
// 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;
}
Try it yourself — TYPESCRIPT

Docker, AWS, Vercel, Netlify, GitHub, GitHub Actions are trademarks of Docker, Inc., Amazon.com, Inc., Vercel, Inc., Netlify, Inc., Microsoft Corporation. DevForge Academy is not affiliated with, endorsed by, or sponsored by these companies. Referenced for educational purposes only. See full disclaimers