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

Nova Two Factor Laravel Package

visanduma/nova-two-factor

visanduma/nova-two-factor adds two-factor authentication to Laravel Nova. It lets users enable TOTP-based 2FA with QR code setup and recovery codes, helping protect Nova access with an extra verification step at login.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire-up (the 20-minute version)

composer require visanduma/nova-two-factor
php artisan vendor:publish --provider="Visanduma\NovaTwoFactor\NovaTwoFactorServiceProvider"
php artisan migrate

Three edits get you a working enrollment screen:

// 1. app/Models/User.php
use Visanduma\NovaTwoFactor\Traits\NovaTwoFactor; // confirm exact trait name in src/Traits

class User extends Authenticatable
{
    use NovaTwoFactor;
}
// 2. app/Providers/NovaServiceProvider.php
public function tools(): array
{
    return [
        \Visanduma\NovaTwoFactor\NovaTwoFactor::make(), // check src for ::canSee()/::canRun() options
    ];
}
// 3. .env — leave enforcement off until the reset runbook exists
NOVA_TWO_FACTOR_ENFORCE=false

Where to look before you write a line of your own code

Read four files in vendor/visanduma/nova-two-factor in this order — they answer every question you'll actually have:

  1. The migration. Column types tell you whether the TOTP secret is an encrypted cast or plaintext, and whether recovery codes are hashed, JSON, or a separate table. This determines your APP_KEY rotation story immediately.
  2. The routes file. Confirms which Nova route group the challenge middleware is attached to — that is the entire enforcement surface.
  3. The middleware. The catch blocks are where the control silently disappears. Read them before you trust the package.
  4. The config file. The full set of knobs you can rely on; anything not there is not a supported extension point.

First use case — the five-minute safety loop

Enroll yourself on staging, log out, log back in with an authenticator code, then delete your row from the 2FA table and confirm you get re-challenged. That loop tells you more about whether this is safe to enforce than the README does. Only then enroll a second admin, and only then flip NOVA_TWO_FACTOR_ENFORCE=true on staging — never in production on the same day.


Implementation Patterns

Put the package behind a seam (do this on day one, not day ninety)

The highest-leverage thing you can do as the developer owning this dependency is to call it from exactly one place. Thin, zero-dependent auth packages break on Nova upgrades; a seam turns a panic into a one-file change.

namespace App\Auth\TwoFactor;

interface TwoFactorProvider
{
    public function isEnrolled($user): bool;
    public function verify($user, string $code): bool;
    public function disable($user): void;
    public function regenerateRecoveryCodes($user): array;
}

final class NovaTwoFactorProvider implements TwoFactorProvider
{
    // every reference to Visanduma\NovaTwoFactor lives here, nowhere else
}

Bind the interface in a service provider. Every Nova card, middleware, controller, artisan command, and test resolves the interface. When you replace this with a Fortify-aligned implementation or fork it, you edit one class.

Enrollment is opt-in — build the nudges

Until enforcement is on, the tool sits in the sidebar and most admins will ignore it. Day-to-day workflow tooling you own:

  • A Nova card or dashboard metric showing who on staff is still unenrolled — this is your rollout dashboard.
  • A scheduled mail to privileged users during the grace window.
  • Your own audit table for enrolled, disabled, recovery_codes_regenerated events with actor/subject/timestamp. The package's per-user toggle is a UI convenience, not an audit log, and auditors will ask.

Build the admin reset / break-glass before enforcement

The package gives per-user enable/disable; it does not give you a helpdesk runbook. Write it yourself:

// php artisan two-factor:reset {email} {--force} {--reason=}
// - super-admin only
// - requires --force plus a non-empty reason
// - logs operator, subject, timestamp, incident ref
// - clears secret + recovery codes, forcing re-enrollment on next login

Ship it with a second super-admin who has 2FA enabled and can invoke it. A reset command that only the locked-out person can run is not a reset command.

Enforcement as an env flag, exercised as a rollback drill

# staging — soft launch
NOVA_TWO_FACTOR_ENFORCE=true
# production — hold until helpdesk is trained
NOVA_TWO_FACTOR_ENFORCE=false

Run the flag off → on → (simulated incident) → off cycle once in staging so rollback is a rehearsed action. php artisan config:clear is part of the drill — see Gotchas.

Testing — the negative assertion is the one that matters

public function test_enrolled_user_cannot_reach_nova_without_a_code(): void
{
    config(['nova-two-factor.enforce' => true]); // confirm real config key
    $user = User::factory()->create();
    $this->enrollTwoFactor($user); // helper writing a known TOTP secret

    $this->post('/nova/login', ['email' => $user->email, 'password' => 'password']);

    $this->get('/nova')->assertRedirect(); // NOT the dashboard
    $this->assertGuest('web');              // or assert on the real session key
}

Write this in your repo, not the package's. Then add a synthetic check (uptime or scheduled job) that logs in as a known enrolled account against the running app — because the worst failure is MFA that stopped enforcing weeks ago and nobody noticed. Your test suite drifts; a synthetic check against the deployed app does not.

Rollout sequencing

  1. Platform engineers + one ops lead enroll; drill device loss on a real phone.
  2. Write and test the reset runbook end-to-end.
  3. Grace window: enforcement off, comms on, enrollment dashboard published.
  4. Enforce. Watch challenge-endpoint error rate for 48 hours.
  5. Publish runbook + break-glass contact.

Gotchas and Tips

APP_KEY rotation becomes an admin lockout event. If the secret column uses an encrypted cast, php artisan key:generate invalidates every enrolled user at once — every admin locked out mid-incident. Confirm the storage format from the migration before enrollment scales, and pre-plan either a re-encrypt step or a reset-all-and-re-enroll script. Never rotate the key casually on an enforced installation.

Fail-open is the real failure mode. A 500 is loud; a skipped challenge is silent. Deliberately test: missing 2FA row, null secret, malformed recovery code, config() returning null after cache warm, exception inside the middleware. In every case the request must not reach the dashboard. Read the middleware's catch blocks — that's where a security control quietly evaporates.

Throttle the challenge endpoint yourself. Six digits is brute-forceable. If the package doesn't rate-limit, add RateLimiter::for('nova-2fa') — 5 attempts, 1-minute decay, keyed on user + IP. Verify a correct password + wrong code does not reset the counter; it shouldn't.

Config cache will bite you during rollout. If php artisan config:cache runs in your pipeline, flipping NOVA_TWO_FACTOR_ENFORCE in .env does nothing until the next deploy. If that flag is your rollback mechanism, either rebuild the cache as an explicit rollback step or drive enforcement from a DB-backed setting read at runtime. Decide deliberately, not by accident.

Enforcement may not cover every Nova entry point. Grep your own routes for Nova's middleware group: custom tools, Nova::routes(), package endpoints, actions, impersonation (nova-impersonate). Any route authenticating a Nova user without the challenge is a bypass. Impersonation especially — decide explicitly whether an impersonated session clears 2FA and encode that decision.

Don't double up with Fortify/Jetstream. If users already carries two_factor_secret from Fortify, you now have two enrollment UIs and two truth sources for "is this user MFA'd." Pick one. This package is a Nova-only admin control; it is not an end-user authentication feature and shouldn't be sold internally as one.

Multi-guard / multi-tenant needs a spike, not an assumption. The trait lives on a model; the middleware resolves the Nova guard. If your Nova guard isn't web, or users are tenant-scoped, prove enrollment and challenge work under each guard before believing it.

"Remember this device," if present, is a factor you now own. Check cookie lifetime, signing, and revocation. A 90-day trust cookie on a shared helpdesk workstation defeats the control more quietly than a missing feature would.

Pin exactly, fork before enforcing. Zero stars, zero dependents, no visible maintenance. Vendor or fork the source into your own registry before turning enforcement on — an upstream deletion or force-push in an auth path is a very bad week. Pin package version + Nova version as an explicit tested pair in your dependency register.

Budget the upgrade tax. Roughly a day per Nova/Laravel cycle to carry the fork, plus the risk of a rushed patch on security-sensitive code. Name an owner on the platform team — an unowned auth dependency is an unmaintained control.

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.
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings