NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Three entry points run exactly the same code, so pick whichever fits the call site:

  • The TwoFactor facade — the shortest form and the recommended default.
  • The manager, injected through the constructor — the same API with an explicit dependency and no static calls. The facade root is the RoundlyConsulting\TwoFactor\Contracts\TwoFactorService contract, implemented by RoundlyConsulting\TwoFactor\TwoFactorManager.
  • Actions — single-purpose classes with one execute() method, for composing into your own actions, jobs and commands.

Injecting the service

Type-hint the contract, not TwoFactorManager: the manager is deliberately not aliased in the container, because the fake implements the contract. TwoFactor::fake() swaps this binding too, so injected code sees the fake:

use RoundlyConsulting\TwoFactor\Contracts\TwoFactorService;

final readonly class ChallengeController
{
    public function __construct(private TwoFactorService $twoFactor) {}

    public function __invoke(Request $request): Response
    {
        $result = $this->twoFactor->for($request->user())->attempt($request->string('code')->toString());
        // ...
    }
}

Running an action

Each write on the handle is one action in RoundlyConsulting\TwoFactor\Actions — resolve it from the container and call execute():

use RoundlyConsulting\TwoFactor\Actions\DisableTwoFactor;
use RoundlyConsulting\TwoFactor\Actions\StartEnrolment;

$setup = app(StartEnrolment::class)->execute($user, issuer: 'Acme');

app(DisableTwoFactor::class)->execute($user); // clears all 2FA state

Facade method → action

Handle methodActionBehaviour
for($user)->start($label, $issuer)StartEnrolment::execute($user, $label, $issuer)Fresh secret and recovery codes, confirmed_at cleared, TwoFactorEnrolmentStarted fired; returns TwoFactorSetup. Throws TwoFactorAlreadyEnabledException when 2FA is already on.
for($user)->confirm($code)ConfirmEnrolment::execute($user, $code)Stamps confirmed_at, claims the confirming timestep, fires TwoFactorConfirmed. Throws TwoFactorNotPendingException or InvalidTwoFactorCodeException.
for($user)->attempt($code)AttemptTwoFactorCode::execute($user, $code)The login challenge — limiter, TOTP with replay guard, then a recovery code; returns VerificationResult.
for($user)->recoveryCodes()->regenerate()RegenerateRecoveryCodes::execute($user)Replaces the code set, fires RecoveryCodesRegenerated and returns the new plaintext codes.
for($user)->disable()DisableTwoFactor::execute($user)Sets every two-factor column to null and fires TwoFactorDisabled.

status() and recoveryCodes()->remaining() are plain reads with no action behind them. The actions depend on TwoFactorService for the TOTP primitives, so under TwoFactor::fake() they run on the fake’s canned secret, codes and programmable verify(). The recovery-code storage helper, Support\RecoveryCodeManager, is @internal — a building block of the actions, not for host use.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.