---
title: "The TwoFactor facade — Two-Factor for Laravel | Roundly"
description: "One entry point: TwoFactor::for($user) for enrolment, login challenges, status, recovery codes and disabling, plus the flat TOTP primitives."
url: https://roundly-consulting.com/open-source/docs/two-factor-for-laravel/facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Two-Factor for Laravel](https://roundly-consulting.com/open-source/docs/two-factor-for-laravel.md)

# 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

```php
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:

```php
$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

```php
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, 'user@acme.io');  // 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; the code is taken exactly as given. |
| `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\] — redacted in the TwoFactor facade’s own stack frame too — and verification checks the whole window without an early return, so it runs in constant time. A malformed secret surfaces as InvalidBase32Exception, and one that decodes but is no usable key (empty, under 10 bytes or all zero bytes) as InvalidTwoFactorSecretException from currentCode(), verify() and attempt() — provisioningUri() refuses an empty one the same way. An out-of-range explicit length, or an explicit window outside 0–2, throws 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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/two-factor-for-laravel.md)
