Files
core/docs/otp-auth.md
Konstantinos Arvanitakis 9f58a36c82 Init
2026-07-01 18:35:39 +03:00

1.7 KiB

OTP Authentication

Staff authentication uses a passwordless OTP flow. No password is stored or required.


Flow

  1. Staff enters their email on the login page
  2. A 6-digit code is generated, stored on the staff record, and emailed
  3. Staff enters the code — if valid and not expired, they are logged in

Implementation

OtpService

Modules\Core\Services\OtpService handles generation and validation.

// Generate and email a code
$otpService->generateAndSend(string $email): bool

// Validate a submitted code — returns the Staff model on success, null on failure
$otpService->validate(string $email, string $code): ?Staff

Codes expire after 10 minutes. After a successful validation the code is cleared from the record.

Database

Two columns on the lunar_staff table (added by 2026_05_06_000001_add_otp_to_lunar_staff_table):

Column Type Purpose
otp_code string, nullable The generated code
otp_expires_at timestamp, nullable Expiry time

Login Page

Modules\Core\Filament\Pages\Login is a Filament SimplePage registered as the panel login via AppServiceProvider:

LunarPanel::panel(fn (Panel $panel) => $panel->login(Login::class));

It has two steps rendered in core::filament.pages.login:

  • Step 1 — email form, submits to requestOtp()
  • Step 2 — code form, submits to authenticate()

After a valid code canAccessPanel() is checked before the session is established.

Email

Modules\Core\Mail\OtpMail sends the code using the core::mail.otp view.


Disabling 2FA

Lunar's built-in 2FA plugin is disabled in AppServiceProvider since the OTP login replaces it:

LunarPanel::disableTwoFactorAuth();