Security has been in every chapter of this book, which is how it should be built and exactly why it is hard to see. A token here, a Policy there, a header, a whitelist. Each decision was right where it was made. None of them tells you whether the whole is sound.
So this chapter does what a team should do before an API meets the world, and again every time it grows: step back and attack it on paper. The method is simple. Take a list of the ways APIs actually get broken, and for each one ask three questions. What would that look like here? What stops it? How would we know if it stopped working?
The list is the OWASP API Security Top 10, the most widely used catalog of API risks. It is ordered by risk, and the order is instructive: the top of the list is not cryptography. It is authorization.
A security review is less a hunt for clever attacks than a check that the boring rules have no gaps. I can say what such a review finds, because an earlier draft of this book was put through one. The token endpoint in Chapter 5 was open to any caller. The provider credentials in Chapter 4 fell back to a shared account. The batch endpoint in Chapter 18 let a read-only token write. All three were written by someone who knew better, one of them in the chapter about authentication. That is what gaps look like: not ignorance, but one check nobody thought to make.
1. Broken Object Level Authorization
The most common API vulnerability of all: a caller changes an ID in the URL and reaches somebody else’s record.
Here: GET /api/consumers/7 asked by consumer 3. POST /api/consumers/7/tokens asked by consumer 3, which would hand it a credential for consumer 7. GET /api/license-batches/{id} or GET /api/exports/{id} with another consumer’s ID.
What stops it: a separate mechanism for each, and it matters which does what.
ConsumerPolicy::view()lets a consumer see only itself, and answers 404 otherwise.TokenControllerrequires theconsumers:manageability, checked before any record is loaded, so the answer is a 403 whatever the ID.- The batch endpoint compares the batch’s owner with the caller and answers 404.
- The export endpoint builds its path from the caller’s own ID, so another consumer’s export has no address.
Both 404s have the body of every other 404, because Chapter 7 renders them all in one place. A caller can’t tell “not yours” from “not there.”
scoped() on the nested token route is not on that list. It makes sure a token in the URL belongs to the consumer in the URL. It says nothing about whether the caller may touch that consumer. It is easy to mistake one for the other, and the mistake is how the token endpoint was left open in the draft.
Licenses have no check at all. Isolation there comes from Chapter 4: each consumer’s requests are made with its own provider credentials, and a consumer without credentials gets an exception, never a shared account. That is the strongest kind of control, the kind where the data isn’t reachable, and it holds only as long as there is no fallback.
How you know: for every route with a parameter in it, a test in which the wrong caller asks.
php artisan route:list --path=api
Every {parameter} in that output is a question: who is allowed to put which value here? And for licenses, the test from Chapter 4 that checks which token the provider was called with. (Laravel documentation: Authorization › Writing Policies.)