Four things sit outside that list and belong in every review.
Dependencies. Your application is mostly other people’s code. composer audit checks every installed package against known vulnerabilities, and Chapter 14 runs it on every build.
Secrets. Know every secret the application holds, where it lives, and how you would replace it. The application key deserves a special look: it encrypts every consumer’s settings, and Chapter 20 explains how to rotate it without losing them.
The signs of an attack. A burst of 401s from one address is someone guessing. A burst of 403s from one token is someone probing what a stolen credential can do. A burst of 404s on one route is someone walking your records. Laravel doesn’t report these as errors, rightly. The request log from Chapter 6 has the status, the address, and the token’s ID on every row, so each of those is a query. Count them, and alert when one caller’s count leaves the ordinary.
A record of who did what. The request log shows that someone called POST /consumers/7/tokens. It doesn’t show which abilities were granted. For the few privileged actions (issuing a token, revoking one, deleting a consumer), write an audit entry that says who, to whom, and what:
// app/Http/Controllers/TokenController.php, in store()
Log::channel('audit')->info('token.issued', [
'by' => $request->user()->id,
'for' => $consumer->id,
'abilities' => $request->validated('abilities'),
]);
audit is a channel of its own in config/logging.php, kept longer than the application log and shipped somewhere the application can’t rewrite it.
A Review You Repeat
A review done once describes an API that no longer exists a month later. The useful form is small enough to run on every new endpoint, as part of the pull request:
- Who may call this? Which ability or Policy, and is it declared on the controller? Is there a row for it in the wrong-caller test?
- Which records may they touch? Is there a Policy, and a test with the wrong caller?
- Which fields may they read and write? Is every field in the Resource meant to leave, and every rule in the request meant to be settable by this caller?
- What is the largest thing they can ask for? Does every input have a bound?
- Does it make a request, or store a URL? Where does the address come from?
Five questions, answered in the pull request that adds the endpoint.
Chapter 19 Summary
- Review the whole, on paper, against a list of how APIs are really broken. Authorization failures lead that list.
- For every ID in a URL, know who may put which value there, and test the wrong caller. Scoped bindings are not authorization.
- Every controller declares who may call it. Keep a table of routes and refused callers, as a test.
- Tenant isolation by credentials holds only if nothing falls back to a shared account.
- Control fields in both directions: a Resource on the way out, validated and fillable attributes on the way in.
- Give every input a bound, headers included.
- A caller may grant only what it holds.
- Protect expensive business operations with business limits, not only rate limits.
- Treat any URL a caller supplies as hostile: HTTPS only, every resolved address public, no redirects, and know what that still leaves open.
- Set production’s configuration deliberately, and read your error responses as an attacker would.
- Delete what you no longer need: versions, tokens, endpoints.
- Treat a provider’s response as input.
- Count the 401s, 403s, and 404s, and audit the privileged actions.
- Ask the five questions of every new endpoint.