Authentication
Authentication runs on Laravel Fortify (headless) with Inertia pages. Fortify handles the routes and the logic, and the Modules/Auth module connects it to the kit's pages, actions and rate limits.
Features
| Feature | Fortify feature | Pages |
|---|---|---|
| Login / logout | always on | auth/Login |
| Registration | Features::registration() | auth/Register |
| Password reset | Features::resetPasswords() | auth/ForgotPassword, auth/ResetPassword |
| Email verification | Features::emailVerification() | auth/VerifyEmail |
| Password confirmation | always on | auth/ConfirmPassword |
| Two-factor auth (TOTP + recovery codes) | Features::twoFactorAuthentication() | auth/TwoFactorChallenge, Security settings |
| Passkeys (WebAuthn) | Features::passkeys() | Login, Confirm password, Security settings |
The page names above use Vue/Svelte casing. React uses kebab-case (auth/two-factor-challenge).
Signed-in users also get these settings pages:
| Page | URL | Contains |
|---|---|---|
| Profile | /settings/profile | Name, email, language, delete account |
| Security | /settings/security (requires password confirmation) | Change password, 2FA, passkeys |
| Appearance | /settings/appearance | Light, dark, system |
| Layout | /settings/layout | Personal layout options |
How it is wired
AuthServiceProvider in the Auth module tells Fortify which actions and Inertia pages to use and registers the rate limiters. On tenant domains it also points Fortify at the tenant guard, so users get logged into the right database.
| Rate limiter | Limit |
|---|---|
login | 5 per minute per email + IP |
two-factor | 5 per minute per login session |
passkeys | 10 per minute per credential (or session) + IP |
Related files:
| File | Role |
|---|---|
Modules/Auth/Providers/AuthServiceProvider.php | Fortify actions and views, rate limiters, StatefulGuard binding |
Modules/Auth/Actions/CreateNewUser.php, ResetUserPassword.php | Registration and password reset |
config/fortify.php | home is /dashboard; the full feature list |
app/Models/User.php | Uses TwoFactorAuthenticatable, PasskeyAuthenticatable, HasRoles |
Central and tenant guards
Central users and tenant users live in different databases, so each context gets its own session guard. Both guards use the same App\Models\User model.
| Guard | Provider and password broker | Database |
|---|---|---|
web | central_users | Central |
tenant | tenant_users | Tenant |
You don't switch guards yourself. On every request the kit sets the Laravel and Fortify guard for the current context, and inside a tenant the auth / guest middleware aliases map web to tenant. Just write ->middleware('auth') in both contexts.
Separate accounts
A central user and a tenant user are separate records in separate databases, even if they share an email address. Sessions are per host too.
Related files: config/auth.php, App\Http\Middleware\InitializeTenancyIfTenantDomain, App\Listeners\ConfigureTenantAuth, App\Listeners\RevertTenantAuth. For the bigger picture, see Architecture → Central vs tenant context.
Per-domain features
Each tenant domain can switch Fortify features off without any code changes. You might use this to stop self-registration for one customer, for example. Open Tenants → Domains → Authentication features and you'll see every feature from config/fortify.php with a toggle.
Here's what happens:
domains.auth_features (JSON) → stored per domain; missing keys count as enabled
tenancy starts → ApplyTenantFortifyFeatures filters fortify.features
request to a disabled feature → EnsureTenantAuthFeatureEnabled returns 404Blocked route names include register, password.request, verification.*, two-factor.* and passkey.*.
The central domain always uses the full config/fortify.php list. If you want a feature gone everywhere, remove it from that list.
Registration and permissions
CreateNewUser creates a user without any role or permissions. Fortify then redirects to /dashboard, which needs View Analytics Dashboard (central) or View Tenant Dashboard (tenant).
New users see a 403
A freshly registered user gets a 403 on the dashboard until an admin gives them a role.
You can handle this in two ways:
- Assign a role in
Modules\Auth\Actions\CreateNewUser::create()(example below).RoleEnum::USERdoesn't have a dashboard permission by default, so pick a role that fits your app, or editconfig/permissions/*.php. - Turn registration off for that domain (see above).
View implementation example
use Modules\RolePermission\Enums\RoleEnum;
use Modules\RolePermission\Services\PermissionService;
$user = User::create([...]);
app(PermissionService::class)->assignRole($user, RoleEnum::USER->value);
return $user;Email verification
Features::emailVerification() is enabled, but App\Models\User doesn't implement MustVerifyEmail (the import is commented out). That means the verified middleware lets unverified users through.
If you want to require verification, uncomment the MustVerifyEmail import in app/Models/User.php and add it to the class:
class User extends Authenticatable implements MustVerifyEmail, PasskeyUserSeeded users are already verified, and invited users get verified when they accept.
Passwords
Password::defaults() is set in AppServiceProvider:
| Environment | Rules |
|---|---|
| Production | At least 12 characters, mixed case, letters, numbers, symbols, not in known breaches |
| Other | Laravel's default rules |
Passkeys
Passkeys use @laravel/passkeys on the frontend and Fortify's passkey routes on the backend.
Setting (config/fortify.php) | Value |
|---|---|
| Relying party ID | Host of APP_URL |
| Allowed origin | APP_URL |
Use HTTPS locally
Browsers only allow WebAuthn on secure origins, so run herd secure before you test passkeys.
Invitations
There are two invitation flows that let admins create accounts without choosing a password:
- Tenant admin invitations: see Multi-tenancy.
- User invitations: see Users, roles & permissions.
Both send queued emails with a signed link that's valid for 7 days, so you'll need a queue worker running.