π Authentication and Authorization β
π±Beginner Β· 0β1 yrBeginner0β1 yr experience Foundational concepts explained from first principles. No prior system-design experience required.Prerequisites: Comfortable reading code. Familiarity with at least one programming language.Foundation concept β required reading for every system.
Authentication (AuthN) answers "Who are you?" and Authorization (AuthZ) answers "What can you do?". They are two separate concerns that must be implemented together β and getting either one wrong compromises the entire system.
π Table of Contents β
- The Core Difference β AuthN vs AuthZ
- Where They Sit in the System
- Authentication Strategies
- Authorization Models
- Token-Based Authentication with JWT
- Session-Based Authentication
- OAuth 2.0 and OpenID Connect
- Password Storage and Hashing
- Multi-Factor Authentication (MFA)
- Common Vulnerabilities and Defenses
- Best Practices Checklist
- Related Topics
1. The Core Difference β AuthN vs AuthZ β
These two terms are often confused but represent distinct responsibilities.
| Aspect | Authentication (AuthN) | Authorization (AuthZ) |
|---|---|---|
| Question | Who are you? | What are you allowed to do? |
| Verifies | Identity (user, service, device) | Permissions (roles, scopes, policies) |
| Happens | First (before anything else) | After successful auth |
| Common methods | Passwords, tokens, biometrics, OAuth, SAML | RBAC, ABAC, ACLs, scopes, resource ownership |
| Failure returns | 401 Unauthorized | 403 Forbidden |
| Example | Login with email + password returns JWT | Admin role can DELETE /users/:id |
TIP
The status codes matter β 401 means "I don't know who you are" and 403 means "I know who you are but you can't do that." Returning the wrong one leaks information.
2. Where They Sit in the System β
Authentication and authorization are typically enforced at the edge (API Gateway / Load Balancer) and re-checked at the service level.
Why enforce at both layers? β
| Layer | What it checks | Why |
|---|---|---|
| Edge / Gateway | Token signature, expiry, basic role | Reject invalid traffic before it hits your services |
| Service | Resource ownership, fine-grained permissions | Defense in depth β never trust the gateway blindly |
| Database | Row-level security (optional) | Last line of defense for highly sensitive data |
WARNING
Never trust only the gateway. Any service that has access to internal data should re-verify the caller's identity and permissions. A compromised internal service should still be unable to read another tenant's data.
3. Authentication Strategies β
Decision Matrix β
| Strategy | Best for | Pros | Cons |
|---|---|---|---|
| Session + Cookie | Server-rendered web apps | Simple, revocable | Stateful, CSRF risk |
| JWT (stateless) | SPAs, mobile, microservices | Scalable, no shared state | Hard to revoke, larger tokens |
| OAuth 2.0 / OIDC | Third-party login, federated identity | No password storage | Complex setup |
| API Key | Server-to-server, machine identity | Simple, easy to rotate | No user context, leak risk |
| SAML | Enterprise SSO | Mature, enterprise-grade | XML-heavy, complex |
| mTLS | Service-to-service in zero-trust networks | Strongest crypto identity | PKI overhead |
4. Authorization Models β
4.1 RBAC β Role-Based Access Control β
Roles are assigned to users; permissions are assigned to roles. Simple, easy to reason about.
// RBAC middleware β Node.js
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: "Authentication required" });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ error: "Insufficient role" });
}
next();
};
}
// Usage
app.delete("/users/:id", authenticate, authorize("admin"), deleteUser);
app.post("/posts", authenticate, authorize("admin", "editor"), createPost);
app.get(
"/posts",
authenticate,
authorize("admin", "editor", "viewer"),
listPosts
);4.2 ABAC β Attribute-Based Access Control β
Policies are evaluated against attributes of the user, resource, and environment. More flexible, more complex.
// ABAC example β policy-as-code
const policies = {
canEditPost: (user, post, context) => {
return (
user.id === post.authorId || // author can edit own
user.role === "admin" || // admin can edit any
(user.role === "editor" && context.timeSincePublish < 60 * 60) // editors within 1h
);
},
};
function checkPolicy(policyName, user, resource, context = {}) {
const policy = policies[policyName];
if (!policy) throw new Error(`Unknown policy: ${policyName}`);
return policy(user, resource, context);
}4.3 ACL and Resource Ownership β
For per-resource checks (e.g., "can this user delete this specific post"), the service must own the authorization decision.
Choosing the Right Model β
| Model | When to use |
|---|---|
| RBAC | Small/medium apps, clear role hierarchy, most common |
| ABAC | Complex rules (time, location, ownership), enterprise systems |
| ACL | Per-resource permissions, multi-tenant apps |
| PBAC | Regulated industries (HIPAA, finance) requiring audit trails |
5. Token-Based Authentication with JWT β
JWT (JSON Web Token) is the de-facto standard for stateless authentication in modern web and mobile apps.
5.1 Anatomy of a JWT β
A JWT has three base64-encoded parts separated by dots:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HEADER.PAYLOAD.SIGNATURE β
β eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoxMjN9.abc123sig β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ// Header
{
"alg": "HS256", // signing algorithm
"typ": "JWT"
}
// Payload (claims β public, not encrypted!)
{
"sub": "user_123", // subject (user id)
"email": "[email protected]",
"role": "admin",
"iat": 1700000000, // issued at
"exp": 1700000900, // expires in 15min
"iss": "api.example.com", // issuer
"aud": "web-app" // audience
}WARNING
The JWT payload is signed, not encrypted. Anyone with the token can decode it. Never put passwords, secrets, or PII in the payload. Use JWE if you need encryption.
5.2 Complete JWT Implementation β
// utils/jwt.js
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET; // 256-bit random
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; // different 256-bit
// Generate access token (short-lived, 15 min)
function generateAccessToken(user) {
return jwt.sign(
{ sub: user.id, role: user.role, email: user.email },
ACCESS_SECRET,
{
expiresIn: "15m",
issuer: "api.example.com",
audience: "web-app",
algorithm: "HS256",
}
);
}
// Generate refresh token (long-lived, 7 days)
function generateRefreshToken(userId) {
return jwt.sign({ sub: userId, type: "refresh" }, REFRESH_SECRET, {
expiresIn: "7d",
issuer: "api.example.com",
});
}
// Verify token β always catch and inspect errors
function verifyAccessToken(token) {
return jwt.verify(token, ACCESS_SECRET, {
issuer: "api.example.com",
audience: "web-app",
algorithms: ["HS256"], // explicit β prevent alg confusion attacks
});
}// middleware/auth.js
const { verifyAccessToken } = require("../utils/jwt");
async function authenticate(req, res, next) {
try {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing token" });
}
const token = header.slice(7);
const decoded = verifyAccessToken(token);
// Optional: re-check user still exists / is active
// const user = await db.users.findById(decoded.sub);
// if (!user || user.banned) return res.status(401).json({ error: "Invalid" });
req.user = { id: decoded.sub, role: decoded.role, email: decoded.email };
next();
} catch (err) {
if (err.name === "TokenExpiredError") {
return res
.status(401)
.json({ error: "Token expired", code: "TOKEN_EXPIRED" });
}
return res.status(401).json({ error: "Invalid token" });
}
}5.3 Refresh Token Flow β
// Refresh token endpoint
app.post("/auth/refresh", async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.status(401).json({ error: "No refresh token" });
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const stored = await db.refreshTokens.findOne({
token: hash(refreshToken),
userId: decoded.sub,
revoked: false,
expiresAt: { $gt: new Date() },
});
if (!stored)
return res.status(401).json({ error: "Invalid refresh token" });
// Rotate: issue new refresh token, revoke old one
const newRefresh = generateRefreshToken(decoded.sub);
await db.refreshTokens.update(
{ id: stored.id },
{ revoked: true, replacedBy: hash(newRefresh) }
);
await db.refreshTokens.insert({
userId: decoded.sub,
token: hash(newRefresh),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});
const accessToken = generateAccessToken({
id: decoded.sub,
role: stored.role,
});
res.cookie("refreshToken", newRefresh, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
} catch {
res.status(401).json({ error: "Invalid refresh token" });
}
});5.4 Where to Store the Token β
| Storage | XSS safe? | CSRF safe? | Best for |
|---|---|---|---|
localStorage | β | β | None β avoid for tokens |
sessionStorage | β | β | None β avoid for tokens |
| httpOnly cookie | β | β οΈ need CSRF token | Recommended for web apps |
| Memory (JS var) | β | β | SPAs with refresh-token flow |
6. Session-Based Authentication β
The classic approach: server stores session state, client gets an opaque session ID via cookie.
When to choose sessions over JWT β
- Server-rendered web apps (e.g., Django, Rails, Express + EJS)
- You need immediate revocation (logout, ban, password change)
- Simpler to debug (no token decoding)
- Compliance requires audit trail of active sessions
Redis Session Implementation β
const session = require("express-session");
const RedisStore = require("connect-redis").default;
const { createClient } = require("redis");
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 1000 * 60 * 60 * 24, // 24 hours
},
})
);7. OAuth 2.0 and OpenID Connect β
OAuth 2.0 is an authorization framework for delegated access. OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0.
7.1 OAuth 2.0 Roles β
7.2 OAuth 2.0 Grant Flows β
| Grant | Use Case | When to Pick |
|---|---|---|
| Authorization Code + PKCE | Web apps, mobile apps, SPAs | Default for all modern apps |
| Client Credentials | Server-to-server, no user | Microservices, cron jobs, background workers |
| Device Code | Smart TVs, IoT, CLI tools | Devices without a browser |
| Refresh Token | Get new access token silently | Always paired with short-lived access tokens |
7.3 Authorization Code Flow with PKCE β
7.4 OAuth vs OIDC β What's the Difference? β
| Aspect | OAuth 2.0 | OpenID Connect |
|---|---|---|
| Purpose | Authorization (scopes) | Authentication (identity) |
| Token | Access token | ID token (JWT with user info) |
| Endpoint | /authorize, /token | /userinfo (adds identity) |
| Use when | "Let me act on behalf of the user" | "Let me know who the user is" |
TIP
Almost always use OIDC when you want "Sign in with Google/GitHub." Use plain OAuth 2.0 only when you need delegated access without identity (e.g., a third-party app posting to your user's Twitter).
8. Password Storage and Hashing β
8.1 The Wrong Ways (and Why) β
// β NEVER do any of these
const plain = "mypassword123";
const md5 = crypto.createHash("md5").update(plain).digest("hex");
const sha1 = crypto.createHash("sha1").update(plain).digest("hex");
const sha256 = crypto.createHash("sha256").update(plain).digest("hex");
const encrypted = encryptAES(plain, key); // reversible β no!8.2 The Right Way β bcrypt / Argon2id β
const bcrypt = require("bcrypt");
// Hashing on signup (cost factor 12 β ~250ms per hash)
async function hashPassword(plaintext) {
const salt = await bcrypt.genSalt(12);
return bcrypt.hash(plaintext, salt);
}
// Verification on login
async function verifyPassword(plaintext, hash) {
return bcrypt.compare(plaintext, hash);
}
// Alternative: Argon2id (winner of PHC, OWASP recommended)
const argon2 = require("argon2");
async function hashPasswordArgon2(plaintext) {
return argon2.hash(plaintext, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 3,
parallelism: 4,
});
}8.3 Password Policy β Show, Don't Just Tell β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Password Requirements β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β At least 12 characters β
β β Must contain uppercase, lowercase, number, symbol β
β β Cannot contain your email or username β
β β Cannot be one of the top 10,000 common passwords β
β β Cannot be reused (last 5 passwords) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ| Algorithm | Recommended? | Why |
|---|---|---|
| Argon2id | β Best | Memory-hard, PHC winner, GPU-resistant |
| bcrypt | β Good | Battle-tested, widely available |
| scrypt | β Good | Memory-hard, but less widely supported |
| PBKDF2 | β οΈ Acceptable | Only when Argon2/bcrypt not available; high iterations |
| MD5 / SHA-1 / SHA-256 | β NEVER | Trivially fast, GPU-cracked |
9. Multi-Factor Authentication (MFA) β
Defense in depth: require two or more of something you know, have, or are.
MFA Flow β TOTP (Time-Based One-Time Password) β
// TOTP β using otplib
const { authenticator } = require("otplib");
authenticator.options = { window: 1, step: 30 }; // 30s steps, Β±1 window tolerance
// On MFA enrollment
function generateSecret(userEmail) {
const secret = authenticator.generateSecret();
const otpauth = authenticator.keyuri(userEmail, "YourApp", secret);
return { secret, otpauth }; // User scans otpauth as QR code
}
// On MFA verification
function verifyToken(token, secret) {
return authenticator.verify({ token, secret });
}MFA Methods Compared β
| Method | Security | UX | Cost |
|---|---|---|---|
| Hardware key (FIDO2/WebAuthn) | βββββ | ββββ | $$ (keys) |
| TOTP (Authenticator app) | ββββ | ββββ | Free |
| SMS code | ββ | βββ | $ (per SMS) |
| Email code | β | ββ | Free |
| Push notification | ββββ | βββββ | $$ (provider) |
WARNING
SMS is vulnerable to SIM-swap attacks β only use it as a last resort. Prefer TOTP or hardware keys for high-value accounts.
10. Common Vulnerabilities and Defenses β
10.1 Attack Surface Map β
10.2 Defense Patterns β
// 1. Rate limiting on auth endpoints
const rateLimit = require("express-rate-limit");
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 min per IP
message: { error: "Too many attempts. Try again later." },
standardHeaders: true,
legacyHeaders: false,
});
app.post("/login", authLimiter, loginHandler);
app.post("/register", authLimiter, registerHandler);
// 2. Account lockout after failed attempts
async function trackFailedLogin(userId) {
const user = await db.users.findById(userId);
user.failedAttempts = (user.failedAttempts || 0) + 1;
if (user.failedAttempts >= 5) {
user.lockedUntil = new Date(Date.now() + 15 * 60 * 1000); // 15 min
}
await user.save();
}
// 3. Never log tokens or passwords
logger.info("Login attempt", { userId, email }); // β
logger.info("Login attempt", { password }); // β NEVER
logger.info("Login attempt", { token }); // β NEVER
// 4. JWT algorithm confusion prevention
jwt.verify(token, SECRET, { algorithms: ["HS256"] }); // β
explicit
// 5. CSRF protection for cookie-based sessions
const csrf = require("csurf");
app.use(csrf({ cookie: true }));
// Send token in meta tag, send in header on every mutation10.3 Security Headers β
const helmet = require("helmet");
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
},
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
})
);11. Best Practices Checklist β
β Authentication β
- [ ] Use HTTPS everywhere β never send credentials over plain HTTP
- [ ] Hash passwords with Argon2id or bcrypt (cost β₯ 12)
- [ ] Enforce strong password policy + check against known breached passwords
- [ ] Implement rate limiting on login/register/reset endpoints
- [ ] Lock account after N failed attempts (exponential backoff)
- [ ] Use short-lived access tokens (15 min) + long-lived refresh tokens (7 days)
- [ ] Store refresh tokens in httpOnly + Secure + SameSite=Strict cookies
- [ ] Rotate refresh tokens on every use
- [ ] Implement MFA for high-value accounts
- [ ] Log all auth events (success, failure, lockout) β never log secrets
- [ ] Implement PKCE for all OAuth 2.0 flows
- [ ] Explicitly list
algorithmsinjwt.verify()to prevent alg attacks
β Authorization β
- [ ] Never trust client-side authorization checks
- [ ] Re-verify permissions at the service layer, not just the gateway
- [ ] Use least privilege β default to deny
- [ ] Prefer RBAC for clarity; reach for ABAC only when you need fine-grained rules
- [ ] Implement resource ownership checks for per-user data
- [ ] Use scopes in OAuth tokens to limit delegated access
- [ ] Return
403(not404) when the user exists but lacks permission β but only if no info leak is possible - [ ] Audit sensitive operations (who accessed what, when)
β Operational β
- [ ] Store secrets in a secret manager (AWS Secrets Manager, Vault) β never in code
- [ ] Rotate JWT signing keys periodically
- [ ] Have a token revocation strategy (blacklist or short TTL)
- [ ] Monitor for anomalous patterns (login from new geo, impossible travel)
- [ ] Run penetration tests and dependency audits regularly
- [ ] Document your incident response plan for credential leaks
12. Related Topics β
This page sits at the foundation of the security stack. To go deeper, follow these threads:
| Topic | Where to go next |
|---|---|
| HTTPS / TLS | Networking β Level 1 |
| Rate Limiting | Scalability β Level 2 |
| API Gateway | Microservices β Level 6 |
| Distributed session store | Caching β Level 4 |
| Security in interviews | Security Interview β Level 12 |
| Design patterns (Strategy, Decorator) | Design Patterns β Level 1 |
π§ One-Page Recap β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AUTHENTICATION β AUTHORIZATION β
β βββββββββββββ β βββββββββββββ β
β β’ Who are you? β β’ What can you do? β
β β’ 401 if unknown β β’ 403 if forbidden β
β β’ JWT / Session / OAuth β β’ RBAC / ABAC / ownership β
β β’ Verify before anything else β β’ Check at gateway AND service β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β STORAGE β bcrypt/Argon2 β CHECKS β server-side, not client β
β TRANSPORT β HTTPS only β TOKENS β short-lived access + β
β COOKIES β httpOnly/Secure β refresh rotation β
β MFA β TOTP or WebAuthn β LEAST β privilege, default denyβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββINFO
Next step: Continue to Security β Interview Prep to see how these concepts are tested in FAANG interviews, or jump to Microservices β Level 6 to see how authentication is enforced across service boundaries.
