Authentication
Arrel authenticates with Sanctum in SPA mode: session cookies protected with CSRF, with no tokens to manage. On top of that there are three optional layers: email verification, a second factor and the bell's notifications.
Configuring Sanctum
The application must add the panel's domain to sanctum.stateful (and, in development, the origin of the Vite server if you work in dev mode):
config([
'sanctum.stateful' => array_merge(
config('sanctum.stateful', []),
['your-domain.test', 'localhost:5173'],
),
]);The authentication API lives under /arrel/api/, whatever the panel prefix:
| Route | What it does |
|---|---|
POST /arrel/api/login | Signs in with email and password. |
POST /arrel/api/login/two-factor | Resolves the second factor of a pending sign-in. |
POST /arrel/api/logout | Signs out. |
GET /arrel/api/user | The authenticated user. |
The rest of the API sits behind auth:sanctum, email verification and the second factor.
Attempt limits
- Sign-in allows five failed attempts per combination of email and IP address. Past the limit, the response is a validation error with the
arrel::auth.throttlemessage. - A sign-in waiting for the second factor expires after five minutes, or after five wrong codes. The user has to type the email and password again.
- The login endpoints also have a limit of 10 requests per minute at route level.
Email verification
If the user model implements Illuminate\Contracts\Auth\MustVerifyEmail, the panel does not open until the email is verified. While it is not, the API answers 403 with email_verification_required: true and the SPA shows the verification screen, with a button to send the email (limited to three per minute).
final class User extends Authenticatable implements MustVerifyEmail
{
// ...
}The link in the email is a signed, temporary URL. It expires according to auth.verification.expire (60 minutes by default), points to arrel.email.verify and, once verified, redirects back to the panel. A user that does not implement MustVerifyEmail never goes through this step.
Second factor
Arrel ships no TOTP implementation of its own. It defines the Arrel\Auth\TwoFactorAuthenticator contract and you bind whichever one you want (a 2FA package, your own implementation over an encrypted secret...).
namespace Arrel\Auth;
interface TwoFactorAuthenticator
{
public function isEnabledFor(Authenticatable $user): bool;
public function isRequiredFor(Authenticatable $user): bool;
public function verify(Authenticatable $user, string $code): bool;
public function newSecret(): string;
public function qrCode(Authenticatable $user, string $secret): string;
public function verifySetup(string $secret, string $code): bool;
/** @return list<string> The recovery codes, to show the user once. */
public function enable(Authenticatable $user, string $secret): array;
public function disable(Authenticatable $user): void;
}| Method | When it is called |
|---|---|
isEnabledFor() | On sign-in, to decide whether a code must be asked for. |
isRequiredFor() | To force a specific user to enable it before entering the panel (for example, depending on their role). |
verify() | When validating the sign-in code. It must also accept a recovery code. |
newSecret(), qrCode() | When starting the setup. qrCode() returns the image that the SPA shows as it is. |
verifySetup() | When confirming the setup, with the secret still pending in the session. |
enable() | Once confirmed. It stores the secret and returns the recovery codes. |
disable() | When turning it off. |
To turn it on, bind your implementation in the container:
// AppServiceProvider::register()
$this->app->bind(TwoFactorAuthenticator::class, TotpAuthenticator::class);With no binding, the second factor does not exist: there is no extra step and no security page to use.
What the user sees
- On sign-in, if
isEnabledFor()is true, the response is{ "two_factor": true }and the SPA asks for the code. - The security page (
/{prefix}/{panel}/security) lets the user turn it on and off. When turning it on, it shows the QR, asks for a first code and, once confirmed, shows the recovery codes a single time. - If
isRequiredFor()is true and the user has not enabled it yet, the whole API answers 403 withtwo_factor_setup_required: trueand the SPA takes them to the security page. The security page's own endpoints stay accessible. - A user for whom it is mandatory cannot turn it off: the endpoint answers 403 with
arrel::auth.required_cannot_disable.
The endpoints are GET and DELETE /arrel/api/two-factor, POST /arrel/api/two-factor/setup and POST /arrel/api/two-factor/confirm. Confirming and turning it off are limited to 10 requests per minute.
Middleware before and after the user
config('arrel.middleware') runs before Sanctum resolves the session: it is the place to identify the tenant. config('arrel.authenticated_middleware') runs after email verification and the second factor, with the user already resolved. See Configuration.
Notifications
The panel's bell reads the authenticated user's Laravel database notifications. Nothing specific to Arrel is needed: a notification that uses the database channel and stores these keys in data is enough:
final class PostPublished extends Notification
{
public function __construct(private readonly Post $post) {}
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return [
'title' => 'Post published',
'body' => $this->post->title,
'url' => "/arrel/admin/posts/{$this->post->id}",
];
}
}| Key | Use |
|---|---|
title | Title of the entry. |
body | Optional text under the title. |
url | Optional link that opens when the entry is clicked. |
The bell shows the 20 latest notifications and the number of unread ones, and lets you mark one or all as read. Each user only sees and modifies their own.
Next step
To decide who can do what inside the panel, see Security and authorization.