--- title: "Tenant Authentication with Fortify & Passkeys" description: "Fortify-based authentication with separate central and tenant guards, two-factor codes, passkeys and per-domain toggles for each sign-in feature." --- # 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. ::: info 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](/docs/core/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: ```text 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 404 ``` Blocked 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). ::: warning 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::USER` doesn't have a dashboard permission by default, so pick a role that fits your app, or edit `config/permissions/*.php`. - Turn registration off for that domain (see above). ::: details View implementation example ```php 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: ```php class User extends Authenticatable implements MustVerifyEmail, PasskeyUser ``` Seeded 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` | ::: tip 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](/docs/core/multi-tenancy#invitations). - User invitations: see [Users, roles & permissions](/docs/core/users-roles-permissions#invitations). Both send queued emails with a signed link that's valid for 7 days, so you'll need a [queue worker](/docs/getting-started/local-development#queue-worker) running.