Diffyne includes several security features to protect your application from common attacks and unauthorized state manipulation.
Diffyne's security model is based on:
- State Signing: HMAC signatures prevent state tampering
- Flexible Verification Modes: Balance security and usability
- Locked Properties: Server-controlled properties cannot be modified from client
- Method Whitelisting: Only explicitly marked methods can be invoked
- Rate Limiting: Prevents abuse and DoS attacks
Every component state is signed with an HMAC signature using your application key. This prevents clients from tampering with state data.
- Server sends state with signature:
{
"id": "diffyne-abc123",
"state": {
"count": 5,
"name": "John"
},
"signature": "a1b2c3d4e5f6..."
}- Client sends state back with signature:
{
"componentId": "diffyne-abc123",
"state": {
"count": 5,
"name": "John"
},
"signature": "a1b2c3d4e5f6..."
}- Server verifies signature:
- If signature matches: Request is processed
- If signature doesn't match:
403 Forbiddenerror
Diffyne offers flexible security configuration to balance security and usability:
What it does:
- ✅ Verifies signatures for property updates (
diff:model.live,diff:model.lazy) - ✅ Allows form submissions without strict signature verification
Best for: Most applications - provides security where it matters most while maintaining good UX.
Configuration:
// config/diffyne.php
'security' => [
'verify_state' => 'property-updates',
],Or via .env:
DIFFYNE_VERIFY_STATE=property-updates
What it does:
- ✅ Verifies signatures for ALL requests (form submissions + property updates)
- ✅ Uses lenient verification for form submissions (if enabled)
⚠️ May cause issues with form submissions if state reconstruction fails
Best for: High-security applications where you need maximum protection.
Configuration:
'security' => [
'verify_state' => 'strict',
'lenient_form_verification' => true, // Recommended
],What it does:
- ❌ Disables signature verification entirely
⚠️ Not recommended for production
Best for: Development only, or when you have other security measures in place.
// config/diffyne.php
'security' => [
// Signing key (defaults to APP_KEY)
'signing_key' => env('DIFFYNE_SIGNING_KEY'),
// Verify state signature mode: 'property-updates', 'strict', or false
'verify_state' => env('DIFFYNE_VERIFY_STATE', 'property-updates'),
// Allow lenient verification for form submissions (strict mode only)
'lenient_form_verification' => env('DIFFYNE_LENIENT_FORMS', true),
// Rate limiting (requests per minute)
'rate_limit' => env('DIFFYNE_RATE_LIMIT', 60),
],For most applications, use this configuration:
'security' => [
'verify_state' => 'property-updates',
'lenient_form_verification' => true,
'rate_limit' => 60,
],This provides:
- ✅ Security for property updates (where tampering is most likely)
- ✅ Smooth form submission experience
- ✅ Rate limiting to prevent abuse
Properties marked with #[Locked] cannot be updated from the client. This prevents tampering with server-controlled data.
use Diffyne\Attributes\Locked;
class PostList extends Component
{
#[Locked]
public array $posts = []; // Server-controlled
#[Locked]
public int $total = 0; // Server-calculated
public int $page = 1; // Client can change
}- Prevent Data Tampering: Users can't modify server data
- Protect Calculations: Totals, counts, etc. are server-only
- Enforce Business Logic: Only server can modify critical data
If a client tries to update a locked property:
// This will fail
fetch('/_diffyne/update', {
method: 'POST',
body: JSON.stringify({
property: 'posts',
value: [], // Trying to clear posts
// ...
})
});Response: 400 Bad Request with error: "Cannot update locked property: posts"
Only methods marked with #[Invokable] can be called from the client. This provides explicit security control.
use Diffyne\Attributes\Invokable;
class UserForm extends Component
{
#[Invokable]
public function save(): void
{
// ✅ Can be called from client
}
public function loadData(): void
{
// ❌ CANNOT be called from client
}
#[Invokable]
public function delete(): void
{
// ✅ Can be called from client
}
}- Explicit Control: You decide what's callable
- Prevent Unauthorized Actions: Internal methods are protected
- Clear Intent: Easy to see what's public API
// This will fail
fetch('/_diffyne/call', {
method: 'POST',
body: JSON.stringify({
method: 'loadData', // Not marked #[Invokable]
// ...
})
});Response: 400 Bad Request with error: "Method loadData is not invokable"
Diffyne includes built-in rate limiting to prevent abuse and DoS attacks.
// config/diffyne.php
'security' => [
'rate_limit' => env('DIFFYNE_RATE_LIMIT', 60), // requests per minute
],- Each IP address is limited to N requests per minute
- Exceeding the limit returns
429 Too Many Requests - Limits are per-route (update, call, etc.)
You can customize rate limits in your middleware or route configuration:
// In your service provider or middleware
RateLimiter::for('diffyne', function (Request $request) {
return Limit::perMinute(100)->by($request->ip());
});// In browser console
const component = document.querySelector('[diff\\:id]');
const componentId = component.getAttribute('diff:id');
const state = JSON.parse(component.getAttribute('diff:state'));
// Try to tamper with signature
fetch('/_diffyne/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
componentId: componentId,
property: 'page',
value: 2,
state: state,
signature: 'tampered_signature_12345', // ❌ Invalid signature
})
})
.then(r => r.json())
.then(data => {
console.log('Result:', data);
// Expected: 403 Forbidden - "Invalid state signature"
});const component = document.querySelector('[diff\\:id]');
const componentId = component.getAttribute('diff:id');
const state = JSON.parse(component.getAttribute('diff:state'));
const signature = component.getAttribute('diff:signature');
// Try to update locked property
fetch('/_diffyne/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
componentId: componentId,
property: 'posts', // Locked property
value: [], // Trying to clear
state: state,
signature: signature,
})
})
.then(r => r.json())
.then(data => {
console.log('Result:', data);
// Expected: 400 Bad Request - "Cannot update locked property: posts"
});const component = document.querySelector('[diff\\:id]');
const componentId = component.getAttribute('diff:id');
const state = JSON.parse(component.getAttribute('diff:state'));
const signature = component.getAttribute('diff:signature');
// Try to call non-invokable method
fetch('/_diffyne/call', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
componentId: componentId,
method: 'loadData', // Not marked #[Invokable]
params: [],
state: state,
signature: signature,
})
})
.then(r => r.json())
.then(data => {
console.log('Result:', data);
// Expected: 400 Bad Request - "Method loadData is not invokable"
});// ✅ Good
#[Locked]
public array $posts = [];
// ❌ Bad - allows tampering
public array $posts = [];// ✅ Good - public action
#[Invokable]
public function save(): void { }
// ❌ Bad - internal method
#[Invokable]
public function loadData(): void { }# ✅ Good - recommended for most apps
DIFFYNE_VERIFY_STATE=property-updates
# ✅ Good - maximum security
DIFFYNE_VERIFY_STATE=strict
# ❌ Bad - only for development
DIFFYNE_VERIFY_STATE=false// ✅ Good - reasonable limit
'rate_limit' => 60, // 1 request per second
// ❌ Bad - too high (allows abuse)
'rate_limit' => 10000,
// ❌ Bad - too low (breaks UX)
'rate_limit' => 5,# ✅ Good - use APP_KEY (strong, random)
DIFFYNE_SIGNING_KEY=
# ❌ Bad - weak key
DIFFYNE_SIGNING_KEY=secret123Even with locked properties, always validate:
#[Invokable]
public function updateName(string $name): void
{
// ✅ Always validate
$validated = $this->validate([
'name' => 'required|string|max:255',
]);
$this->name = $validated['name'];
}class ProductList extends Component
{
#[Locked]
public array $products = []; // Server loads
#[Locked]
public int $total = 0; // Server calculates
public string $search = ''; // Client can change
public function updatedSearch(): void
{
// Server reloads products based on search
$this->loadProducts();
}
private function loadProducts(): void
{
$query = Product::query();
if ($this->search) {
$query->where('name', 'like', "%{$this->search}%");
}
$this->products = $query->get()->toArray();
$this->total = count($this->products);
}
}class AdminPanel extends Component
{
#[Invokable]
public function deleteUser(int $id): void
{
// ✅ Always check authorization
if (!auth()->user()->isAdmin()) {
abort(403);
}
User::find($id)->delete();
}
}class UserForm extends Component
{
public string $email = '';
#[Invokable]
public function updateEmail(): void
{
// ✅ Always validate
$validated = $this->validate([
'email' => 'required|email|unique:users',
]);
$this->email = $validated['email'];
}
}-
Check your
.envfile:DIFFYNE_VERIFY_STATE=property-updates -
Clear config cache:
php artisan config:clear
-
Verify state signature is being sent correctly
If you were experiencing issues with strict verification:
// Old (causing issues)
'verify_state' => true, // or 'verify_state' => env('DIFFYNE_VERIFY_STATE', true)
// New (recommended)
'verify_state' => 'property-updates', // or env('DIFFYNE_VERIFY_STATE', 'property-updates')No code changes needed - your forms will now work smoothly while property updates remain secure.
- All server-controlled data uses
#[Locked] - Only public actions are marked
#[Invokable] - State verification is configured appropriately (
DIFFYNE_VERIFY_STATE=property-updatesrecommended) - Rate limiting is configured appropriately
- All user input is validated
- Authorization checks are in place for sensitive actions
- Signing key is strong (using APP_KEY)
- Security testing has been performed
Learn more about keeping your app secure:
- Attributes - Use Locked and Invokable attributes
- Component State - Understand state management
- Validation - Validate user input
- Testing - Test your security features