ibrows/hmac-bundle
ibrows/hmac-bundle provides HMAC-based authentication for Symfony/PHP applications, letting you sign and verify requests with shared secrets to secure APIs and protect endpoints from tampering and unauthorized access.
First, accept the framing. ibrows/hmac-bundle is a Symfony bundle, not a Laravel package. composer require will install it, but there is no service provider, no middleware, no autoloading into Laravel's container, and nothing will execute. Do not try to wire symfony/framework-bundle into a Laravel app to satisfy it. Your first move is to read the source as a specification, not to make it run.
Read the source before anything else. There is no published documentation and zero community usage, so the code is the only ground truth. Open, in this order:
IbrowsHmacBundle.php — see what it registers (event subscribers, DI extensions).DependencyInjection/Configuration.php — the config tree reveals real option names, defaults, and whether a secret is required.Resources/config/services.xml (or .yml) — the actual service IDs and wiring. This is the real "public API."hash_equals().composer.json constraints. A 0-star, Symfony-2/3-era bundle is very likely pinned to symfony/symfony: ~2.x or PHP 5.6/7.x and will not install on modern Symfony or PHP 8.2+. If so, stop trying to install and copy the logic instead.Extract the contract, not the code. Capture the exact inputs to the signature: HTTP method, path, timestamp, nonce, body digest, header names, encoding, and field order. Write this down as a one-page spec. That spec is what you'll implement in Laravel and hand to integrators — the package itself is disposable.
First use case: one endpoint, one caller. Spike a Signer service and a VerifyHmacSignature middleware against a single test route. Get one green sign→verify cycle end-to-end before touching anything else. The fastest way to reverse-engineer the canonical string the bundle expects is to sign on one side and watch verification fail (or succeed) on the other — mismatches surface immediately.
Verify against your stack early. Run composer require in a throwaway directory against your target Laravel/PHP version. If it installs, inspect what it actually pulled in. If it doesn't, that confirms the porting path and you can move on without further debate.
Start with a single low-risk endpoint and a single internal client. Do not roll the middleware out across all routes until you have one green cycle, a captured example of the exact canonical string being hashed, and a fixed test vector.
The realistic architecture. Port the signing/verification into three pieces: a Signer service (canonical construction + hash_hmac), a VerifyHmacSignature middleware (extract headers, replay check, timing-safe compare), and a service provider binding the named middleware into a route group. Discard the bundle.
Canonical string construction (client side). Never sign the raw request; sign a deterministic canonical string. A working shape:
$timestamp = time();
$nonce = bin2hex(random_bytes(16));
$bodyHash = hash('sha256', $rawBody);
$canonical = implode("\n", [
strtoupper($method), // POST
$path, // /api/orders (path only, no query, or sorted query — pick one, document it)
(string) $timestamp,
$nonce,
$bodyHash,
]);
$signature = hash_hmac('sha256', $canonical, $secret);
Attach X-Hmac-Timestamp, X-Hmac-Nonce, X-Hmac-Signature, and (for multi-tenant) X-Hmac-Key-Id or kid.
Inbound verification (server side). Register the middleware on protected route groups only — not globally — so public routes don't carry the cost:
class VerifyHmacSignature
{
public function handle($request, Closure $next)
{
$sig = $request->header('X-Hmac-Signature');
if (!$sig) {
return response()->json(['error' => 'missing_signature'], 401);
}
$ts = (int) $request->header('X-Hmac-Timestamp');
if (abs(time() - $ts) > config('hmac.tolerance', 300)) {
return response()->json(['error' => 'stale_timestamp'], 401);
}
$kid = $request->header('X-Hmac-Key-Id');
$secret = $this->keyResolver->resolve($kid); // rotate-friendly
$canonical = /* must match client construction exactly */;
$expected = hash_hmac('sha256', $canonical, $secret);
if (!hash_equals($expected, $sig)) {
return response()->json(['error' => 'invalid_signature'], 401);
}
if (!$this->nonceStore->claim($nonce, $ts + 300)) {
return response()->json(['error' => 'replay_detected'], 401);
}
return $next($request);
}
}
Hash the raw body before Laravel parses it. $request->getContent() gives you the raw stream, but only if nothing has consumed it. Read it in the middleware before any body parsing, or you'll be signing a re-serialized array with a different byte sequence. This is the single most common cause of signature mismatches in ported implementations.
Replay protection. Timestamp tolerance narrows the window; the nonce store closes it. Use a shared, low-latency store (Redis) keyed by nonce with a TTL equal to twice the tolerance window. SET NX with expiry gives you atomic claim. In-process or in-memory stores break as soon as you scale to more than one node.
Key resolution and rotation. Keep a map kid => secret, accept old-and-new during a rotation window, and let clients emit the new kid first. If you defer kid support until after clients ship, retrofitting it is a coordinated redeploy — build it in from day one.
Testing. Pin a fixed vector: known canonical string + key + expected base64/hex signature. Assert both signer and verifier produce it. Inject a fake clock so timestamp tests aren't flaky. Add a route coverage test that enumerates Route::getRoutes() and asserts every public route either carries VerifyHmacSignature or is on an explicit allowlist — this catches silent bypasses when routes are added outside the protected group.
Rollout. Run log-only (compute, compare, log match/mismatch, always allow) in staging, then on one production endpoint, then enforce. Emit canonical-string dumps to non-prod logs only — never to production.
Maintenance and provenance are the headline risk. Zero stars, zero dependents, no visible maintenance, no CVE channel, no security disclosure path. HMAC verification is security-critical; treat the package strictly as a reading reference and own the implementation. Retain the MIT notice if you copy logic.
Compatibility is likely already broken. Verify composer.json constraints against your PHP/Symfony versions before any spike. Expect Symfony 2/3 and ContainerAware-style code that won't run on modern versions — which is fine, because you're porting, not installing.
Canonicalization is where 90% of failures live. Almost every mismatch traces to: query-string ordering (sort vs. preserve), URL-encoded vs. raw path, trailing slash differences, duplicate parameters, whitespace, or a body re-serialized by a framework layer. Fix the spec once, document it, and test the edge cases (empty body, chunked transfer, JSON with reordered keys) explicitly.
Timing-safe comparison is non-negotiable. Use hash_equals(). If the bundle uses == or ===, that alone is a reason not to trust it verbatim — port the algorithm, not the comparison.
Check the default digest and key length. Older bundles sometimes default to MD5. Standardize on SHA-256 with 32+ byte secrets and document the algorithm in your published signing contract.
Proxies and middleware ordering rewrite bodies. A load balancer that decompresses or a middleware that trims whitespace produces a different byte sequence than what was signed. Verify the signature before any body-transforming middleware, and confirm proxy behavior with a captured raw-body test.
Clock skew causes 401 bursts. Pick a tolerance (±300s is common), document it, and return a distinct error code (stale_timestamp vs invalid_signature) so integrators can diagnose it. Log client clock drift.
Decide fail-open vs. fail-closed explicitly. If the nonce store is down, fail-open means auth bypass, fail-closed means an API outage. Default to fail-closed on write paths and alert loudly. Write this decision down.
Don't sign the parsed request. The Laravel Request object normalizes input. Sign the raw bytes you received. If you're generating outbound signatures, sign the exact byte payload you'll send — not a re-encoded array.
Opaque debugging is a support cost. Log the canonical string, expected signature, and received signature at debug level in non-prod only. In production, log only a correlation ID and a pass/fail reason — never the secret or the canonical string.
Extension point is the key resolver and nonce store. Design those two as interfaces from the start. That's where rotation, multi-tenancy, and scaling concerns live, and it's the cheapest thing to mock in tests.
How can I help you explore Laravel packages today?