Configuration
The published config/two-factor.php in full. The TOTP defaults match standard authenticator apps (RFC 6238):
use RoundlyConsulting\Crypto\Otp\OtpAlgorithm;
use RoundlyConsulting\TwoFactor\Enums\RecoveryCodeStorage;
use RoundlyConsulting\TwoFactor\Enums\ReplayGuardMode;
return [
// TOTP parameters — defaults match standard authenticator apps (RFC 6238).
// Bounds are enforced at runtime; out-of-range or non-integer values (e.g.
// 'five') throw InvalidTwoFactorConfigException rather than silently weakening 2FA.
'algorithm' => OtpAlgorithm::Sha1->value, // 'sha1' | 'sha256' | 'sha512'
'digits' => 6, // 6–8
'period' => 30, // 15–120 seconds per timestep
'window' => 1, // 0–2: accept ±N timesteps of drift
'secret_length' => 32, // base32 chars (16–4096); 32 = 160 bits; 1/3/6 (mod 8) round up by one
// Provisioning (otpauth:// URI). issuer falls back to config('app.name') at runtime.
'issuer' => env('TWO_FACTOR_ISSUER'), // null/blank → app.name
'recovery_codes' => [
'count' => 8,
'storage' => RecoveryCodeStorage::Hashed->value, // 'hashed' (default) | 'encrypted'
],
// Built-in brute-force limiter for attempt(), keyed per user. Set to null
// to disable it and rely on your own throttle middleware instead.
'attempts' => [
'max' => 5, // failed attempts before lockout
'decay' => 60, // seconds the lockout lasts
],
// Replay protection: reject any code whose timestep <= the last successful one.
'replay_guard' => ReplayGuardMode::Column->value, // 'column' | 'cache' | 'none' (null → none; blank → column)
'cache' => [
'store' => env('TWO_FACTOR_CACHE_STORE'), // null/blank → default store (used by cache guard)
'ttl' => 60 * 60 * 24, // seconds to retain last timestep in cache mode
],
// The table the published migration adds the columns to. Other account
// tables get them via `$table->twoFactorColumns()` in your own migration.
'table' => env('TWO_FACTOR_TABLE', 'users'),
// Column names on the host account table(s) — remap for non-standard schemas.
'columns' => [
'secret' => 'two_factor_secret',
'recovery_codes' => 'two_factor_recovery_codes',
'confirmed_at' => 'two_factor_confirmed_at',
'last_used_timestep' => 'two_factor_last_used_timestep',
],
];Every key
| Key | Default | Env | Purpose |
|---|---|---|---|
algorithm | sha1 | — | HMAC hash — sha1, sha256 or sha512. Keep sha1 for authenticator-app compatibility. |
digits | 6 | — | Code length, 6–8. |
period | 30 | — | Seconds per timestep, 15–120. |
window | 1 | — | Accepted clock drift in ± timesteps, 0–2. |
secret_length | 32 | — | Base32 secret length, 16–4096 characters; 32 characters = 160 bits. A length no base32 string can have (1, 3 or 6 mod 8, e.g. 17 or 30) is rounded up one character, so the secret always decodes. |
issuer | null | TWO_FACTOR_ISSUER | Provisioning issuer; null or blank falls back to app.name. A non-string value throws. |
recovery_codes.count | 8 | — | Recovery codes generated per enrolment (at least 1). |
recovery_codes.storage | hashed | — | hashed (one-way) or encrypted (reversible, can be displayed again). |
attempts | ['max' => 5, 'decay' => 60] | — | Built-in per-user brute-force limiter; only null disables it — false, 'off' or 0 throws, and a blank value keeps the shipped limits. |
attempts.max | 5 | — | Failed attempts before lockout (≥1). |
attempts.decay | 60 | — | Seconds the lockout lasts (≥1). |
replay_guard | column | — | Where the last-used timestep lives: column, cache, or none/null to disable. A blank value is not set and keeps column. Strings only — false or 0 throws. |
cache.store | null | TWO_FACTOR_CACHE_STORE | Cache store for the cache guard; null or blank = the default store. A non-string value throws. |
cache.ttl | 86400 | — | Seconds to retain the last timestep in cache mode (at least 1). |
table | users | TWO_FACTOR_TABLE | Table the published migration alters; users when not set (absent, null or blank). A non-string value throws. |
columns.secret | two_factor_secret | — | Encrypted secret column. A blank columns.* name is not set and takes its default; a non-string name throws. |
columns.recovery_codes | two_factor_recovery_codes | — | Recovery-codes column. |
columns.confirmed_at | two_factor_confirmed_at | — | Confirmation timestamp. |
columns.last_used_timestep | two_factor_last_used_timestep | — | Last-used timestep, read by the column replay guard. |
Bounds are enforced
digits (6–8), period (15–120), window (0–2), secret_length (16–4096) and attempts.max / attempts.decay (at least 1) are validated at runtime. An out-of-range value — or one that isn’t an integer at all, or an unknown algorithm, recovery_codes.storage or replay_guard — throws InvalidTwoFactorConfigException instead of silently weakening 2FA.
Strict reads
Every setting is read strictly. A key that is not set — absent, null or blank ('' or whitespace, what a KEY= line in .env gives) — takes the default above; a present value of the wrong shape throws InvalidTwoFactorConfigException naming the key. Integers accept an int or a canonical integer string ('30', as env values arrive), so 'five' or '1.5' throws rather than becoming 0 — a junk window never silently disables drift. attempts is switched off by null only; false, 'off' or 0 throws, and a blank value keeps the shipped limits. replay_guard is switched off by null or none only; a blank value keeps the column guard. A non-string issuer throws (a blank one is not set, so app.name applies), and so does a non-string table or column name — a blank one takes its default.
Backing enums
The string values are backed by enums, so an unknown value fails at resolution — never a silent hash downgrade:
use RoundlyConsulting\Crypto\Otp\OtpAlgorithm; // Sha1, Sha256, Sha512 — backs `algorithm`
use RoundlyConsulting\TwoFactor\Enums\RecoveryCodeStorage; // Encrypted, Hashed; cast(); ::fromConfig()
use RoundlyConsulting\TwoFactor\Enums\ReplayGuardMode; // Column, Cache, None; ::fromConfig() (null → None)
use RoundlyConsulting\TwoFactor\Enums\TwoFactorMethod; // Totp ('totp'), RecoveryCode ('recovery_code')Environment
Three keys read environment variables:
TWO_FACTOR_ISSUER="Acme"
TWO_FACTOR_CACHE_STORE=redis
TWO_FACTOR_TABLE=usersInspecting the configuration
The package adds a section to php artisan about. It reports the algorithm, code length and period, drift window, secret length, whether an issuer is set, the recovery-code count and storage mode, the replay guard, the attempt limit and whether the column map is default or remapped — never a secret, a recovery code, the issuer itself or a cache store name:
php artisan about --only=two-factorShow 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 cryptoBy donating, you agree to our donation terms.
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.