---
title: "Login challenges — Auth for Laravel | Roundly"
description: "The multi-step challenge engine — second factors, passkey steps and forced enrolment, the policy that picks them and how state stays safe."
url: https://roundly-consulting.com/open-source/docs/auth-for-laravel/challenges
language: en
---

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

[Auth for Laravel](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)

# Login challenges

When a first factor verifies but more is needed, the result is a challenge: an opaque token (only its HMAC is stored), the steps that remain and the attempts left. Steps complete in order; verification steps always come before enrolment steps:

```json
POST /users/auth/login
{ "identifier": "ada@example.com", "password": "…" }

200 { "status": "challenge", "challenge_token": "…", "expires_at": "…", "attempts_left": 5, "method": "password",
      "completed": [], "remaining": [ { "step": "second_factor", "methods": ["totp", "recovery_code", "passkey"] } ] }

POST /users/auth/challenge/two-factor
{ "challenge_token": "…", "code": "123456" }

200 { "status": "authenticated", "token_type": "Bearer", "access_token": "eyJ…", "refresh_token": "…", "session_id": "0199…" }
```

## Steps

| Step | Methods | Endpoints |
| --- | --- | --- |
| `second_factor` | totp, recovery\_code, passkey | `challenge/two-factor, challenge/passkey/options + challenge/passkey` |
| `passkey` | passkey | `challenge/passkey/options + challenge/passkey` |
| `enrol_two_factor` | totp\_enrolment | `challenge/two-factor/enrol + …/confirm` |
| `enrol_passkey` | passkey\_enrolment | `challenge/passkey/enrol/options + challenge/passkey/enrol` |

## Which steps a login needs

It depends on the guard’s two-factor mode, passkey mode and passkey second-factor rule, on what the account has enrolled, and on the risk reaction:

- Passkey primary — an enrolled TOTP is asked for only when satisfies\_mfa is off; under two\_factor.mode = required without TOTP, enrolment is forced when satisfies\_mfa is off or required\_with\_passkey is on.
- Email primaries (magic link, email code, invitation) skip the second-factor policy — no verification step — when two\_factor.after\_email\_login is false, but a passkey that passkeys.mode = required mandates is still enrolled. A password reset is never exempt, and neither is registration (it proves no mailbox).
- An enrolled TOTP means a second\_factor step (TOTP, recovery code, and a passkey when second\_factor = allowed).
- passkeys.second\_factor = required (or required\_when\_enrolled with a passkey) adds a passkey step — or passkey enrolment when the account has none.
- two\_factor.mode = required without TOTP forces enrolment — or accepts the account’s passkey as the second factor when second\_factor = allowed and passkey\_satisfies\_required is on; passkeys.mode = required without a passkey forces passkey enrolment.
- A risk reaction of require\_second\_factor prepends a second-factor step, or denies when the account has none. It applies to every login method — after\_email\_login = false exempts email logins from the second-factor policy, never from a step-up. A passkey login that counts as MFA (satisfies\_mfa) already is the step-up; otherwise only TOTP or a recovery code counts, else the login is denied.

## Configuration

```php
'challenge' => [
    'ttl' => 300,
    'enrolment_ttl' => 900,
    'max_attempts' => 5,
    'allow_enrolment' => true,
    'enrolment_requires_verified_email' => true,
    'bind' => ['user_agent' => true, 'ip' => false, 'device_header' => true],
    'max_active_per_account' => 3,   // older active challenges are superseded
],

'two_factor' => [
    'mode' => env('AUTHENTICATION_TWO_FACTOR', 'optional'),  // off|optional|required
    'after_email_login' => true,          // magic link / email OTP / invitation also need the 2nd factor
    'required_with_passkey' => false,     // passkey primary still forces TOTP enrolment when mode=required
    'passkey_satisfies_required' => true, // a passkey second factor satisfies mode=required (false ⇒ TOTP also required)
],

'passkeys' => [
    'mode' => env('AUTHENTICATION_PASSKEYS', 'optional'),    // off|optional|required
    'second_factor' => 'allowed',        // off|allowed|required_when_enrolled|required
    'satisfies_mfa' => true,             // a UV passkey login needs no further factor
],
```

## Lifecycle and safety

- Device binding — a fingerprint of user agent, IP and X-Device-Id per challenge.bind (IP off by default: mobile clients change networks mid-login), compared in constant time.
- Attempts — a TOTP, recovery or enrolment-confirmation code takes its attempt atomically before it is checked, so overlapping guesses never check more codes than max\_attempts allows; a code that verifies gives its attempt back. Passkey assertions count on failure only. At max\_attempts the challenge is invalidated and the response carries attempts\_left.
- Supersession — at most max\_active\_per\_account challenges stay active; older ones are superseded before the new one is inserted and again after, so racing logins cannot leave more active.
- Single use — steps advance with an optimistic version check against the copy the step verified, and that step must still be the next one: a request that lost a race (a passkey assertion overtaken by a restarted ceremony) gets challenge\_invalid instead of skipping the step after it; finalisation re-checks the account state and its token-version snapshot, so a reset, disable or logout-everywhere in between kills the login.
- Invalidation — any invalidation with a scope other than none also invalidates the account’s active challenges.

## Enrolment inside a challenge

Forced enrolment requires challenge.allow\_enrolment and — with enrolment\_requires\_verified\_email — a verified address; otherwise the answer is enrolment\_required (403). Starting enrolment again re-issues it without counting an attempt; a wrong confirmation counts one. Confirming TOTP enrolment fires TwoFactorEnabled and ends other sessions, but not the challenge itself.

## 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/auth-for-laravel.md)
