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

2Fa Laravel Package

scheb/2fa

scheb/2fa adds two-factor authentication to Symfony applications with TOTP authenticator apps, email codes, backup codes, and trusted-device support. It integrates cleanly with Symfony Security and provides extensible, well-tested building blocks for securing user logins.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: despite the "framework-agnostic core" wording, this package is Symfony-first in practice. The core (scheb/2fa) is just interfaces + the authentication-context state machine; the actual wiring — firewall listener, form entry point, Twig templates, session token swap — lives in scheb/2fa-bundle. If you're on Laravel, you'll get more value from Fortify's built-in 2FA or pragmarx/google2fa + bacon/bacon-qr-code; you can still steal the scheb/2fa-totp code validator, but you'll be reimplementing the flow. On Symfony, everything below applies directly.

Install the bundle plus only the providers you actually need (they're separate packages so you don't drag in email/QR dependencies you won't use):

composer require scheb/2fa-bundle scheb/2fa-totp scheb/2fa-backup-code scheb/2fa-trusted-device

Enable Scheb\TwoFactorBundle\SchebTwoFactorBundle (Flex does this for you), then look at these three files in this order — they are the whole integration surface:

  1. config/packages/scheb_2fa.yaml — which providers are enabled, templates, issuer name, cookie lifetime.
  2. config/packages/security.yaml — the two_factor block on your firewall and security_tokens.
  3. Your User entity — the Model\Totp\TwoFactorInterface / Model\Email\TwoFactorInterface / Model\BackupCode\BackupCodeInterface implementations.

Minimal configuration:

# config/packages/scheb_2fa.yaml
scheb_two_factor:
    security_tokens:
        - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
    totp:
        enabled: true
        issuer: "MyApp"
        template: security/2fa_form.html.twig
    backup_codes:
        enabled: true
# config/packages/security.yaml
security:
    firewalls:
        main:
            form_login: { login_path: app_login, check_path: app_login }
            two_factor:
                auth_form_path: /2fa          # GET: render the code form
                check_path: /2fa_check        # POST: validate the code
                default_target_path: app_home

First use case to get working end-to-end: TOTP enrolment + verification. Add getTotpAuthenticationSecret() / getTotpAuthenticationUsername() / isTotpAuthenticationEnabled() to your user, build a small controller that generates a Base32 secret, renders a QR code (otpauth://totp/{issuer}:{username}?secret=...&issuer=...), asks the user to confirm one code before persisting, then let the normal login flow take over. Note the bundle does not generate secrets for you — that's your job (spomky-labs/otphp or pragmarx/google2fa).

Implementation Patterns

Interface-per-factor on the user entity. Each provider is driven by a marker interface on your user. Keep them thin and delegate to your own services:

public function isTotpAuthenticationEnabled(): bool
{
    return $this->totpSecret !== null && $this->totpConfirmedAt !== null;
}

public function getTotpAuthenticationUsername(): string
{
    return $this->getUserIdentifier();
}

The is…Enabled() method is the enforcement switch — returning false for a given user lets them through with password only. Use it for gradual rollout, admin exemptions, or "user hasn't finished enrolment yet" states.

Enrolment as a separate flow from login. The login flow only ever validates. Enrolment (secret generation → QR → confirm → persist → generate backup codes → show once) should be a normal authenticated controller under /account/2fa. Gate it behind IS_AUTHENTICATED_FULLY, and treat the confirm step as a write operation, not as part of the login listener.

Custom providers (SMS, WebAuthn, push). Implement TwoFactorProviderInterface — prepareAuthentication(), validateAuthenticationCode(), isTwoFactorAuthEnabledForUser(), plus the form template — and tag the service scheb_two_factor.provider. That's the single extension point that covers 90% of "we need a different second factor" requests. prepareAuthentication() is where you send the SMS/email and stash the one-time code in the session or a cache.

Workflow hooks for policy. The bundle fires a require event before showing the form and a check event after validation. Grab the AuthenticationContext to inspect the token, request, and firewall name, and call skipTwoFactor() for allow-listed situations (trusted office IP range, service accounts, impersonation). This is the cleanest place to implement "admins must use 2FA, contractors must not" without touching every user object.

Trusted devices. Implement TrustedDeviceInterface (you need getTrustedVersion() returning an integer) and enable trusted_device in config. The bundle sets a signed cookie (lifetime, default 60 days) and the template renders a "remember this device" checkbox named _trusted. Bump trustedVersion when the user changes their password or disables 2FA to invalidate every remembered device at once — that's the whole revocation story, and forgetting it is a real security bug.

Backup codes are your recovery path. Generate ~10 codes at enrolment, show them exactly once, and store them on the user. Implement getBackupCodes() and replaceBackupCode($index); the bundle calls the latter when a code is consumed. Make sure the mutation actually gets flushed (via your persister/Doctrine listener) or the same code stays valid forever.

Testing. Write functional tests that walk the real login form, then assert on the redirect to auth_form_path. Don't rely on loginUser() — it bypasses the firewall's authentication flow entirely and will silently skip 2FA.

Gotchas and Tips

  • The token changes after the first factor. Once the password is accepted, the session token becomes a TwoFactorToken, not your usual PostAuthenticationToken. Anything doing isGranted('ROLE_USER') or access_control will behave differently mid-flow. Grant access to the 2FA routes using the IS_AUTHENTICATED_2FA_IN_PROGRESS attribute (TwoFactorInProgressVoter), or you'll lock users out of the very page that finishes their login.
  • security_tokens must match your actual token class. If you use a custom authenticator or an older Symfony with UsernamePasswordToken, list it there. Symptom of a mismatch: login "succeeds" but 2FA is never triggered — a silent, dangerous no-op.
  • The check_path must be reachable unauthenticated-ish. It sits inside the firewall but must be exempt from your normal access_control rules, same as the login check path.
  • Custom templates need the CSRF token and correct field names. Post _auth_code (or whatever auth_code_parameter_name is set to) and include the firewall's CSRF token if CSRF protection is on. A wrong field name shows the form again with no error, which is maddening to debug.
  • Rate-limit the check path. A 6-digit code is brute-forceable. Put Symfony's RateLimiter in front of /2fa_check keyed on user + IP, and consider a lockout after N failures.
  • Clock drift. TOTP failures on real devices are usually NTP drift, not code bugs. The TOTP config exposes a window (leeway in time steps); bump it to 1–2 before you start rewriting your validator.
  • Backup codes are compared in plaintext by default because getBackupCodes() returns the codes. If your security policy requires hashed-at-rest codes, you'll need a custom provider rather than the built-in one.
  • Config is cached. After changing scheb_2fa.yaml or the firewall block, run bin/console cache:clear — otherwise you'll spend twenty minutes debugging a change that isn't loaded.
  • Multi-firewall setups need two_factor on each relevant firewall, and the pending token remembers its firewall name. A user authenticated on admin won't have 2FA state on main.
  • Remember-me + 2FA interact. A remember-me cookie can restore a session that never completed the second factor if you're careless with the firewall ordering; decide explicitly whether remember-me should be issued before or after 2FA.
  • IS_AUTHENTICATED_FULLY is not enough. After 2FA, prefer a dedicated role/attribute check in access_control for sensitive routes so that a half-authenticated session can't reach them via any other path.
  • Version pinning matters. This is an actively maintained monorepo where the sub-packages move together; pin all scheb/2fa-* packages to the same minor version and read the UPGRADE notes between majors — the firewall config shape and the AuthenticationContext API have changed across 5.x → 8.x.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin