JWT structure, signing and verification, refresh token rotation, bcrypt cost factors, Express auth middleware, session vs stateless — and OAuth 2.0 social login.
Module P-2 — Authentication and Authorization
What this module covers: Authentication answers "who are you?" Authorization answers "what are you allowed to do?" These are two distinct concerns that most tutorials conflate. This module covers the complete production authentication stack: bcrypt for password hashing, JWT for stateless tokens, refresh token rotation for long-lived sessions, Express middleware for protecting routes, and role-based access control. By the end you will have a working auth system you can drop into any Node.js API.
Authentication vs Authorization
Before writing any code, get the terminology straight — confusing these two causes real security bugs:
- Authentication — verifying identity. "Is this really Jatin?" Handled by login, tokens, sessions.
- Authorization — verifying permission. "Is Jatin allowed to delete this post?" Handled by role checks, ownership checks, policies.
A user can be fully authenticated (we know who they are) and still be unauthorized (they don't have permission for this specific action). Both checks are needed, and they run in that order.
Passwords: Never Store Plaintext
If your database is ever breached, plaintext passwords give attackers instant access to every account — and to every other site where users reused that password. Always hash passwords with a slow, purpose-built algorithm.
bcrypt is the standard. It uses a configurable "cost factor" (work factor) that controls how long hashing takes. The higher the cost, the more computation an attacker needs to brute-force the hash.
Cost factor guidelines:
| Environment | Recommended cost | Approx. time per hash |
|---|---|---|
| Development | 10 | ~65ms |
| Production | 12 | ~250ms |
| High-security | 14 | ~1 second |
At cost 12, a user waits ~250ms on login — imperceptible. An attacker trying to brute-force a leaked database hash faces 250ms per attempt. At scale, that's the difference between cracking a password in hours vs years.
Timing-safe comparison: bcrypt.compare is timing-safe — it takes the same amount of time regardless of whether the password is correct or not. This prevents timing attacks that infer the correct password by measuring response time differences.
JSON Web Tokens (JWT)
After a user authenticates, you need a way to identify them on subsequent requests without asking for their password again. JWT is the most common stateless solution.
JWT Structure
A JWT is a Base64-encoded string with three parts separated by dots: header.payload.signature
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzE2MjkwMDAwLCJleHAiOjE3MTYyOTM2MDB9.abc123...
- Header — algorithm used (
HS256,RS256) - Payload — claims:
sub(subject/userId),role,iat(issued at),exp(expiry) - Signature — HMAC of header+payload using your secret key
The payload is Base64-encoded, not encrypted — anyone can decode and read it. Never put sensitive data (passwords, PII) in the payload. The signature guarantees it has not been tampered with.
Signing and verifying tokens
Generate secure secrets:
Why Two Tokens?
A single long-lived token is a security liability — if stolen, the attacker has access for days or weeks. The two-token pattern mitigates this:
- Access token — short-lived (15 min). Sent with every API request. Verified entirely from the signature — no database lookup needed. If stolen, expires quickly.
- Refresh token — long-lived (7 days). Stored securely. Used only to get a new access token when the old one expires. Can be invalidated by deleting it from the database.
The Complete Auth Flow
User registration
User login
Token refresh
Refresh token rotation: every time a refresh token is used, it is deleted and a new one is issued. If a stolen refresh token is used, the legitimate user's next refresh will fail (their token was also invalidated). This is how you detect token theft.
Logout
The Authentication Middleware
This middleware extracts the JWT from the Authorization header, verifies it, and attaches the decoded user to req.user.
Usage in routes:
Authorization: Role-Based Access Control
Authentication confirms identity. Authorization enforces what they can do. The most common approach is role-based access control (RBAC) — users have a role, roles have permissions.
Ownership checks
RBAC is not enough for "users can only edit their own posts". That requires an ownership check in the service:
Ownership logic lives in the service, not the route or middleware. It has access to the full business context.
Auth Routes
Session-Based Auth vs Stateless JWT
You will encounter both in production. Choosing correctly matters:
| JWT (stateless) | Sessions (stateful) | |
|---|---|---|
| Server state | None — self-contained token | Session store (Redis/DB) |
| Revocation | Hard — token valid until expiry | Instant — delete session |
| Horizontal scaling | Trivial — any server can verify | Needs shared session store |
| Token theft response | Wait for expiry | Delete session immediately |
| Complexity | Refresh token rotation required | Simpler — one session ID |
Use JWT when: you have multiple services, horizontal scaling, or mobile clients. The statelessness simplifies architecture.
Use sessions when: you need instant revocation (e.g. "log out all devices"), you have a monolith, or your team is more familiar with sessions. Express + express-session + Redis is the standard stack.
For most modern APIs (especially mobile or multi-service), JWT with refresh token rotation is the right choice.
Security Checklist
Before shipping auth:
- Passwords hashed with bcrypt, cost factor ≥ 12
- JWT secrets are long (≥ 32 bytes), random, and different for access vs refresh
- Access tokens expire in ≤ 15 minutes
- Refresh tokens are stored in the database and rotated on use
- Login returns the same error for "wrong email" and "wrong password" (prevents user enumeration)
- Rate limiting on
/auth/loginand/auth/register(covered in P-6) - HTTPS enforced in production — tokens in plaintext over HTTP are useless
- Refresh tokens sent in
HttpOnlycookies rather than response body (prevents XSS theft) -
Authorizationheader checked withstartsWith('Bearer '), not split/regex
Summary
- Passwords: always bcrypt with cost ≥ 12. Never MD5, SHA1, or plaintext.
bcrypt.compareis timing-safe. - JWT:
header.payload.signature. Payload is readable — never put secrets in it. Signature prevents tampering. - Two-token pattern: short-lived access tokens (15 min, stateless), long-lived refresh tokens (7 days, stored in DB). Rotation on use detects theft.
authenticatemiddleware extracts the Bearer token, verifies it, attachesreq.user. Run it on every protected route.- Authorization:
authorize('admin')middleware for role checks. Ownership checks belong in the service layer. - Same error message for "user not found" and "wrong password" — never reveal which one failed.
- JWT vs sessions: JWT for multi-service/mobile, sessions for instant revocation in monoliths.
Next: TypeScript in Node.js — adding type safety to everything you have built so far.
When implementing refresh token rotation, what happens when a user attempts to use a refresh token that has already been used?
In an Express application, where should the "Ownership check" logic (e.g., verifying a user can only edit their own post) typically reside?
Which scenario strongly favors using stateful Session-based Authentication over stateless JWTs?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion