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)->… | Returns | Purpose |
|---|---|---|
start(?string $label = null, ?string $issuer = null) | TwoFactorSetup | Begin (or restart) a pending enrolment. Throws TwoFactorAlreadyEnabledException. |
confirm(string $code) | void | Confirm the pending enrolment with the first code. Throws TwoFactorNotPendingException or InvalidTwoFactorCodeException. |
attempt(string $code) | VerificationResult | The login challenge — limiter, replay guard, recovery-code fallback. Throws TwoFactorRateLimitedException. |
status() | TwoFactorStatus | Enabled, 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() | int | How many single-use recovery codes remain. |
disable() | void | Clear 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| Property | Type | Meaning |
|---|---|---|
enabled | bool | A confirmed second factor — secret set and confirmed. |
pending | bool | Enrolment started, waiting for its first code. |
recoveryCodesRemaining | int | Single-use recovery codes left. |
confirmedAt | ?CarbonImmutable | When 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>| Method | Returns | Purpose |
|---|---|---|
for($user) | UserTwoFactor | The per-user handle — everything that reads or changes one user’s two-factor (next table). |
generateSecret(?int $length = null) | string | CSPRNG-backed base32 secret; the length defaults to secret_length. |
currentCode(string $secret, ?int $timestamp = null) | string | The TOTP code now, or at a given Unix timestamp. |
verify(string $secret, string $code, ?int $window = null) | int|false | Bare 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) | string | The 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() | TwoFactorFake | Swap 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 cryptoBy 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.