Skip to main content

The two-factor handoff

When the Fortify bridge is active and a verified user has two-factor authentication enabled by Fortify's own verdicthasEnabledTwoFactorAuthentication(), the same check Fortify's password login makes — the magic link is handed to the challenge. With Fortify's confirm option on, that verdict is a stored secret and a two_factor_confirmed_at, so a user mid-setup is never locked out; with confirm off, which is how Fortify's shipped config registers the feature, a stored secret alone counts, exactly as it does for the password form.

  1. The token is consumed.
  2. Fortify's login.id session key is set and the request is redirected to Fortify's two-factor.login challenge — without logging the user in.
  3. The login completes inside Fortify only after the TOTP code passes.

There is no path that signs a two-factor user in without the second factor, and an end-to-end test runs the real Fortify challenge to keep it that way across Fortify upgrades.

The trade-off

The token is already spent when the handoff happens, so if a user abandons the TOTP step they must request a fresh link. This is intentional — the link is single-use and the challenge is a separate, deliberate step.

Guard alignment

When the handoff is enabled, email-magic-link.guard must resolve to the same provider as fortify.guard, because Fortify re-resolves the challenged user from its own guard's provider. With mismatched providers the challenge fails closed (the user cannot complete login) rather than logging anyone in. The default web guard satisfies this out of the box.

The same rule applies when a request selects another guard — see Multiple guards.

Configuration

'fortify' => [
'mode' => env('EMAIL_MAGIC_LINK_FORTIFY', 'auto'),
'respect_two_factor' => true,
'challenge_route' => 'two-factor.login',
],
KeyEffect
mode: 'auto'Activate the bridge only when laravel/fortify is installed — the default
mode: trueThe same, and warn at boot if Fortify is missing
mode: falseNever activate, even when Fortify is installed
respect_two_factorRoute users with two-factor enabled through the challenge
challenge_routeThe Fortify challenge route name

fortify.respect_two_factor = false disables this handoff. This is a security downgrade: magic-link logins will skip two-factor for users who have it enabled. It emits a warning at boot.

What fires

The bridge dispatches TwoFactorChallengeRequired when it hands a user to the challenge. MagicLinkAuthenticated fires only on a completed login, never for a handoff — which makes it the precise signal for an audit log.