tourze/json-rpc-sign-bundle
A Symfony bundle that adds request signing and verification to JSON-RPC services, helping secure client-server communication with configurable signature handling. Integrates with Tourze's JSON-RPC stack for tamper-evident API calls.
Read the diff before you read the docs. The 0.1.0 release was published with an empty notes body — the only artifact is the compare link (0.0.3...0.1.0). Before adding this to a project, run composer require tourze/json-rpc-sign-bundle:^0.1.0 in a scratch app and diff the two tags locally:
git clone https://github.com/tourze/json-rpc-sign-bundle
git diff 0.0.3..0.1.0 -- src/ composer.json
A 0.0.x → 0.1.x bump in a pre-1.0 package almost always means a config-key rename, a changed default algorithm, or a signature-payload change. Those are exactly the changes that silently break a working integration, so budget an hour for this.
Where to look first, in order:
composer.json — check whether it pulls symfony/framework-bundle or symfony/dependency-injection with a hard constraint. Despite the -bundle suffix, what you actually want in Laravel is the underlying signer/verifier services, not the DI extension.src/DependencyInjection/Configuration.php — the Symfony config tree is the de-facto option reference. Every knob the bundle exposes lives here, and the README usually lags it.src/Resources/config/services.yaml — shows which concrete class is bound to each interface, i.e. your extension points.First use case: signing outbound JSON-RPC calls to a partner gateway. Concretely, resolve the signer from the container (or instantiate it directly with a shared secret) and wrap your HTTP call so the signature is computed over the exact body bytes you send:
$payload = json_encode($rpcBody, JSON_UNESCAPED_SLASHES);
$headers = $signer->sign($payload); // timestamp, nonce, signature headers
Http::withHeaders($headers)->withBody($payload, 'application/json')->post($endpoint);
If you only need this, skip the bundle registration entirely and depend on the underlying library — you'll avoid dragging Symfony's container into a Laravel app for one HMAC.
Decorate your JSON-RPC client, don't sprinkle signing calls. Whether you use laravel/http, Saloon, or a hand-rolled client, put signing in one place — a Guzzle middleware or a Saloon request plugin — so every RPC method is signed identically. Signing at the call site is how you end up with one endpoint whose body was serialized with different flags and therefore fails verification.
Verify inbound with middleware, before the RPC layer. For webhook or callback routes, register a route middleware that reconstructs the signature from the raw request body:
$raw = $request->getContent(); // not $request->all()
abort_unless($verifier->verify($raw, $request->headers->all()), 401);
Return 401 on signature failure and 400 on malformed JSON — collapsing the two makes debugging partner integrations miserable.
Per-endpoint configuration, not global config. Secrets, algorithm, and header names usually differ per counterparty. Model this as a config array keyed by integration name and bind the right signer instance via a small factory or a container when() context:
$this->app->when(AcmeRpcClient::class)
->needs(SignerInterface::class)
->give(fn () => $this->app->make(SignerFactory::class)->for('acme'));
Freeze the clock and nonce source in tests. The two things that make signature tests flaky are time and randomness. Extract a ClockInterface and a nonce generator, bind deterministic fakes in the test container, and assert against a hardcoded expected header value. A test that only asserts "verification passes" won't catch a canonicalization regression; a test with a golden signature string will.
Log verification failures at warning with the reason, not the body. You need to distinguish "bad signature" (config/key mismatch) from "stale timestamp" (clock drift) from "replayed nonce" (a real attack or a retry bug). Never log the secret or the full signed payload — payloads in this domain routinely carry credentials.
Store nonces where multiple app servers can see them. If the package ships an in-memory replay guard, it is single-process only. Swap it for a cache-backed store (Cache::store('redis')->add("nonce:{$n}", 1, $ttl)) with a TTL matching your accepted timestamp window.
-bundle suffix is a trap in Laravel. Symfony bundles are bootstrapped by Kernel::registerBundles(), which Laravel doesn't have. If the classes you need only exist as DI-managed services, either instantiate them manually or write a thin service provider that re-declares the wiring — don't try to shoehorn Bundle registration into a provider's register().json_encode flag difference (JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE), a float serialized as 1 vs 1.0, an empty array encoded as [] vs {}, or header keys lowercased on one side. Sign and send the same byte string; never re-encode between signing and transport."tourze/json-rpc-sign-bundle": "0.1.*" rather than ^0.1. With no release notes, a 0.1.1 patch could still change a default you depend on. Re-read the diff before every bump and re-run your golden-signature test.config:cache. Anything placed under config/*.php is baked into the cached config file. Keep the actual secret in .env and reference it via env() only inside the config file — never pass secrets into a config array built at runtime from a DB, or config:cache will silently freeze a stale value.services.yaml are your seam — a custom SignerInterface implementation lets you support a new algorithm or header scheme without forking. Bind yours in a service provider after the package's provider so yours wins.How can I help you explore Laravel packages today?