---
title: "Verifying at login — Two-Factor for Laravel | Roundly"
description: "Check a challenge code with TwoFactor::for($user)->attempt() — TOTP with replay protection first, then a single-use recovery code."
url: https://roundly-consulting.com/open-source/docs/two-factor-for-laravel/verification
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)

# Verifying at login

After the password step, verify the code the user types. TwoFactor::for($user)->attempt() accepts either a TOTP code or a recovery code and returns a VerificationResult:

```php
use RoundlyConsulting\TwoFactor\Exceptions\TwoFactorRateLimitedException;
use RoundlyConsulting\TwoFactor\Facades\TwoFactor;

try {
    $result = TwoFactor::for($user)->attempt($request->string('code')->toString());
} catch (TwoFactorRateLimitedException $e) {
    // too many failed attempts — retry after $e->secondsUntilAvailable seconds
}

if ($result->verified) {
    // accepted — TOTP (replay-safe) or a single-use recovery code
}
```

## Knowing how the challenge was passed

The result tells you whether the code passed, by which method, and how many recovery codes are left:

```php
use RoundlyConsulting\TwoFactor\Enums\TwoFactorMethod;
use RoundlyConsulting\TwoFactor\Facades\TwoFactor;

$result = TwoFactor::for($user)->attempt($request->string('code')->toString());

$result->verified;               // bool — TOTP (replay-safe) or a single-use recovery code
$result->method;                 // TwoFactorMethod::Totp | ::RecoveryCode | null on failure
$result->remainingRecoveryCodes; // int, after this attempt — "1 recovery code left"
$result->replayed;               // true when a valid code's timestep was already used

if ($result->method === TwoFactorMethod::RecoveryCode) {
    // e.g. notify the user, add 'recovery_code' to an amr claim
}
```

| Property | Type | Meaning |
| --- | --- | --- |
| `verified` | bool | The code passed — TOTP (replay-safe) or a single-use recovery code. |
| `method` | ?TwoFactorMethod | Totp or RecoveryCode; null when not verified. |
| `remainingRecoveryCodes` | int | Recovery codes left after this attempt. |
| `replayed` | bool | A valid TOTP code whose timestep was already claimed. |

The remaining count after a recovery code is read from the row locked for the spend, so it is correct even when two requests race.

## Order of checks

- A user without a confirmed enrolment fails immediately — a pending enrolment is never a valid second factor.
- The attempt is counted against the built-in limiter first, in one atomic increment. Past attempts.max, TwoFactorRateLimitedException is thrown before any verification work.
- The TOTP code is checked across the drift window; a match atomically claims its timestep. An already-claimed timestep is a replay — the result has replayed: true and TwoFactorReplayDetected fires. Spaces and dashes are stripped first when exactly the configured number of digits remains (123 456, 123-456), so a correctly read code isn’t counted as a wrong guess; anything else is checked as typed.
- Otherwise the code is tried as a single-use recovery code — normalized the way people type it — and consumed on a match.
- A success clears the limiter counter and fires TwoFactorVerified. A failure or a replay stays counted; a genuine failure also fires TwoFactorVerificationFailed.

## From the user model

Two trait shorthands run the same attempt(): one returns the result, the other only the bool:

```php
$result = $user->attemptTwoFactorCode($code);  // → VerificationResult (== TwoFactor::for($user)->attempt())
$ok     = $user->verifyTwoFactorCode($code);   // bool (== ->attempt($code)->verified)
```

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