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

The TwoFactor facade

RoundlyConsulting\TwoFactor\Facades\TwoFactor — auto-aliased as TwoFactor — is the recommended way in. Everything that reads or changes one user’s two-factor goes through TwoFactor::for($user); the stateless TOTP primitives stay flat on the facade.

The per-user handle

use RoundlyConsulting\TwoFactor\Facades\TwoFactor;

$twoFactor = TwoFactor::for($user);

$setup  = $twoFactor->start(issuer: 'Acme');            // TwoFactorSetup — begin (or restart) enrolment
$twoFactor->confirm($code);                             // first authenticator code switches 2FA on
$result = $twoFactor->attempt($code);                   // VerificationResult — the login challenge
$status = $twoFactor->status();                         // TwoFactorStatus
$codes  = $twoFactor->recoveryCodes()->regenerate();    // list<string> — show once
$left   = $twoFactor->recoveryCodes()->remaining();     // int
$twoFactor->disable();                                  // clears every 2FA column
TwoFactor::for($user)->…ReturnsPurpose
start(?string $label = null, ?string $issuer = null)TwoFactorSetupBegin (or restart) a pending enrolment. Throws TwoFactorAlreadyEnabledException.
confirm(string $code)voidConfirm the pending enrolment with the first code. Throws TwoFactorNotPendingException or InvalidTwoFactorCodeException.
attempt(string $code)VerificationResultThe login challenge — limiter, replay guard, recovery-code fallback. Throws TwoFactorRateLimitedException.
status()TwoFactorStatusEnabled, pending, recovery codes left and confirmation time in one read.
recoveryCodes()->regenerate()list<string>Replace the recovery codes, returning the new plaintext set once.
recoveryCodes()->remaining()intHow many single-use recovery codes remain.
disable()voidClear every two-factor column.

Every write on the handle resolves its action from the container, so your own action overrides and TwoFactor::fake() both apply. The recovery codes sit behind the recoveryCodes() sub-accessor.

Reading the state

status() returns a TwoFactorStatus with the whole state in one read — handy for a settings screen:

$status = TwoFactor::for($user)->status();

$status->enabled;                // bool — a confirmed second factor
$status->pending;                // bool — enrolment started, waiting for its first code
$status->recoveryCodesRemaining; // int
$status->confirmedAt;            // ?CarbonImmutable — null unless enabled
PropertyTypeMeaning
enabledboolA confirmed second factor — secret set and confirmed.
pendingboolEnrolment started, waiting for its first code.
recoveryCodesRemainingintSingle-use recovery codes left.
confirmedAt?CarbonImmutableWhen the current enrolment was confirmed; null unless enabled.

TOTP primitives

use RoundlyConsulting\TwoFactor\Facades\TwoFactor;

$secret = TwoFactor::generateSecret();                          // base32
$code   = TwoFactor::currentCode($secret);                      // current 6-digit code
$step   = TwoFactor::verify($secret, $code);                    // int timestep | false
$uri    = TwoFactor::provisioningUri($secret, '[email protected]');  // otpauth:// URI
$codes  = TwoFactor::generateRecoveryCodes();                   // list<string>
MethodReturnsPurpose
for($user)UserTwoFactorThe per-user handle — everything that reads or changes one user’s two-factor (next table).
generateSecret(?int $length = null)stringCSPRNG-backed base32 secret; the length defaults to secret_length.
currentCode(string $secret, ?int $timestamp = null)stringThe TOTP code now, or at a given Unix timestamp.
verify(string $secret, string $code, ?int $window = null)int|falseBare RFC 6238 check across the drift window: the matched timestep, or false. No replay guard, recovery codes or limiter.
provisioningUri(string $secret, string $label, ?string $issuer = null)stringThe otpauth:// URI for a secret and label.
generateRecoveryCodes(?int $count = null)list<string>Fresh plaintext codes, not persisted; the count defaults to recovery_codes.count.
fake()TwoFactorFakeSwap the service for the recording test double — see Testing.

verify() is the bare RFC 6238 check: it matches a code against a secret across the drift window and returns the timestep, but claims nothing, skips recovery codes and never touches the limiter. Use TwoFactor::for($user)->attempt() for a login challenge.

Every secret and code parameter is marked #[SensitiveParameter], and verification checks the whole window without an early return, so it runs in constant time. A malformed secret surfaces as InvalidBase32Exception; an out-of-range explicit length or window as InvalidTwoFactorConfigException.

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.