Without notes, state yesterday’s main idea and one unresolved question.
Week 8 secured API review
Backend Architecture, Authentication, and Security
Objective
Run a security-focused regression review and document the backend architecture.
Authentication identifies the operator; authorization checks which controls that operator is permitted to activate.
- threat-driven review
- negative tests
- architecture diagram
Why this matters
Six days ago you had an API that worked. Today you have one that knows who is calling, refuses what they may not do, records what happened without leaking secrets, and proves all of it with a test command. Today you close the week the way real teams close a security milestone: an adversarial review of your own work, negative tests for the things that must fail, a diagram someone else can read, and an honest list of what is still broken.
The last item is the one that separates engineering from theatre. A milestone with documented known limitations is trustworthy. One that claims to be secure is not.
Threat-driven review
A review that walks the code file by file finds typos. A threat-driven review starts from what an attacker wants and works backwards to the code that stands in the way. Same hour, far better questions.
Take your Day 53 threat model and run each attacker against each asset:
| Attacker | Wants | Ask of your code |
|---|---|---|
| Anonymous stranger with curl | Any data, any write | Does every protected route sit behind requireAuth? |
| Logged-in technician | Admin actions | Is requireRole on every admin-only route? |
| Logged-in technician | Another user's records | Is ownership checked after loading the row? |
| Malicious website the user visits | A state change using their cookie | What does sameSite do about a cross-site POST? |
| Someone holding a copy of the database | Passwords | Is every stored value a salted bcrypt hash? |
| Someone reading log files | Credentials | Do the logs contain a password, cookie, or session ID? |
Each row is a question you answer by running something, not by remembering. That is the whole method: attacker, asset, path, evidence.
Sign-off, not a glance
Before a board ships you do not admire the layout — you run the test plan: continuity where continuity is required, isolation where isolation is required, and the fault injection that proves the protection circuit trips. A threat-driven review is the isolation and fault-injection half. Confirming the happy path only proves the thing conducts.
Deny by default, one more time
The rule that has run under the whole week: when permission is unclear, the answer is no.
Unclear means any of these: a route you have not classified, a role you have not seen before, a session that failed to load, a resource whose owner you could not determine. Every one of them must end in a refusal, not a shrug.
// wrong — an unrecognised role falls through and is allowed
if (actor.role === 'technician' && !isAdminOnly(path)) return next();
if (actor.role === 'admin') return next();
next(); // ← anything unmatched gets through
// right — the default is refusal
if (!isAllowed(actor, action)) {
return res.status(403).json({ error: 'insufficient permissions' });
}
next();
The difference is not style. The first shape breaks silently the day someone adds a role; the second breaks loudly, in a way you find immediately. Choose the failure you will notice.
The door that locks when the power fails
A secure door fails locked; a fire door fails open. Both are correct, because someone decided which failure is worse for that door. Undecided is the only wrong answer — and an unclassified route is an undecided door.
Negative tests
A negative test asserts that something is refused. Yesterday's suite already has a few; today you make them systematic, because they are the only tests that can prove a security control exists.
Positive tests confirm the feature. Negative tests confirm the boundary:
- Anonymous request to every protected route →
401 - Technician on every admin-only route →
403 - Technician editing another technician's record →
403 - Expired session cookie →
401 - A session ID that was logged out →
401 - Duplicate serial number →
409 - Missing required field →
400
Test that a control fails correctly and you have tested the control. Test only the success path
and you have tested nothing about it — a function returning true unconditionally passes every
positive test you can write.
The architecture diagram
One page, drawn so a new developer can find things. Boxes for your layers, arrows for dependency direction, and a mark at each trust boundary. Text is fine — it survives in Git and it diffs.
┌───────────────────────────────┐
browser ──────▶│ routes/ (HTTP only) │
cookie: sid │ requestLogger → requireAuth │
│ → requireRole → handler │
└───────────────┬───────────────┘
── trust boundary ────────────│──── user controls everything above
▼
┌───────────────────────────────┐
│ services/ (rules, ownership) │
│ throws AppError(code) │
└───────────────┬───────────────┘
▼
┌───────────────────────────────┐
│ repositories/ (SQL, $1 args) │
└───────────────┬───────────────┘
▼
PostgreSQL
users · sessions · equipment
maintenance_records
Two things the diagram must show, because they are the two things people get wrong: which direction dependencies point (down only, never back up) and where the trust boundary sits (everything above it is under the user's control, as Day 1 established).
Dependency and secret checks
Two commands, both fast, both worth running before any milestone.
Dependencies. Your code is a minority of what ships; the rest is packages.
npm audit
# npm audit report
found 0 vulnerabilities
If it reports findings, read them before acting. npm audit fix applies compatible updates;
npm audit fix --force may install breaking major versions, so run your tests immediately after.
Not every advisory is exploitable in your usage — a vulnerability in a dev-only tool is different
from one in your request path. Note the reasoning; do not silence the tool.
Secrets. The rule is that credentials come from the environment and never from a file in Git.
git ls-files | grep -i "env" # .env must NOT be listed; .env.example may be
git grep -n -i -E "password *= *['\"]|secret *= *['\"]|api[_-]?key"
git log -p -S "password" -- src/ | head -40
The last command searches the whole history, not just the current files.
Deleting a committed secret does not un-leak it
A password removed in a later commit still sits in the history, in every clone and every fork.
The only real fix is to rotate the secret — change the password, revoke the key — and then
clean the history if you can. Treat anything ever committed as public. This is also why the
.gitignore entry goes in before the first commit, not after the mistake.
Three minutes, right now
Run all three secret commands. If they find nothing, you have evidence. If they find something, you have found it before someone else did — which is the entire value of running them.
Walkthrough: the review pass
Work through this in order, writing results as you go. Each step produces evidence, not an opinion.
# 1. Everything still works
DATABASE_URL=postgres://localhost/maintenance_test npm test
Test Files 3 passed (3)
Tests 14 passed (14)
# 2. Anonymous access to a protected route
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE http://localhost:3000/equipment/1
401
# 3. Wrong role
curl -c tech.txt -s -X POST http://localhost:3000/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"[email protected]","password":"correct horse battery staple"}' > /dev/null
curl -s -o /dev/null -w '%{http_code}\n' -b tech.txt -X DELETE http://localhost:3000/equipment/1
403
# 4. Logout really revokes
curl -s -b tech.txt -X POST http://localhost:3000/auth/logout > /dev/null
curl -s -o /dev/null -w '%{http_code}\n' -b tech.txt http://localhost:3000/equipment
401
# 5. Nothing sensitive stored or leaked
psql maintenance -c "SELECT email, left(password_hash, 7) FROM users;"
email | left
------------------+---------
[email protected] | $2b$12$
Then the two checks above — npm audit and the secret grep — and finally re-read one log excerpt
looking specifically for a password, a cookie, or a session ID.
Checkpoint
You should have five status codes, one hash prefix, one audit result, and one clean secret scan — all captured, none remembered.
Your turn
Produce the milestone: a secured API with documented known limitations.
- Run the full test suite. Everything green before you review anything.
- Add negative tests for every row of the list above that you do not already cover. Include the logged-out-session case, which is the one people forget.
- Do the threat-driven pass: for each attacker row in the table, run the command and record the status code you actually got beside the one you expected.
- Fix every disagreement. If you cannot fix one today, it becomes a known limitation with a sentence explaining the risk.
- Draw the architecture diagram in
docs/architecture.mdwith dependency arrows and the trust boundary marked. - Run
npm auditand the three secret commands. Record the output verbatim, including "found 0 vulnerabilities" if that is what you got. - Update
docs/threat-model.md: move anything now mitigated into the mitigations list with the file that implements it, and leave the rest under known limitations with an honest one-line risk statement each. - Commit with a message naming what is secured and what is not.
Reviewer mode — at step 3, on your own code
"Review this backend as an adversarial but authorized tester. Prioritize exploitable issues and provide reproduction steps." Paste your routes, middleware, and one service. Demand a reproduction command for each finding — that is what makes a finding checkable and what separates a real issue from a plausible-sounding one. Verify every claim by running it yourself before you change any code, and dismiss anything you can disprove. Specific actionable findings with evidence are the only useful output here; general praise tells you nothing, and a wholesale rewrite you cannot explain is worse than the bug. This is your own project, and adversarial review of your own system is the only kind you are entitled to run.
You are done when
Every claim in your threat model has a command behind it, and your known-limitations list is not empty. An empty list on a seven-day-old auth system means you stopped looking.
Common pitfalls
- Reviewing by reading. Reading finds what you expect. Running finds what is true.
- An empty known-limitations list. You are missing rate limiting on login, at minimum. Say so.
- Fixing findings without a test. A bug fixed with no negative test comes back next month.
- Treating
npm auditas a pass/fail gate. Read the advisory, decide whether it reaches your code, write down the reasoning either way. - Assuming the removed secret is gone. History keeps it. Rotate it.
Verify it yourself
Open today's reference, the OWASP Top 10, and read the category list end to end — not one entry, the whole list.
- Pick the category your API is most exposed to right now and justify the choice in one
paragraph in
docs/threat-model.md. - Find one category this week never addressed at all. Add it to known limitations with a note on what addressing it would involve. Week 11's security review will pick it up.
Naming your weakest point in writing, before anyone asks, is the professional habit this week was really teaching.
The hour
- 0–5 min Recall
- 5–20 min Learn
Read only the listed concept notes and official reference sections needed today.
- 20–48 min Build
Complete auth, authorization, tests, logs, and an architecture diagram. Run dependency and secret checks.
- 48–55 min Explain and verify
Run the result, inspect evidence, and explain the data/control flow in your own words.
- 55–60 min Quiz and commit
Complete the quiz, record one lesson, and commit the verified change when applicable.
What to hand in
A secured API milestone with documented known limitations.
Working with AI today
Provide existing work and ask for concrete defects, risks, missing tests, and unsupported assumptions—not praise.
Review this backend as an adversarial but authorized tester. Prioritize exploitable issues and provide reproduction steps.
References
End-of-day quiz
Explain-back gate
Pass the quiz above to unlock completion.
Quiz + explain-back checks required.