aporat/laravel-auth-signature
Laravel middleware that authenticates incoming HTTP requests using signed signatures, letting you verify request integrity and identity without exposing credentials on each call.
composer require aporat/laravel-auth-signature — package discovery registers the service provider, the ValidateAuthSignature middleware alias and the Http::withSignature() macro, so there is nothing to wire up in config/app.php.php artisan vendor:publish and pick the AuthSignature provider) and define at least one client: client_id, client_secret, and the auth version string you intend to use. Both sides of the integration must agree on the version.ValidateAuthSignature to an api route group (or a single route) and confirm an unsigned curl now fails. Then, from the calling Laravel app, replace Http::post(...) with:Http::withSignature('my-client-id')->post('https://api.example.test/orders', $payload);
If it round-trips, the two halves are configured consistently. That single call is the whole happy path — the macro signs the final method, path, query string and JSON/form body at send time, using the same SignatureGenerator the middleware validates with, so there is no canonicalisation to keep in sync by hand.
Aporat\AuthSignature\Client\RequestSigner (the single signing implementation shared by the macro and the command), SignatureGenerator::stringToSign() (canonical string) and SignatureGenerator::generate().php artisan auth-signature:sign --debug. It prints the X-Auth-* headers plus the canonical string being signed, without ever printing the secret — so it is safe to run on a shared terminal and paste into a ticket.Outgoing calls (the default path). Use the macro everywhere you call a signature-protected endpoint. It signs at send time, which means query parameters, ->asForm() vs JSON, and payloads assembled by chained ->withQueryParameters()/->withBody() are all covered without ordering rules. Keep the client ID in config, not inline strings:
Http::withSignature(config('services.partner.client_id'))
->acceptJson()
->retry(3, 200)
->post($url, $payload);
Per-request secret override. The third argument of withSignature($clientId, $authVersion, $secret) replaces the stored client_secret for that call only. Two good uses: multi-tenant integrations where each tenant supplies its own secret, and secret rotation — start signing with the new secret while the receiver still accepts the old one, then flip over.
Non-Laravel / PSR-7 stacks. When you are not going through the HTTP client (a custom Guzzle handler, a third-party SDK that takes a RequestInterface, an outbound webhook relay), use Aporat\AuthSignature\Client\RequestSigner on the PSR-7 request instead of hand-rolling headers. Resist reimplementing the canonical string; both the macro and the command delegate to it, and anything you write yourself will drift from ValidateAuthSignature at the first edge case.
Integration tips
--curl produces a ready-to-run reproduction command, --debug/-v shows the canonical string. When a partner reports a failure, ask for their --debug output and diff it against your own invocation — the mismatch is almost always path, query encoding or body encoding, and it is visible in one glance.Http::fake() plus Http::assertSent(fn ($request) => $request->hasHeader('X-Auth-Signature')) (adjust to the header the command prints) is enough to prove the macro is attached. For canonical-string assertions, call SignatureGenerator::stringToSign() directly rather than asserting a hard-coded hash — the hash will churn on any canonicalisation change and the string is the meaningful thing to pin.bootstrap/app.php snippet (in withExceptions()) to map the middleware's exception to its proper 4xx code — apply it before you go live, and use the class name from the README rather than guessing at it. Doing this also makes the failure legible in logs and lets clients implement sane retry logic (they should not retry a 401).date -u on both hosts before debugging anything else, and keep NTP enabled. Long retry backoffs have the same failure mode — a request that waits six minutes between attempts is signed with a stale timestamp, so cap backoff below the window.->retry() re-signs each attempt and gets a fresh timestamp. Anything you build that reuses a prepared request object must respect the same rule.->asForm() and ->json() are different canonical inputs, and re-encoding JSON yourself with different flags (JSON_UNESCAPED_SLASHES, key ordering) produces a different string. Let the macro see the real payload; if you must send a raw body, send byte-identical content on both sides. --debug is how you confirm which form was actually signed.--debug/-v freely — secrets are never printed. The canonical string is safe to paste into a ticket. Still, treat it as internal: it reveals endpoint shapes and payload structure.illuminate/http, illuminate/routing, illuminate/console and guzzlehttp/guzzle directly. That is good for consumers (no more accidental reliance on a transitive Guzzle), but it means your Laravel major must satisfy those constraints — check before bumping the package on an older app. PHPUnit ^11 || ^12 only matters if you fork the package or run its suite; it does not affect your app's test setup..gitattributes). Don't expect vendor/aporat/.../tests to exist, and don't write tooling that assumes it. If you need to verify behaviour, the new end-to-end route tests live in the repository, not in the tarball — clone the repo to read them.SignatureGenerator::generate($..., $secret) for rotation-style signing outside the HTTP client, stringToSign() for logging/diffing, and RequestSigner as the seam if you ever need to sign a non-Laravel request. Everything else — new headers, alternate canonicalisation — should be treated as a fork rather than a local patch, or the two halves of the integration will silently diverge.How can I help you explore Laravel packages today?