Add signed actions; Livewire v4 Support and Documentation#3
Add signed actions; Livewire v4 Support and Documentation#3h4or wants to merge 12 commits intowire-elements:mainfrom
Conversation
Introduces #[Signed] attribute for Livewire component methods that makes action calls tamper-proof using HMAC-SHA256 signatures. When a method is marked as #[Signed], direct frontend calls are blocked. Actions must go through the @livewireAction Blade directive, which signs the method name, parameters, and component ID with the app key.
There was a problem hiding this comment.
Pull request overview
This PR adds a new “Signed Actions” security feature to livewire-strict to prevent client-side tampering of Livewire action parameters by requiring HMAC-signed payloads for marked methods, along with documentation updates and Livewire v4 dev support.
Changes:
- Introduces
#[Signed]methods enforced via a LivewireComponentHookplus a Blade@livewireActiondirective that emits signed action payloads. - Adds exceptions + a new test suite covering signature verification, tampering, scoping, and TTL expiry behavior.
- Expands dev dependency support to Livewire
^3.5|^4.0and adds new docs pages.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/LivewireStrictServiceProvider.php | Registers the signed-actions hook and introduces the @livewireAction Blade directive. |
| src/LivewireStrict.php | Adds LivewireStrict::signedActions() toggles + includes it in enableAll(). |
| src/Features/SupportSignedActions/SupportSignedActions.php | Implements signature generation/verification + blocks direct calls to #[Signed] methods. |
| src/Features/SupportSignedActions/InvalidSignedActionException.php | Adds a dedicated exception for invalid/missing signatures or malformed payloads. |
| src/Features/SupportSignedActions/ExpiredSignedActionException.php | Adds a dedicated exception for TTL-expired signed actions. |
| src/Features/SupportSignedActions/UnitTest.php | Adds coverage for direct-call blocking, signature validation, tampering, scoping, malformed payloads, and TTL. |
| src/Attributes/Signed.php | Introduces the #[Signed] attribute used to mark methods requiring signed payloads. |
| docs/signed-actions.md | Documents the new signed-actions feature and recommended TTL usage. |
| docs/locked-properties.md | Adds documentation for the existing locked-properties feature. |
| docs/README.md | Adds a docs index + quick start guidance for enabling features. |
| composer.json | Adds a test script and expands Livewire dev dependency to include v4. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I am looking into the things Copilot mentioned. |
Fixes: - methodIsSigned() now requires isPublic() and !isStatic() to prevent fatal access errors when non-public methods are marked #[Signed] - Added guard for missing/invalid $params[0] in __callSigned to return a controlled exception instead of a 500 error - __callSigned method name collision now throws a clear LogicException instead of silently returning, which would leave signed methods uncallable on the affected component - Removed stale @param docblock referencing a $ttl parameter that did not exist on generateSignedPayload() New: - #[Signed] attribute now accepts an optional ttl parameter for per-method expiration, e.g. #[Signed(ttl: 60)] - Per-method TTL takes precedence over the global TTL set in LivewireStrict::signedActions(ttl: 300) - @livewireAction Blade directive automatically resolves the correct TTL from the component's method attributes - 3 new tests for per-method TTL (override, within window, fallback) - Updated signed-actions.md with per-method TTL documentation
|
@h4or all good 😄 let me know if it's ready for another look |
|
Yup, I did go over the issues and implemented the suggestions. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Fix #[Signed(ttl: 0)] creating an immediately expired payload instead of disabling expiration as documented. getMethodTtl() now returns null when ttl is explicitly set to 0, meaning "no expiration even if global TTL is set". - Make Signed attribute extend Livewire\Features\SupportAttributes\Attribute for consistency with the Unlocked attribute, ensuring it is accessible through Livewire's attribute system ($component->getAttributes()). - Extract duplicated checkIsRequired() logic from SupportSignedActions and SupportLockedProperties into a shared MatchesComponents trait. - Add test for #[Signed(ttl: 0)] verifying it bypasses expiration even with a global TTL configured. - Add test for signed methods with no parameters to verify they work correctly through signed payloads. - Clarify docs that #[Signed] works on no-parameter methods even though it is not required for them.
|
Damn, I still gotta learn more tho haha.. Anyway I adjusted the issues. |
- Fix ttl: 0 creating immediately expired payloads instead of disabling expiration. Both global (LivewireStrict::signedActions(ttl: 0)) and per-method (#[Signed(ttl: 0)]) now correctly resolve to no expiration. - Add Signed::NO_EXPIRATION constant for explicit, self-documenting opt-out of expiration instead of the magic number 0. - Extract TTL validation and normalization into a reusable NormalizesTtl trait (rejects negative values, normalizes 0 to null). Used by SupportSignedActions, the Signed attribute, and LivewireStrict facade. - Add tests: ttl: 0 per-method, ttl: 0 global, negative TTL rejection. - Update docs to use Signed::NO_EXPIRATION in examples.
- Extract SignedPayload value object from the hook (encode, verify, forComponent, toAction) - Mirror SupportLockedProperties pattern: lean ComponentHook with early-return guards - Replace raw ReflectionMethod with Livewire's getAttributes() API throughout - Centralize TTL validation and per-method resolution in the Signed attribute - Add app.key guard (RuntimeException if missing) - Move exceptions to Exceptions/ subfolder - Remove NormalizesTtl trait (inlined and simplified) - Remove Signed::NO_EXPIRATION constant (0 and null both mean no expiration) - Convert all raw if/throw to Laravel throw_if/throw_unless helpers - Add full docblocks for TTL semantics on Signed attribute and LivewireStrict facade - Add 12 new tests: multi-method TTL isolation, non-string param guard, payload replay, missing app.key, __callSigned collision, toAction round-trip, and TTL edge cases - Update docs to match code (replay note, APP_KEY requirement, TTL examples)
Update PHP versions in GitHub Actions workflow
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Decoded payloads are user-controlled input, so fields like method, sig, and id could be arrays or integers instead of strings, causing TypeError in hash_equals() or the exception constructor. Now validates types before signature verification: - id, method, sig must be scalar (cast to string) - params must be an array - exp, if present, must be an int Any type mismatch throws InvalidSignedActionException with a clean error message. Added regression tests for each malformed-payload case.
- Derive a purpose-specific HMAC key from APP_KEY instead of using the raw key directly, preventing cross-system signature confusion - Add security tests: raw-key rejection, empty payloads, null values, empty method name, extra field handling, key rotation, runtime toggle - Document __callSigned collision guard and negative TTL validation - Update How It Works section to reflect domain-separated key derivation
- Extract duplicated payload/HMAC logic into private instance methods (payloadData(), sign()) to reduce code noise (DRY) - Reorder methods: public static -> public instance -> private static -> private instance (standard PHP convention) - Make handleSignedCall() and methodIsSigned() private (minimal surface) - Combine chained filter() calls into single filter in methodIsSigned() and Signed::resolveMethodTtl() - Use nullsafe operator in resolveMethodTtl() ($signed?->ttl) - Replace implicit TTL falsy coercion ($ttl ?) with explicit $ttl > 0 - Add test for __callSigned called with no params - Add documentation comments for design tradeoffs (public constructor, replay behavior, TTL 0->null normalization, $__livewire dependency) - Update docs to reflect current architecture
|
@PhiloNL I am terribly sorry for all the noise and messy parts from my side. I hope I now have cleared most of the stuff and adjusted it to mirror the working of the LockedProperties in general. |
This PR extends livewire-strict with a new security feature and project-wide improvements.
Signed Actions
Adds tamper-proof Livewire action calls using HMAC-SHA256 signatures. This prevents attackers from modifying action parameters in the DOM (e.g., changing
wire:click="delete(5)"todelete(999)).Targets #2
How it works
#[Signed]@livewireAction('delete', $post->id)in Blade instead of inline callsPayload expiration
Optional TTL support prevents stale payloads from being replayed:
The expiration timestamp is part of the HMAC, so attackers cannot extend it.
Test coverage
10 new tests covering: