Going further
Security
Every #[Server] call carries the component's state, signed with the application key, the session's own secret and the logged-in user. The endpoint refuses anything the page could not have sent.
On this page
What the endpoint refuses
Nitro checks every request to its endpoint. It refuses these requests, with these status codes:
| Refused | Status |
|---|---|
A changed state, or a state made in another session or for another user. This includes another visitor's state, and one from before a logout, even an
Auth::logout()
that keeps the session
|
419 |
A state older than
nitro.state.lifetime
, when you have set one
|
419 |
A method that isn't
#[Server]
, a locked or unknown property, or a value or argument of the wrong type
|
403 |
An upload to anything other than an
Upload
or
#[Uploads]
property
|
403 |
A visitor who no longer passes the page's authorization middleware (
auth
,
can
,
verified
, ..., and your own middleware that extends them), or a page whose route no longer exists
|
403 |
| A partial reload that was rendered for a different page than the component's | 403 |
An upload reference is only valid in the session that uploaded the file. Redirects only go to http(s) URLs
and paths, so redirect('javascript:...') throws an exception, and the browser refuses such a
redirect too. The endpoint runs in the web middleware group, with sessions and CSRF protection.
What the browser can see
Every public property is part of the page's state, including #[Locked] ones. A model property
sends its toArray(): attributes listed in $hidden are left out, but loaded relations
are sent. Keep secrets in protected or private properties, or load them in the #[Server] method
that needs them.
class NewsletterPanel extends Component
{
public int $issueId; // Sent to the browser.
#[Locked] public string $status; // Sent, but can't be changed.
#[Server]
public function send(): void
{
// Load the subscribers where you need them, not in the state.
$issue = Issue::findOrFail($this->issueId);
Mail::to($issue->subscribers)->send(new IssueMail($issue));
}
}
An unlocked property accepts any value of its type from the browser. Its #[Validate] rules are
checked before a #[Server] method runs (see Properties).
Locked doesn't mean current
Signing stops the browser from changing a #[Locked] value. It doesn't stop the same visitor
from sending an older state from their own session, as long as that state is still valid. So
#[Locked] means a value can't be changed, not that it is up to date. When something must be
current, such as a balance, a step that was paid for or a permission, read it from the database in the
#[Server] method.
use Nitro\Attributes\Locked;
use Nitro\Attributes\Server;
class Checkout extends Component
{
#[Locked]
public int $orderId;
#[Server]
public function pay(): void
{
// The browser can't change $orderId, so this is the order the page was given.
// Whether it is still unpaid comes from the database, not from the state.
$order = auth()->user()->orders()->unpaid()->findOrFail($this->orderId);
$order->pay();
}
}
Trusting a child's props
A child component is created with the props its parent's view passes it, taken from the parent's state at
that moment. During a #[Server] call, that state includes the browser's changes. This means a
child's mount() can receive a value the browser chose, and a #[Locked] property set
from it is only as trustworthy as its source. Pass children values the parent keeps #[Locked],
or check the value in the child's mount().
Configuring sessions and lifetimes
// config/nitro.php
'state' => [
'bind_to_session' => true, // Turn off only for pages cached as whole HTML for every visitor.
'lifetime' => null, // In seconds. Set it when an old state must never come back.
],
When a call is refused with a 419, the page reloads with fresh state. To do something else instead, listen
for the nitro:expired event and call preventDefault():
addEventListener('nitro:expired', (event) => {
event.preventDefault();
showBanner('This page was open too long. Reload to carry on.');
});
Tested by forging requests
Every refusal above is tested with hand-made requests, in every form a forger might try, and by fuzzing the endpoint with malformed requests. Each one is refused, nothing runs, and nothing answers with a 500 error.