feat(http): middleware, auth primitives, and a lazily resolved logger - #1
Conversation
Discovery builds the router while the container is still being assembled, before the initializers that provide the logger have themselves been found. Asking for one in the constructor made every application installing this plugin fail to boot — including ones with no routes at all, since discovery constructs the router either way. The registries in the framework and in the tasks plugin already resolve lazily for exactly this reason; this one did not. The existing tests missed it because they build a container of their own with a logger already in it, which is not the container discovery has.
A handler had no way to tell who was asking, and the familiar answers are all unsafe in a process that serves thousands of requests without restarting: $_COOKIE holds whatever the CLI process started with, setcookie() writes nowhere, and $_SESSION is one session shared by everybody who ever connects. So the pieces that are safe, and only those: a cookie read off this request's own header, a bearer token from Authorization, a cookie set on the response with HttpOnly and SameSite=Lax already on, and comparisons that run in constant time. `===` on a signature stops at the first byte that differs, which is measurable over enough requests. Cookie values are rawurlencoded rather than urlencoded, since a browser hands "+" back as a plus rather than a space. Nothing here remembers anything between requests, which is the property that makes it safe at all. The README now says why, and says not to reach for PHP's own session handling.
|
Pushed the stateless auth primitives onto this branch as well, since the plugin is not released with either yet. $request->cookie('session'); // off this request's Cookie header
$request->bearerToken(); // from "Authorization: Bearer ..."
Secret::matches($configured, $request->bearerToken() ?? '');
Secret::signed($request->body(), $request->header('X-Signature') ?? '', $secret);
return Response::noContent()->withCookie(new Cookie('session', $id, secure: true));Why these and not sessions. Everything here is stateless, which is the property that makes it safe in a process serving thousands of requests over weeks. Cookies default to The README now also says plainly that |
A check on whether a caller may ask at all has to be able to stop the work rather than disapprove of it afterwards, and there was nowhere to put one: a route was matched and its handler called. Middleware are listed on the route, built by the container, and wrapped outermost-first. Returning an answer instead of calling $next stops the request there — the handler never runs, and neither does anything behind it in the list. A handler's answer is normalised to a Response before the middleware see it, so one that wraps the answer is never handed whatever a handler happened to return.
|
Added middleware support on top, since a signature check has to be able to stop a request rather than disapprove of it afterwards — and there was nowhere to put one. #[Route(Method::POST, '/api/links', middleware: [VerifySignature::class])]First listed is outermost; returning an answer instead of calling A handler's answer is normalised to a 55 tests, phpstan clean. This PR is now three things — the boot fix, the auth primitives and middleware — because none of them is released yet and they only make sense together. |
|
🎉 This PR is included in version 1.1.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Installing this plugin made an application fail to boot:
Discovery constructs the router while the container is still being assembled — before the initializers that provide the logger have themselves been found. It does this whether or not the application has any routes, so every consumer was affected, not just ones using the plugin.
The registries in the framework (
EventsRegistry) and in the tasks plugin already resolve lazily for exactly this reason. This one did not.Why the tests did not catch it
They build a container of their own with a logger already in it — which is not the container discovery has. There are now two tests for the actual constraint: one builds a
Routeragainst an emptyGenericContainerand routes a request through it, and one checks the logger is still reached when a handler throws.🤖 Generated with Claude Code