---
title: "Enrolment — Two-Factor for Laravel | Roundly"
description: "Start a pending enrolment, show the secret, QR URI and recovery codes once, then confirm it with the first authenticator code."
url: https://roundly-consulting.com/open-source/docs/two-factor-for-laravel/enrolment
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)

# Enrolment

Enrolment is two steps: start a pending enrolment, then confirm it with the first code from the user’s authenticator app. Nothing counts as a second factor until it is confirmed.

## Start

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

$setup = TwoFactor::for($user)->start();

$setup->secret;          // base32 secret (store is handled for you)
$setup->provisioningUri; // otpauth://totp/Acme:user@acme.io?secret=...&issuer=Acme&...
$setup->recoveryCodes;   // list<string> — show these once, they are the only plaintext copy
$setup->issuer;          // the issuer the URI carries — show it next to the QR
```

| Property | Type | Contents |
| --- | --- | --- |
| `secret` | string | The base32 secret — already persisted, encrypted. |
| `provisioningUri` | string | The otpauth:// URI to render as a QR code. |
| `recoveryCodes` | list<string> | Plaintext recovery codes — the only copy, show them once. |
| `issuer` | string | The issuer the URI carries — show it next to the QR. |

start() generates a fresh secret and recovery codes, persists them with confirmed\_at cleared and fires TwoFactorEnrolmentStarted. Starting again while pending restarts it with a new secret and new codes; starting when 2FA is already enabled throws TwoFactorAlreadyEnabledException.

## Issuer per guard or tenant

The issuer defaults to two-factor.issuer, then app.name — a blank value at either level counts as unset. Brand it per call with the issuer argument — one name per guard or tenant — and override the account label the same way; the URI and $setup->issuer always agree:

```php
$setup = TwoFactor::for($client)->start(label: 'billing@acme.io', issuer: 'Acme Partner Portal');
```

## Confirm

The user scans the QR code and submits the first code to finish enrolment:

```php
TwoFactor::for($user)->confirm($request->string('code')->toString());
// throws InvalidTwoFactorCodeException on a wrong code,
// TwoFactorNotPendingException if there is no pending enrolment — including when 2FA is
// already enabled: a confirm is never a silent no-op, so a clean return always means this
// code just switched 2FA on.
```

A successful confirm stamps confirmed\_at, fires TwoFactorConfirmed and claims the confirming timestep in the replay guard, so the code used to confirm cannot double as the first login code.

The code is checked against the account row locked for the write, never the model you pass in — so if enrolment was restarted meanwhile (say, in a second tab), only a code for the secret now on file switches 2FA on, never one for a QR code the user no longer has. Only confirmed\_at is written to that row; your model’s two-factor columns are then synced from it, and any other unsaved attribute on it is left alone. A code typed with a space or a dash (123 456, 123-456) is accepted when exactly the configured number of digits remains.

## From the user model

The same flow is available as trait verbs, with an optional label and issuer. They delegate to TwoFactor::for($user), so TwoFactor::fake() sees them too:

```php
$setup = $user->startTwoFactorEnrolment();                  // == TwoFactor::for($user)->start()
$setup = $user->startTwoFactorEnrolment('billing@acme.io', issuer: 'Acme Billing');

$user->confirmTwoFactor($request->string('code')->toString()); // == ->confirm($code)
```

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