Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fortify Laravel Package

laravel/fortify

Laravel Fortify is a frontend-agnostic authentication backend for Laravel, powering registration, login, password reset, and two-factor authentication for Laravel starter kits and your own custom UI.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and scaffold

composer require laravel/fortify
php artisan fortify:install   # publishes config/fortify.php + registers FortifyServiceProvider
php artisan migrate           # adds two_factor_* columns to users

fortify:install writes config/fortify.php and wires App\Providers\FortifyServiceProvider. If your app uses a custom provider list, confirm the provider is registered in bootstrap/providers.php.

2. Add the trait to your user model

use Laravel\Fortify\TwoFactorAuthenticatable;

class User extends Authenticatable implements MustVerifyEmail
{
    use TwoFactorAuthenticatable; // required for 2FA, recovery codes, passkeys
}

The migration only adds two_factor_secret, two_factor_recovery_codes, two_factor_confirmed_at — the trait provides everything else (twoFactorQrCodeSvg(), recoveryCodes(), replaceRecoveryCode()).

3. Look at three files first

  • config/fortify.php — the features array is your master switchboard; everything else (username field, guard, prefix, rate limiters, lowercase_usernames, home) lives here.
  • app/Providers/FortifyServiceProvider.php — where you hook custom logic (createUsersUsing, authenticateUsing, response bindings).
  • php artisan route:list --name=login / --path=user — Fortify registers its routes for you; seeing the actual names (login, two-factor.login, password.email, user-password.update, …) is faster than reading source.

4. First real use case

Adding session auth to an API-first/SPA app with no Blade auth scaffolding. Set 'views' => false, keep Features::registration() and Features::resetPasswords(), then POST /login with Accept: application/json:

curl -X POST https://app.test/login \
  -H 'Accept: application/json' -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"secret"}'

You get a JSON payload plus a session cookie — no controllers written. A second common first task is enabling 2FA on an existing app; that's Features::twoFactorAuthentication(['confirm' => true, 'confirmPassword' => true]) plus the trait.


Implementation Patterns

Feature toggles as the control plane. Comment/uncomment entries in config('fortify.features') rather than deleting routes. Features::registration(), resetPasswords(), emailVerification(), updateProfileInformation(), updatePasswords(), canUpdatePasswords(), twoFactorAuthentication([...]), and (since 1.37) passkeys(). Disabled features deregister their routes entirely, so a 404 is your signal that a flag is off — not a bug.

Customize behavior through response contracts, not by overriding controllers. Bind in FortifyServiceProvider::register():

$this->app->singleton(LoginResponse::class, App\Auth\LoginResponse::class);
$this->app->singleton(TwoFactorLoginResponse::class, App\Auth\TwoFactorLoginResponse::class);
$this->app->singleton(PasswordResetResponse::class, App\Auth\PasswordResetResponse::class);

This is the sanctioned seam for "return a token instead of a redirect", "redirect admins to /admin", or "return 204 for the SPA". Use singleton — Fortify resolves these per request.

Replace actions for domain logic. Fortify::createUsersUsing(CreateNewUser::class), updateUsersUsing(...), updateUserPasswordsUsing(...), resetUserPasswordsUsing(...). These are plain invokable classes, so wrapping them is trivial: extend the default action, call parent::create($input), then dispatch your own event or attach a trial.

Custom authentication. When login is by username (or phone) rather than email:

Fortify::authenticateUsing(function (Request $request) {
    $user = User::where('username', $request->username)->first();
    return $user && Hash::check($request->password, $user->password) ? $user : null;
});

Also set 'username' => 'username' so validation, throttling keys, and password reset lookup all agree. Returning null produces the standard failed-login ValidationException — don't throw your own, or you'll skip the rate limiter.

2FA flow as a state machine. POST /user/two-factor-authentication (enable, writes pending secret) → GET /user/two-factor-qr-code (SVG or JSON svg/url) → POST /user/two-factor-authentication/confirm (writes two_factor_confirmed_at) → GET /user/two-factor-recovery-codes. Login then redirects to two-factor-challenge with login.id / login.remember / login.guard stashed in the session; POST there accepts code or recovery_code. Use InteractsWithTwoFactorState (1.30+) if you need to inspect or clear that pending state between requests.

Passkeys (1.37+). Features::passkeys() exposes registration/authentication endpoints that piggyback on the same session flow; the user model gets a passkeys relationship. Treat passkey login as an alternative to the password step — 2FA challenge still applies afterwards if enabled.

Events are the integration point. Fortify fires Login, Logout, Registered, PasswordReset, Verified, TwoFactorAuthenticationConfirmed, TwoFactorAuthenticationDisabled, RecoveryCodesGenerated, RecoveryCodeReplaced. Listen for audit logging, welcome emails, or "revoke other sessions on password change" — far cleaner than editing actions.

Headless/SPA mode. 'views' => false turns the create/show endpoints into bare responses; pair with Sanctum's EnsureFrontendRequestsAreStateful and keep Fortify's 'guard' => 'web' (a sanctum guard here is the classic misconfiguration that yields silent auth failures). Always send Accept: application/json so Fortify picks the JSON branch over a redirect.

Testing. Hit named routes with postJson('/login', [...]); for 2FA specs either disable confirmation in the test config or post a recovery code after asserting the two-factor-challenge redirect. Bind stub response contracts in tests to assert intent instead of rendering.


Gotchas and Tips

  • Duplicate routes. Installing Breeze or Jetstream alongside a manual Fortify install registers the same URIs twice; the last one wins, usually without an error. Call Fortify::ignoreRoutes() in a service provider register method and declare your own routes pointing at Fortify controllers when you need control.
  • 'views' => false is not "API mode". It only skips rendering; redirects and response shapes still come from the response contracts. If a SPA gets a 302 with an empty body, you forgot Accept: application/json or a contract binding.
  • Repeated 2FA enable deleted the pending secret — fixed in 1.39.0. If you allow re-opening the QR screen without confirming, pin >=1.39, otherwise a user can lock themselves out mid-setup.
  • two_factor_confirmed_at semantics. With confirm => false, the column stays null and hasEnabledTwoFactorAuthentication() keys off the secret instead. Disabling 2FA must null the timestamp — that behavior was only corrected in 1.25.3, so older pins can leave "confirmed" users with no secret.
  • Recovery codes are encrypted JSON. Anything that rotates APP_KEY or changes the encrypter matters: Fortify honours Model::encryptUsing / Fortify::encryptUsing() (1.28/1.31) so key rotation is possible, but a user whose codes were written under the old key gets DecryptException on the recovery-code screen. Test key rotation explicitly.
  • lowercase_usernames cuts both ways. It normalizes login and registration, and since 1.37.3 password-reset lookups too. On a case-sensitive collation, users created before you enabled the flag can fail to reset. Backfill before flipping it.
  • Password changes revoke API tokens (1.26). Great default, surprising in Sanctum apps — document it or override UpdateUserPasswords if tokens should survive.
  • Rate limiters are structural. config('fortify.limiters.login') feeds Limit::perMinute(...)->by(lowercasedUsername.'|'.ip). Replacing a limiter with something that doesn't return a Limit instance removes lockout silently; LockoutResponse returns 429, not 422, and your frontend probably only handles the latter.
  • FormRequest::failOnUnknownFields() could break Fortify's internal requests before 1.37.0 — relevant if you globally enable strict field checking.
  • Octane. Core bindings are scoped since 1.35, but any custom singleton you bind that captures request data will leak between requests. Prefer scoped() for per-request response objects.
  • Testing config. Feature flags come from config at request time, so config(['fortify.features' => [...]]) inside a test works; forgetting config:clear after editing fortify.php locally does not.
  • Extension points worth knowing: Fortify::redirects('login', '/dashboard') for post-auth paths, Fortify::loginView(fn () => view(...)) (closure or string) to keep views without publishing, and the RecoveryCode class override added in 1.38 if you need custom code formatting. The repo also ships Laravel Boost/AI guidelines (skills/guidelines files) — useful to feed an assistant so it stops suggesting Jetstream-era patterns.
  • Email verification requires both Features::emailVerification() and the MustVerifyEmail contract on the model. One without the other produces a /email/verify 404 or an endlessly unverified user, respectively.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections