Last 30 Days
No notifications
Authentication verifies identity. Authorization checks permissions.
| Method | How It Works | Best For |
| Session/Cookie | Server stores session, client gets cookie | Traditional web apps |
| JWT (Token) | Client stores signed token | APIs, SPAs, mobile apps |
| OAuth 2.0 | Delegate auth to Google/GitHub | Social login |
| API Keys | Static key in header | Server-to-server |
1. Client → POST /login { email, password }
2. Server → Verify credentials
3. Server → Create JWT with user info
4. Server → Send JWT to client
5. Client → Store JWT (localStorage/cookie)
6. Client → Send JWT in Authorization header
7. Server → Verify JWT on each requestheader.payload.signatureeyJhbGciOiJIUzI1NiJ9. ← Header (algorithm)
eyJ1c2VySWQiOjEsInJvbGUi. ← Payload (user data)
SflKxwRJSMeKKF2QT4fwp... ← Signature (verification)
| ❌ Never | ✅ Always |
| Store plain text passwords | Hash with bcrypt |
| Use MD5/SHA1 for passwords | Use bcrypt (salt + hash) |
| Send passwords in URLs | Send in request body over HTTPS |
Authentication answers "who are you?". Authorization answers "what are you allowed to do?". They are different jobs and they fail in different ways. Mixing them up is the most common cause of security incidents in junior code.
When a request hits your API, the server needs to:
1. Identify the caller (read a token / cookie / API key). 2. Verify that the credential is genuine (signature valid, not expired). 3. Authorize the action (does this user own this post? do they have role "admin"?).
Missing any of the three = a vulnerability. "401 vs 403" maps cleanly: 401 = step 1 or 2 failed, 403 = step 3 failed.
| Sessions (cookie + server store) | Tokens (JWT) | |
| Where state lives | Server (Redis, DB) | Inside the token |
| Revoke a user | Delete their session row | Hard — token is valid until it expires |
| Cross-domain APIs | Awkward (CORS + cookies) | Easy (Authorization header) |
| Mobile / 3rd-party clients | Painful | Designed for it |
| Default for browser-only apps | ✅ Often the right choice | Overkill |
Real apps often mix: short-lived JWT access tokens for the API + a session-ish refresh token in an httpOnly cookie.
Never store raw passwords. Never use MD5 or SHA-256 directly (too fast — attackers brute-force billions/sec on a GPU). Use a slow, salted hash:
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(plainPassword, 12); // cost factor 12
const ok = await bcrypt.compare(plainPassword, hash);The cost factor (rounds) controls slowness. Higher = safer but slower login. Pick the highest value that keeps login under ~250ms on your server.
| Cost | ~Time | Use case |
| 10 | ~65ms | Old default |
| 12 | ~250ms | Recommended today |
| 14 | ~1s | Paranoid / very sensitive |
A JWT is three Base64-encoded parts joined by dots:
xxxxx.yyyyy.zzzzz
header.payload.signature{ "alg": "HS256", "typ": "JWT" }sub, exp, role, etc.) — readable by anyone, just Base64.HMAC(secret, header + payload) — proves the payload has not been tampered with.const jwt = require('jsonwebtoken');
const token = jwt.sign({ sub: user.id, role: user.role }, process.env.JWT_SECRET, {
expiresIn: '15m',
issuer: 'campuscrate',
audience: 'web',
});const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if bad
Key rule: JWT is signed, not encrypted. Do not put passwords, full names, addresses, or anything sensitive in the payload — anyone with the token can decode it.
1. Storing JWTs in localStorage — any XSS bug = stolen token. Use httpOnly + Secure + SameSite=Strict cookies for browsers.
2. No expiration — expiresIn is mandatory. Long-lived tokens you cannot revoke = nightmare.
3. Using a weak / hard-coded secret — must be ≥ 32 random bytes, from process.env, never committed.
4. Same secret across environments — dev, staging, prod each get their own.
5. Comparing tokens or hashes with === — use crypto.timingSafeEqual to avoid timing attacks.
6. Telling users "wrong password" vs "unknown email" — leaks which emails are registered. Always say "invalid credentials".
Short access tokens are great for security but annoying for users (re-login every 15min). The fix:
Login → access token (15m, JSON) + refresh token (7d, httpOnly cookie)
API call → send access token in Authorization header
401 expired → call /auth/refresh → server reads cookie, issues new access token
Logout → server deletes the refresh token from its storeRotate refresh tokens on every use (one-time-use). If the *same* refresh token is replayed, assume it was stolen and revoke the whole family — this is called *refresh token reuse detection*.
function auth(requiredRole) {
return (req, res, next) => {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: 'NO_TOKEN' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
if (requiredRole && payload.role !== requiredRole) {
return res.status(403).json({ error: 'FORBIDDEN' });
}
next();
} catch {
res.status(401).json({ error: 'INVALID_TOKEN' });
}
};
}app.get('/me', auth(), (req, res) => res.json(req.user));
app.get('/admin/stats', auth('admin'), adminController);
The login endpoint is the #1 brute-force target. Always layer:
express-rate-limit, e.g. 10 attempts / 15min).role: 'admin' 'editor'
'viewer'. Easy, covers 80% of apps.user.team === post.team && action === 'edit').resource.userId === req.user.sub. This is the most-missed check in real bugs.code → server exchanges code for tokens.passport, openid-client, NextAuth, Auth0, Clerk) — never roll your own.JWTs are *bearer tokens*: whoever holds it is treated as the user. To support "log out everywhere":
jti) in Redis with TTL = token's remaining lifetime.tokenVersion on the user — verify it matches the JWT's tokenVersion claim. Logout = increment, all old tokens fail.1. Implement POST /signup and POST /login with bcrypt cost 12 and JWT (15m).
2. Add refresh tokens stored as httpOnly cookies + a POST /auth/refresh endpoint.
3. Add auth(role) middleware and protect GET /admin routes; double-check ownership in resource handlers.
4. Add express-rate-limit to login and confirm a brute-force script gets blocked after 10 tries.