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

Laraguard Laravel Package

darkghosthunter/laraguard

Laraguard brings TOTP-based two-factor authentication to Laravel. It offers a dedicated auth guard, middleware, recovery codes, QR-code onboarding, and events, making it easy to secure logins with Google Authenticator-compatible codes.

Deep Wiki
Context7

Getting Started

Install and wire it up (5 steps)

composer require darkghosthunter/laraguard
php artisan vendor:publish --provider="DarkGhostHunter\Laraguard\LaraguardServiceProvider"
php artisan migrate
  1. Add the package trait to every authenticatable model that should support 2FA (App\Models\User), usually alongside Notifiable/Authenticatable:
use DarkGhostHunter\Laraguard\Traits\HasTwoFactorAuth;

class User extends Authenticatable
{
    use HasTwoFactorAuth;
}
  1. Make sure the model's $hidden (or $visible) hides two_factor_secret and two_factor_recovery_codes, or you will leak TOTP seeds through any toArray()/JSON response.
  2. Confirm the service provider registered the 2fa and 2fa.confirm middleware aliases. On Laravel ≤10 this happens in the HTTP Kernel; on Laravel 11+ you must re-alias them in bootstrap/app.php yourself ($middleware->alias([...])).
  3. Decide whether you use the package's auto-registered routes (under the 2fa prefix) or roll your own — the config lets you change the prefix or turn the routes off entirely.
  4. Set issuer in the published config to your app name so codes show up as "Acme" rather than a random string in Google Authenticator / 1Password.

Where to look first

  • The published config — it is the whole surface area: issuer, digits/period/window, recovery code count and length, safe-device settings, rate limiting, cache store, route prefix, middleware group.
  • vendor/darkghosthunter/laraguard/routes/ — the entire HTTP contract (enable, confirm, secret/QR, recovery codes, challenge) is a handful of routes; read them before designing any UI.
  • The HasTwoFactorAuth trait — the public methods there are what you call from your own controllers when you don't want the built-in endpoints.
  • The published migration — tells you exactly which columns exist and what the package expects to be set for "enabled".

First use case: enrol one user end-to-end with curl/Postman

Log a user in, GET /2fa/secret (returns the secret plus a QR payload/SVG), scan it, then POST /2fa/confirm with the 6-digit code. Then POST /2fa/recovery-codes and store the returned plaintext codes somewhere you can inspect. Do this before touching Blade/Inertia — it proves the trait, migration, middleware and session guard are all aligned, and it gives you a known-good secret to test the login challenge against.

Implementation Patterns

Two-phase enrolment (never skip the confirm step)

The pattern the package is built around: request a secret → show QR → user submits a code → only then is 2FA considered active (two_factor_confirmed_at gets set). If you enable the flag on secret generation, users who fail to scan will be locked out on next login. Keep the secret request and the confirm request as separate screens so a user can retry the QR.

Login challenge via the session guard

After credentials validate, users with 2FA are bounced into a challenge step instead of being fully authenticated; a correct code completes the login and fires TwoFactorAuthenticated. Practical implications:

  • Your post-login redirect must tolerate the intermediate state — don't hardcode redirect()->intended() in a custom login controller unless you've checked how the challenge hook interacts with it.
  • The challenge is session-bound. Any custom guard (admin panel, multi-tenant) needs the same middleware group and session driver as web, or the pending state is lost.

Enforcing 2FA on a route group

Route::middleware(['auth', '2fa'])->group(function () {
    // users without 2FA get redirected to enrolment
});

Use 2fa.confirm on genuinely sensitive actions (changing email, disabling 2FA, exporting data) when you want a fresh code even though the user is already logged in — it composes well with Laravel's password.confirm.

Recovery codes as a first-class flow

Codes are single-use and stored hashed, so you can only display them once. The pattern that works: generate on enrolment, show a copy/download screen, then require regeneration (which invalidates the old set) — and fire your own notification/audit entry at the same time so a silent regeneration is visible to the user.

Audit via events

Listen for TwoFactorEnabled, TwoFactorDisabled, TwoFactorAuthenticated, TwoFactorFailed, and the recovery-codes-changed event. Minimum viable: email the user on disable and on recovery-code regeneration, and log failures with the user ID + IP for a SIEM or your own failed_logins table. Everything else is cosmetic.

Custom UI instead of the shipped endpoints

Disable the package routes, build your own controllers, and call the trait's helpers directly (generate a secret, confirm it against a code, validate a code, disable, generate recovery codes). This is the right call for Inertia/Livewire/SPA front ends — the built-in JSON responses are fine for prototyping but rarely match a product's UX. Keep your controllers thin and let the trait do the crypto.

Testing

Freeze time with Carbon::setTestNow() before computing a code, otherwise TOTP tests are flaky by design. Use Event::fake() to assert the events fire, and test the negative paths explicitly: expired code, reused recovery code, rate-limit lockout, and the "2FA enabled but user has no safe device" login.

Gotchas and Tips

It is archived — treat this as a liability decision, not a dependency choice. Last release February 2022, no security patches, and it predates Laravel 10/11 conventions (middleware aliasing, the bootstrap/app.php structure). If you're on a current Laravel, prefer Laravel Fortify's built-in 2FA or a maintained TOTP package, and only pull Laraguard in if you're pinned to an older app. If you do adopt it, pin the version and budget for vendoring/forking.

Clock drift is the #1 support ticket. TOTP is UTC-based so timezones don't matter, but server clock skew and phone clock skew do. The window setting lets you accept codes from adjacent periods — one step either side is the usual compromise; zero makes real users fail, and large windows weaken the scheme. Run NTP on your app servers.

Rate limiting behind a load balancer. The failure throttle keys off the client IP; if you're behind a proxy without TrustProxies configured, every request looks like it came from the proxy and one user can lock out everyone (or nobody gets limited at all). Verify with a deliberate bad-code loop from two different clients.

Safe devices are a security trade-off you must own. "Remember this device" cookies bypass the code for a long period. Check the cookie name, expiry, Secure, HttpOnly and SameSite settings, and invalidate remembered devices on password change and on 2FA disable. If your threat model includes stolen laptops, shorten the window or turn the feature off.

Cache store matters in multi-node setups. Throttling counters and device tokens can land in the cache. file in a load-balanced environment gives inconsistent behaviour — use Redis and a shared prefix.

The secret column is not encrypted by default. Add an encrypted cast (or an accessor) on two_factor_secret; a database dump otherwise hands an attacker working TOTP seeds. Same for anything you store about recovery codes — never log them, never return them outside the one-time display response.

Route collisions and route:cache. Auto-registered routes plus your own 2fa-prefixed routes will silently shadow each other. Pick one: either the package's routes or routes.enabled = false with your own. Run php artisan route:list | grep 2fa after any change.

QR generation dependencies. SVG output avoids ext-gd; PNG output needs it. If the secret endpoint returns an error on a fresh container, that's usually the missing extension, not the code path.

Debugging checklist. route:list for the actual endpoints → check the published migration ran (columns present) → php artisan tinker and generate a code from the stored secret to see whether the server or the client is wrong → inspect the session for the pending-challenge key → check the cache store for throttle keys → Event::fake() off in the failing test to confirm whether the listener bailed early.

Extension points that actually work. Views are publishable, the middleware group and route prefix are configurable, and the event/listener surface is the intended hook for notifications and auditing. Overriding behaviour deeper than that (the verification algorithm, the challenge controller) means subclassing or forking — for an archived package, fork deliberately and record why.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib