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 Bundle Laravel Package

scheb/two-factor-bundle

Symfony bundle that adds two-factor authentication to your app with TOTP, email codes, backup codes, trusted devices, and extensible authentication providers. Integrates with Symfony security and supports custom providers and recovery workflows.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first: this is not a Laravel package and you cannot composer require it into a Laravel app. It is a Symfony bundle that plugs into the Symfony security firewall and token storage. There is no service provider, no facade, no Auth::user() integration, no Artisan commands. If you are a Laravel developer, treat this package as something you will encounter in legacy Symfony 2/3/4/5.0–5.2 codebases you are asked to maintain, not something you add to a Laravel project.

If you are on the Laravel side of the fence, the equivalents to reach for are:

composer require laravel/fortify          # built-in 2FA + recovery codes
# or, lower level:
composer require pragmarx/google2fa-laravel bacon/bacon-qr-code

If you actually have to work with this bundle, the minimal wiring is:

// app/AppKernel.php (Symfony 2/3) or config/bundles.php (Symfony 4/5)
$bundles = [
    new Scheb\TwoFactorBundle\SchebTwoFactorBundle(),
];
# app/config/config.yml  (config/packages/scheb_two_factor.yaml on Symfony 4/5)
scheb_two_factor:
    firewall: main              # must match a firewall name in security.yml
    totp:
        enabled: true
        template: SchebTwoFactorBundle:Authentication:form.html.twig
        issuer: "My App"
    backup_codes:
        enabled: true

Your User entity implements Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface (or the Google/Email variant):

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

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

public function getTotpAuthenticationSecret(): ?string
{
    return $this->totpSecret;
}

Where to look first:

  1. security.yml — the firewall named in scheb_two_factor.firewall. Nothing works if this name is wrong.
  2. The User entity — this is where the library expects its state, not a separate table.
  3. The bundle's route names: 2fa_login and 2fa_login_check (the second is the POST target you must exclude from your own controller).
  4. vendor/scheb/two-factor-bundle/Resources/views/ — the Twig templates you will be overriding.

First use case to implement: TOTP on top of an existing form-login. Generate a secret, show a QR code during enrolment, persist the secret, and force the 2FA form on the next login. Do not start with trusted devices or backup codes — layer those after the basic login interception works.

Implementation Patterns

The interception flow. Understanding this is 80% of debugging:

  1. Form login authenticates the user.
  2. A TwoFactorListener on the firewall inspects the token, calls isTotpAuthenticationEnabled() (etc.) on the user.
  3. If 2FA is required, the token is stored in session with a TwoFactorToken and the user is redirected to 2fa_login.
  4. The user POSTs a code to 2fa_login_check; the bundle validates it and re-issues a fully authenticated token.
  5. Any URL is reachable between steps 2 and 4 only through the access_control rules you wrote — a common mistake is leaving /admin open because the firewall "handles it".

Per-firewall configuration. In apps with an admin area and a public API, configure separately:

scheb_two_factor:
    firewall: admin
    totp:
        enabled: true

The API firewall must be left untouched; mixing a stateless firewall with this bundle is not supported.

Custom provider. When you need SMS or a hardware token, implement the bundle's provider interface (prepare / validate / is-complete style methods) and register it as a service, then reference it in scheb_two_factor.providers. This is the main extension point — keep the provider thin, delegate to a domain service that talks to your SMS gateway, and unit-test that service independently of Symfony.

Trusted devices. Enable trusted_device in config and add a Doctrine-backed TrustedDeviceToken to your user. The bundle sets a signed cookie and skips the 2FA form for N days. In practice this is the single feature users complain about most — decide deliberately whether to turn it on.

Twig overrides. Copy the vendor templates into app/Resources/SchebTwoFactorBundle/views/Authentication/form.html.twig (or templates/bundles/SchebTwoFactorBundle/Authentication/form.html.twig on Symfony 4/5). Never edit vendor/. If you need a different QR renderer, that is a template change, not a config change.

Integration tip — enrolment is your job, not the bundle's. The bundle handles challenge only. Enrolment (generate secret → show QR → verify first code → persist) is code you write in a controller. The clean pattern is: generate the secret, store it in a pendingTotpSecret column, render the QR, and only promote it to totpSecret after the user proves they scanned it. Otherwise you lock users out of their own accounts.

Testing. Functional tests are painful because 2FA is stateful. Use loginUser() with a user whose isTotpAuthenticationEnabled() returns false for the bulk of your suite, and have a small dedicated set of tests that drive the real form with a fixed TOTP secret and NelmioSecurityBundle-style time freezing.

Gotchas and Tips

It is abandoned. v4.18.4 (2020-10-30) is the final tag; the repository was archived shortly after. No security patches, no PHP 8.1+ compatibility guarantees, no Symfony 6 support. If you are choosing today, use scheb/2fa (the successor, which splits into scheb/2fa-bundle, scheb/2fa-totp, scheb/2fa-trusted-device, etc.). If you are stuck on this bundle, put a migration ticket on the backlog now — a package that owns your authentication flow is the worst place to be unmaintained.

v4.18.4 is a Symfony 5.2 compatibility fix, and it is the ceiling. This release fixed the Undefined class constant 'PUBLIC_ACCESS' error — Symfony 5.2 introduced PUBLIC_ACCESS (deprecating IS_AUTHENTICATED_ANONYMOUSLY) and the bundle had referenced the constant unconditionally. Practical consequences:

  • Pin to 4.18.4 exactly ("scheb/two-factor-bundle": "4.18.4") if you are on Symfony 5.2. Earlier 4.18.x tags will fatal on boot on 5.2+.
  • 4.18.4 is a floor, not a promise of forward compatibility — it does not mean the bundle tracks Symfony 5.3/5.4/6.x. Test the upgrade path deliberately.
  • If you are still on Symfony 4.x or 5.0/5.1 and see this constant error after an unrelated bump, you upgraded Symfony, not the bundle.

Version constraints bite. The bundle's Symfony/PHP requirements will block a PHP 8 upgrade. Expect to have to either pin an old PHP or migrate to scheb/2fa as part of the upgrade — often the upgrade forces the issue.

Firewall name mismatch is the #1 bug. scheb_two_factor.firewall must exactly equal a key under security.firewalls. A typo produces no error and no 2FA — it silently does nothing. Verify by logging in and checking the Symfony profiler's Security panel for the TwoFactorToken.

Never edit vendor/ templates. Override under app/Resources/SchebTwoFactorBundle/views/ (or templates/bundles/...). Translation strings come from the bundle's domain; if your app is not English, copy and translate the XLIFF files into app/Resources/translations/.

CSRF on the 2FA form. If your app enables CSRF globally, the check route needs a token. A missing CSRF token looks like "wrong code" to the user, which produces terrible bug reports.

Session and token storage. The half-authenticated token lives in the session. Any custom session handler, aggressive session.gc_maxlifetime, or load-balanced setup without shared sessions will cause users to be bounced mid-flow. If you see "2FA form appears twice" bugs, look at session storage before you look at the bundle.

Backup codes need a Doctrine flush. Codes are consumed on use; if your user entity uses a custom repository or a second-level cache, verify the invalidation path. Also store them hashed — the bundle's default is plaintext in the entity, which is a finding in any audit.

Trusted-device cookie needs a stable secret. It is signed with the framework secret; rotating secret silently invalidates every trusted device. Document it so nobody rotates it casually.

Debugging checklist when the form never appears:

  • Is the user's is*AuthenticationEnabled() actually returning true? dump() it.
  • Is the request hitting the right firewall? Check _firewall_context in the request attributes.
  • Does access_control allow the 2FA routes anonymously — IS_AUTHENTICATED_ANONYMOUSLY on Symfony <5.2, PUBLIC_ACCESS on 5.2+? Using the wrong one for your Symfony version is either a deprecation warning or a redirect loop.
  • Is the user object a fresh instance from the user provider (2FA state must be persisted, not computed from a request-scoped value)?

Extension points worth knowing: custom TwoFactorProvider (the big one), a custom TwoFactorFormRenderer if you need JSON/API responses instead of Twig, and the authentication events the bundle dispatches if you need to log or rate-limit attempts. The bundle does not rate-limit attempts by default — add your own throttle or you have shipped an online brute-force oracle.

What to port to Laravel. The mechanics here map almost one-to-one onto Fortify: a two_factor_secret column (encrypted), two_factor_recovery_codes, a two_factor_confirmed_at timestamp to model the pending/confirmed enrolment split described above, ConfirmPassword middleware before enabling/disabling, and per-route throttling on the challenge endpoint. If you are reading this bundle to learn the shape of 2FA, read Fortify's implementation instead — it is maintained, and it is what you will actually ship.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons