Skip to content

Add signed actions; Livewire v4 Support and Documentation#3

Open
h4or wants to merge 12 commits intowire-elements:mainfrom
h4or:main
Open

Add signed actions; Livewire v4 Support and Documentation#3
h4or wants to merge 12 commits intowire-elements:mainfrom
h4or:main

Conversation

@h4or
Copy link

@h4or h4or commented Feb 13, 2026

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)" to delete(999)).

Targets #2

How it works

  1. Mark sensitive methods with #[Signed]
  2. Use @livewireAction('delete', $post->id) in Blade instead of inline calls
  3. The directive signs the method, parameters, and component ID with the app key
  4. Direct calls to signed methods are blocked - only verified payloads are accepted

Payload expiration

Optional TTL support prevents stale payloads from being replayed:

LivewireStrict::signedActions(ttl: 300); // 5-minute expiry

The expiration timestamp is part of the HMAC, so attackers cannot extend it.

Test coverage

10 new tests covering:

  • Direct call blocking
  • Valid signature verification
  • Tampered parameters rejection
  • Wrong component ID rejection
  • Non-signed method passthrough
  • Feature toggle behavior
  • Namespace scoping
  • Malformed payload handling
  • Signed payload on non-signed methods
  • TTL expiration and tampered expiry

h4or added 4 commits February 13, 2026 21:47
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.
@h4or h4or changed the title Add signed actions; Livewire v4 Support, Documentation Add signed actions; Livewire v4 Support and Documentation Feb 13, 2026
@h4or h4or mentioned this pull request Feb 13, 2026
@PhiloNL PhiloNL requested a review from Copilot February 13, 2026 21:38
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Livewire ComponentHook plus a Blade @livewireAction directive 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.0 and 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.

@h4or
Copy link
Author

h4or commented Feb 13, 2026

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
@PhiloNL
Copy link
Contributor

PhiloNL commented Feb 13, 2026

@h4or all good 😄 let me know if it's ready for another look

@h4or
Copy link
Author

h4or commented Feb 13, 2026

Yup, I did go over the issues and implemented the suggestions.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@h4or
Copy link
Author

h4or commented Feb 13, 2026

Damn, I still gotta learn more tho haha.. Anyway I adjusted the issues.

h4or and others added 3 commits February 14, 2026 00:54
- 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
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

h4or added 3 commits February 14, 2026 11:31
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
@h4or
Copy link
Author

h4or commented Feb 14, 2026

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants