Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Http Hmac Php Laravel Package

acquia/http-hmac-php

PHP implementation of HTTP HMAC authentication for securing service-to-service API requests. Sign and verify requests using shared secrets, with support for PSR-7 and framework request objects, making it easy to add tamper-resistant authentication to PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a library, not a Laravel package. There is no service provider, no config/http-hmac.php to publish, no facade, no artisan command. composer require acquia/http-hmac-php:^6.2 and you are on your own for glue — plan for that in the estimate; the glue is ~60 lines and the key management is the rest.

Where to look first, in this order:

  1. src/Acquia/Hmac/ — the whole library is a handful of classes. Read RequestSigner, AuthorizationHeaderBuilder, RequestAuthenticator, KeyInterface, KeyLoaderInterface. That is the entire mental model.
  2. The adapter subnamespaces (Psr7/Request/Symfony) — pick the PSR-7 path in Laravel. Ignore the Symfony security classes; symfony/security-bundle is only a suggested dependency and pulling it in for HmacToken/HmacAuthenticationEntryPoint is not worth the weight.
  3. The HTTP HMAC spec the README links — you need the canonical-string definition when a partner returns 401 and you have to prove which side is wrong.

First use case — sign one outbound request and eyeball the headers. Do this in php artisan tinker before you write any middleware:

$key    = new Acquia\Hmac\Key('my-key-id', config('services.partner.secret'));
$signer = new Acquia\Hmac\RequestSigner($key, 'MyRealm');   // confirm arg order in the class
$psr7   = (new \GuzzleHttp\Psr7\Request('GET', 'https://api.partner.test/v1/things?b=2&a=1'));
$signed = $signer->sign($psr7);

dump($signed->getHeaders());  // Authorization, X-Authorization-Timestamp, X-Authorization-Content-SHA256, nonce

If you can see a plausible Authorization value and a timestamp, you are past the hard part. The second half — receiver-side verification — is a Route::post('/webhook', ...) with RequestAuthenticator in front of it, and you should build both halves in the same branch so you can test them against each other.

Implementation Patterns

1. Outbound: a dedicated signed client, not a global middleware. Do not push an HandlerStack middleware onto every Http:: call in the app — you will end up signing requests to unrelated services. Bind one named client:

// app/Support/PartnerClient.php
final class PartnerClient
{
    public function __construct(private \Illuminate\Http\Client\Factory $http) {}

    public function client(): \Illuminate\Http\Client\PendingRequest
    {
        $stack = \GuzzleHttp\HandlerStack::create();
        $stack->push(new HmacMiddleware($this->key()), 'hmac');   // signing middleware wrapper

        return $this->http->withOptions(['handler' => $stack])
            ->baseUrl(config('services.partner.base_url'))
            ->acceptJson();
    }
}

For a small number of endpoints, a plain service class that takes a PSR-7 request, signs it, and sends it with GuzzleHttp\Client is honestly less friction than fighting PendingRequest's body lifecycle. Both are fine; pick one and be consistent.

2. Middleware ordering is the whole ballgame. The signature covers the body hash. So the signing middleware must run after the body is finalized (json(), withBody(), attach()) and before the request goes on the wire. Push it as the outermost middleware, or sign in a terminal .send() wrapper. If you sign first and set a body later, the receiver's body hash check fails and you get a generic 401.

3. Inbound: verification middleware behind a custom guard. Laravel's Illuminate\Http\Request is not PSR-7. Convert with symfony/psr-http-message-bridge (PsrHttpFactory) — it is a lightweight, already-adjacent dependency — then hand the PSR-7 request to RequestAuthenticator. Wrap it as a middleware that, on failure, returns 401 with no detail and logs the detail server-side.

4. Pluggable key provider = your rotation story. Implement KeyLoaderInterface::load(string $keyId) returning a Key or null. Back it with config('services.partners') for the small case, or a partner_keys table (key_id, secret encrypted cast, active, rotated_at) once you have more than a couple of integrations. Load by key id from the incoming signature, never by "the one key we know" — that is what makes zero-downtime rotation possible. Cache the lookup (Cache::remember, 5 min) so verification doesn't hit the DB per request.

5. Replay protection is two mechanisms, and you own one of them. The library enforces the timestamp window (reject anything outside the receiver's tolerance). The nonce is only useful if you store it. Add:

if (! Cache::add("hmac:nonce:{$nonce}", true, now()->addMinutes(15))) {
    abort(401);  // replayed
}

Use add() (atomic set-if-absent), not has() + put(). Shared cache store across all app instances, or the protection is per-pod and effectively nil.

6. Custom signed headers, when a partner demands it. AuthorizationHeaderBuilder is the extension point for which headers land in the canonical string. Use it when a partner spec requires Content-Type or a custom X-Partner-Id to be signed. Don't extend it speculatively — every custom header is another place canonicalization can drift from the receiver.

7. Testing pattern that actually catches bugs. Don't mock the signer. Build a fixture: a known secret, a fixed request (method, path, query, headers, body), and an asserted canonical string. Snapshot the resulting Authorization header. When a partner says "your signature is wrong", diffing against that fixture is how you find the trailing-slash or header-casing bug in ten minutes instead of a day. Also test the failure paths — expired timestamp, replayed nonce, unknown key id, tampered body — because those are the branches that rot silently.

Gotchas and Tips

  • PHP 8.4 subclass audit. If you extend AuthorizationHeaderBuilder, RequestSigner, ResponseSigner, HmacToken, HmacAuthenticationEntryPoint, or any Malformed*Exception, every override of a nullable parameter needs an explicit ?Type or you emit deprecations. This bites hardest in exception subclasses, which are only constructed on error paths and therefore never exercised by green test suites. grep -rE "extends (AuthorizationHeaderBuilder|RequestSigner|ResponseSigner|HmacToken|HmacAuthenticationEntryPoint|Malformed)". If you run tests with deprecations promoted to exceptions, this is a hard failure, not a warning.
  • Stream pointer / body hash. PSR-7 streams are stateful. If the body stream has already been read, getContents() returns '' and you sign an empty-body hash — silently, with a valid-looking signature. Rewind before hashing, or build the body as a string. Streamed uploads (multipart, file attaches) are the classic failure here.
  • Retries re-sign — or don't. Laravel's Http::retry() replays the constructed request. If your signer ran once, the retry carries a stale timestamp; if it reruns, the body may be consumed. Either way, wrap retries outside signing and re-sign per attempt, and keep the timestamp window in mind for slow backoffs.
  • Clock skew. Nothing in the library fixes a container whose clock has drifted. NTP/chrony on every node, and confirm the receiver's tolerance (commonly 15 minutes; some partners use 5). A "works locally, 401 in staging" is very often a drifted VM clock.
  • Canonicalization drift is the #1 real-world failure, and both sides always think they're right. Suspects: query-string ordering and + vs %20; header name casing and whitespace around values; duplicate headers; Host including or omitting a non-default port; trailing slash on the path; Content-Type: application/json; charset=utf-8 vs bare. Log a hash of the canonical string plus the key id on both sides — never the secret, never the full string in production logs.
  • 401/403 misdiagnosis. A partner 401 looks like an auth bug and sends people hunting through guards and tokens. Make your client throw a dedicated SignatureRejectedException that surfaces key id, timestamp, and canonical hash. That one class saves hours.
  • The 401 with no diagnostic is intentional. Don't be tempted to echo "timestamp expired" back to the caller — that's an oracle. Keep the detail in logs, return a flat 401.
  • Secrets: env() at config-load only, encrypted cast if in the DB, never in logs, never in a queued job payload. Secret distribution and rotation across environments is where this class of integration actually fails; the signing math is the easy part. Decide the rotation procedure (two active key ids, retire the old after the receiver's window) before you ship.
  • psr/http-message is ^1.0 || ^2.0 — adoption won't force a PSR-7 major bump. But your own code that constructs PSR-7 requests must match whichever major you have; mixing v1/v2 in one process is a fatal.
  • Do not use this from a browser or mobile app. Shared-secret HMAC means the client holds the secret. Server-to-server only (and ideally over TLS regardless — HMAC gives you integrity and authentication, not confidentiality, and does not stop replay of the body beyond the timestamp/nonce window).
  • PHP 8.0–8.3 are covered by inference, not by the release CI matrix (7.4, 8.4, 8.5). Fine in practice, but if you're on 8.2 and something looks version-shaped, you're the first to see it.
  • This is not an auth framework. It answers "was this request signed by the holder of key X, recently, once?" It does not do authorization, users, scopes, or token expiry. Pairing it with a Laravel guard is a decision you own — keep HMAC verification in middleware and do authorization separately, or you'll end up with a guard that can't express either concern cleanly.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource