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

Two Factor Authentication Laravel Package

liberusoftware/two-factor-authentication

A Laravel/PHP package from Liberu Software for adding two-factor authentication to your applications. It supports TOTP-based 2FA, QR code enrollment, recovery codes, and straightforward integration with existing user models and login flows.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a small, opinionated module: it gives you the policy and primitives for TOTP (RFC 6238) plus single-use recovery codes, not a full UI. Expect to wire the views yourself. Treat the low star count / zero dependents as a signal to read the source before trusting it in production — it is roughly 10–20 files, so budgeting an hour to skim src/ is realistic.

composer require liberusoftware/two-factor-authentication
php artisan vendor:publish --provider="LiberuSoftware\TwoFactorAuthentication\TwoFactorServiceProvider"
php artisan migrate

Then:

  1. Read the config first. config/two-factor.php is where the enforcement policy lives (likely: which guards/roles are required to have 2FA, grace period before lockout, issuer name for the otpauth URI, number of recovery codes, and the code window/drift tolerance). Everything else in the package keys off this file.
  2. Read the migrations. They will tell you what state the package owns — typically a two_factor_secret (encrypted cast), two_factor_confirmed_at, and a two_factor_recovery_codes table or JSON column. If you already have Fortify/Jetstream two-factor columns on users, reconcile these before running migrate or you will get duplicate-column errors.
  3. Add the contract/trait to your User model. The package almost certainly ships a HasTwoFactorAuthentication trait or TwoFactorAuthenticatable interface; implements + use is the intended entry point.
  4. Register the enforcement middleware in bootstrap/app.php (Laravel 11+) or Http/Kernel.php under an alias, e.g. 2fa, and attach it to your authenticated route group.

First use case to get working end-to-end: enroll → confirm → enforce → recover.

  • Enroll: generate a secret, render the otpauth:// URI as a QR code (bacon/bacon-qr-code or a JS lib), store the secret as pending.
  • Confirm: user submits a 6-digit code; only then set two_factor_confirmed_at. This is the step that prevents half-enrolled users getting locked out.
  • Enforce: middleware redirects confirmed-but-unverified sessions to a challenge route.
  • Recover: consume one recovery code, invalidate it, log the event.

Get that loop green in a test before touching any UI polish.

Implementation Patterns

Two-phase enrollment (pending → confirmed). Never flip a user to "2FA required" on secret generation alone. The natural shape:

$secret = $user->createTwoFactorSecret();          // stored, not yet active
return view('2fa.setup', ['qr' => $user->twoFactorQrCodeUri($secret)]);
// ...
$user->confirmTwoFactor($request->input('code'));   // sets confirmed_at, returns codes

The confirm call is where you hand back the recovery codes once — that response is the only time they should ever be readable in plaintext.

Enforcement as middleware, not as a login listener. Attach 2fa after auth and before anything that mutates state, so an unverified session cannot POST to business endpoints. A middleware placement like:

Route::middleware(['auth', '2fa'])->group(function () { ... });
// challenge + logout routes outside the group

keeps the redirect logic in one place. If the package exposes an "enforcement policy" object, drive it from config (e.g. enforce_for_roles: ['admin']) rather than hardcoding role checks in the middleware — that's the seam the package is actually selling.

Atomic recovery-code consumption. The one-time property is worthless if implemented as read-then-write. Wrap it:

DB::transaction(function () use ($user, $code) {
    return $user->recoveryCodes()
        ->where('used_at', null)
        ->lockForUpdate()
        ->get()
        ->first(fn ($row) => Hash::check($code, $row->code));
});

Look for whether the package's consumeRecoveryCode() does this internally. If it does a plain ->delete() on a matched hash without a lock, you have a replay window under concurrent requests — patch it, or add a unique index plus a conditional UPDATE ... WHERE used_at IS NULL and check affected rows.

Compose with Fortify/Breeze rather than fighting it. If you already use Fortify's two-factor feature, disable its columns and challenge view and let this package own the secret storage; otherwise you get two competing two_factor_secret sources and the middleware will check the wrong one. Whichever you keep, delete or rename the other set of migrations.

Event-driven auditing. Enable-2FA, disable-2FA, failed challenge, and recovery-code-use should each fire an event (or at minimum a Log::info with user id and IP, never the secret). Listeners here are the cheapest way to satisfy an audit requirement and to alert on repeated recovery-code use.

Testing. Freeze time; TOTP is time-dependent and flaky tests are the #1 complaint with 2FA packages:

$this->travelTo(now()->startOfMinute());
$code = $user->currentTwoFactorCode(); // or generate via the package's TOTP helper
$this->post('/2fa/verify', ['code' => $code])->assertRedirect('/dashboard');

Test the negative cases explicitly: expired window, reused recovery code, code reuse within the same window, and middleware redirect when confirmed_at is set but the session flag is absent.

Config-driven behaviour over subclassing. Tune window/digits/period, recovery-code count, and issuer via config. Extension beyond that (custom code generator, custom notifier) should go through container bindings — check TwoFactorServiceProvider::register() for singleton/bind calls you can override in AppServiceProvider.

Gotchas and Tips

  • Drift tolerance cuts both ways. A window of ±1 step (±30s) is pragmatic; ±3 expands the replay surface. If the config exposes window, don't raise it to paper over server clock skew — fix NTP. Authenticator apps on phones with manual time are the usual culprit.
  • Show recovery codes exactly once. If your confirm endpoint is refreshable (GET-rendered), a user hitting F5 gets a 419 or, worse, a fresh set. Use a flashed session payload for the one-time display and never persist plaintext.
  • Hash recovery codes at rest. If the package stores them plaintext (or symmetrically encrypted rather than hashed), treat that as a finding: you can't Hash::check an encrypted value the same way, and a DB leak hands out second factors. Verify before shipping.
  • Middleware ordering. 2fa before verified/verified before 2fa both "work" until a user is unverified and un-enrolled — decide which redirect wins and test it. Also exclude the challenge, logout, and recovery routes from the group or you build an infinite redirect.
  • Migration collisions. Duplicate two_factor_secret / two_factor_recovery_codes columns against Fortify or a hand-rolled implementation is the most common install failure. Read the published migration before migrate, especially in an existing app.
  • config:cache in deploy. Publishing config then forgetting php artisan config:cache in the deploy script means your issuer name and enforcement flags silently revert. Add it to the pipeline and clear on rollback.
  • Missing/unconfirmed secret handling. Calling currentTwoFactorCode() or the verify action on a user with a null secret should return a clean 4xx/redirect, not a DecryptException 500. Wrap the challenge controller entry point.
  • Never log the secret, the QR URI, or a submitted code. The QR URI is bearer-equivalent. Scrutinise any Log::debug calls in the package source before enabling debug logging in staging.
  • Disabling 2FA is an attack path. Require password (and ideally a current TOTP or recovery code) re-confirmation on the disable route. A hijacked session should not be able to turn off 2FA.
  • Maintenance risk is real. Zero stars, zero dependents, and a release date in the future relative to typical package history mean you should pin an exact version ("liberusoftware/two-factor-authentication": "1.0.*" or a commit hash), commit composer.lock, and keep a fork path in mind. If the TOTP primitive is thin, consider keeping the policy/config layer but swapping the code-generation core for pragmarx/google2fa (well-maintained, widely audited) behind the package's interface.
  • Container override as escape hatch. When you need to change behaviour without forking, bind your own implementation of the package's TOTP/recovery contracts in AppServiceProvider::register() — check the provider for unqualified bind() calls first so you know which keys are safe to replace.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package