19

Product support

Visit this product's website for support.

Categories

Two-Factor Authentication plugin

Plugin adds Time-based One-Time Password (TOTP) two-factor authentication to the October CMS backend. Protect administrator accounts with authenticator apps, single-use recovery codes, and trusted devices.

Features

  • Backend 2FA powered by TOTP (RFC 6238) — compatible with Authy, FreeOTP, Google Authenticator, Microsoft Authenticator, LastPass Authenticator, 1Password and any standard authenticator app
  • QR code setup with a configurable color (falls back to the backend brand primary color)
  • Encrypted secret storage (Laravel Crypt) and hashed, single-use recovery codes
  • Trusted devices — let users skip 2FA on recognized browsers for a configurable number of days
  • Per-user and per-IP rate limiting of verification attempts to prevent brute-force
  • Replay protection — a TOTP code accepted anywhere (sign-in, enrolment, My Account) cannot be reused within the validation window
  • Force-enable 2FA for super users or for every backend user on next login
  • Admin override — super users can disable 2FA for any account when a user loses access
  • My Account integration — enable, disable, rotate recovery codes from the user profile
  • Recovery codes download as a .txt file
  • Multilingual: English, Polish, German, French, Spanish, Brazilian Portuguese, Italian, Russian, Dutch and Czech translations included — more available on request

Why is this a paid plugin?

Something that is free has little or no perceived value. Users do not commit to free products and only use them until something else that looks nice and free comes along. When I invest my time in the development of a new plugin I commit to supporting and maintaining it. I ask my customers to do the same. I do not make money from this plugin by advertisements, upgrades or additional services like hosting or setup.

Did you know that 30% of your purchase or donation goes to help fund the October Project?

My plugins take many hours to develop (40-120+) and even more hours to document and maintain. My paid plugins have to pay for both this time, and the time I am spending on free plugins and less successful paid plugins. This means that it will take even a successful plugin years to become profitable. Please consider buying an extended license if you want me to continue to maintain these plugins for the very small fee I ask in return or hire me for adding functionality that you feel is missing but valuable.

Like this plugin?

If you like this plugin, give this plugin a Like or Make donation with PayPal.

My other plugins

Please check my other plugins.

Support

Please use GitHub Issues Page to report any issues with plugin.

Reviews should not be used for getting support, if you need support please use the Plugin support link.

Icon made by Darius Dan from www.flaticon.com.

Scan the QR code, then enter the 6-digit code to enable 2FA.

Installation via Command Line

php artisan plugin:install Renatio.TwoFactorAuth

Usage

After installation plugin will register its settings page under Settings → System → Two-Factor Authentication and add a new Two-Factor Authentication tab to every backend user profile.

To enable 2FA for your own account:

  1. Go to Settings → Administrators and open your profile (My Account).
  2. Switch to the Two-Factor Authentication tab and click Set up two-factor authentication.
  3. Scan the QR code with your authenticator app (or paste the secret key manually).
  4. Enter the 6-digit code shown in the app to confirm.
  5. Save the recovery codes shown on screen — they will not be displayed again. Download them as a .txt file and confirm with I have saved my recovery codes.

From the next login, after entering your password you will be redirected to the verification screen and prompted for the current 6-digit code from your authenticator app. If enforcement is enabled and the user has no 2FA configured, the same flow walks them through enrolment (enable → codes → sign in) before granting access.

Supported authenticator apps

Any TOTP-compatible app works. Recommended:

Settings

Plugin settings are available at Settings → System → Two-Factor Authentication and grouped in five tabs. Every option ships with a sensible default.

General

  • Issuer name — label shown in authenticator apps next to the account. Leave empty to use the site host name.
  • QR code color — foreground color of the generated QR code. Leave empty to fall back to the current backend brand primary color.

Enforcement

  • Require 2FA for super users — forces every super user to set up 2FA on next login.
  • Require 2FA for all users — forces every backend user to set up 2FA on next login.
  • Pending login TTL (seconds) — how long a user has to complete the verification step after entering their password. Default: 600 (10 minutes).
  • Verification window — number of 30-second TOTP cycles accepted in either direction. Default: 1 (±30s), which matches the default of the underlying pragmarx/google2fa library. Increase only if users see "code expired" errors — make sure your server clock is NTP-synced first, because every extra step widens the window in which a captured code still works.

When enforcement is enabled, users without 2FA configured are walked through enrolment after entering their password and cannot reach the backend until setup is complete. Closing the browser before the QR code is confirmed does not bypass the check — the next login starts enrolment again. Once the code is confirmed the secret is stored, so a user who closes the browser on the recovery-codes screen is enrolled but has no saved codes: the next login goes straight to the verification screen, and they should generate a fresh set from My Account → Two-Factor Authentication → Manage recovery codes. The first backend session is only opened once the codes screen is acknowledged.

Enforcement applies at the next sign-in. Sessions that are already open are not interrupted.

Trusted devices

  • Allow trusted devices — let users skip 2FA on devices they mark as trusted.
  • Trusted device lifetime (days) — how long a trusted device is remembered. Default: 30.
  • Trusted device cookie name — cookie name used to remember trusted devices. Default: oc_tfa_trust.

When a user checks Trust this device on the verification screen, an encrypted, Secure, HttpOnly, SameSite=Lax cookie is issued. The Secure flag is not configurable, so trusted devices only work over HTTPS — the checkbox is hidden when the backend is served over plain HTTP, and a request that still asks for it shows a warning instead of recording a device the browser could never present.

On subsequent logins from the same browser the 2FA prompt is skipped until the cookie expires or the user disables 2FA. Each trusted device is also recorded in the database and can be reviewed or revoked individually from My Account → Two-Factor Authentication. Each user gets their own cookie (<name>_<user id>), so several accounts can trust the same shared browser at once; a cookie issued by an earlier release under the shared name keeps working and is moved to the per-user name on first use.

Recovery codes

  • Number of recovery codes — how many single-use recovery codes are generated per user. Default: 10.
  • Recovery code length — total number of characters in each code, split across two chunks. Default: 10 (rendered as two 5-character chunks separated by a dash, e.g. WP3DY-TNQBG).

Rate limiting

  • Max attempts per IP — failed verifications allowed per IP before a temporary lockout. Default: 5.
  • IP lockout window (seconds) — how long the IP counter is remembered. Default: 300.
  • Max attempts per user — failed verifications allowed per user across all IPs. Default: 20.
  • User lockout window (seconds) — how long the per-user counter is remembered. Default: 3600.

Recovery codes

Recovery codes are single-use fallback codes shown once during 2FA setup. They are stored hashed and let a user sign in if their authenticator device is unavailable.

Codes are rendered as two chunks separated by a dash (e.g. WP3DY-TNQBG) and can be regenerated from My Account → Two-Factor Authentication → Manage recovery codes after confirming with a fresh authenticator code (or the account password, in a session opened with a recovery code — see Emergency access below). Regenerating invalidates all previous codes and the freshly generated set is shown in place inside the popup, with a .txt download button — they will not be displayed again.

On the verification screen the Don't have your phone? link leads to the recovery code form. Submitting a valid recovery code signs the user in and removes that code from the remaining pool.

Admin override

Super users can force-disable 2FA for another user from that user's profile. This is useful when a user loses access to their authenticator device and has no recovery codes left. The override:

  • Clears the encrypted TOTP secret.
  • Deletes all recovery codes.
  • Forgets every trusted device for the user.

The user will be prompted to configure 2FA again on next login if enforcement is enabled.

Rate limiting

Verification attempts — TOTP codes and recovery codes at sign-in, and the code-or-password confirmation when disabling 2FA or regenerating codes from My Account — are rate-limited per IP and per user to prevent brute-force once a password is known. Two counters run independently:

  • Per user and IP: 5 failed attempts within 300 seconds by default, so a single browser is locked out quickly
  • Per user, across all IPs: 20 failed attempts within 3600 seconds by default, which caps a distributed attack on one account

Neither counter limits one IP address across different accounts.

A successful verification resets both counters. All four values are configurable in Settings → Rate limiting.

Requirements

  • October CMS 4.x
  • PHP 8.2 or higher
  • The GD PHP extension (ext-gd), used to render the enrolment QR code

A note on APP_KEY

TOTP secrets are encrypted with the Laravel APP_KEY. Changing that key — or restoring the database into an environment with a different key — makes every stored secret unreadable and 2FA sign-in fails with a decryption error.

Recovery codes are hashed rather than encrypted, so they keep working: use the Don't have your phone? Use a recovery code link to sign in. Disabling 2FA from My Account also needs a working code, so have another super user use Force disable for this user, or run php artisan tfa:disable <login> (see Emergency access), then enrol again.

Never rotate APP_KEY on a site with enrolled administrators without disabling 2FA for every account first (php artisan tfa:disable --all).

Emergency access

If the last super user loses both their authenticator and their recovery codes, 2FA can be cleared from the command line without a second super user:

php artisan tfa:disable admin          # login or e-mail address
php artisan tfa:disable --all          # every enrolled user, e.g. before rotating APP_KEY
php artisan tfa:disable admin --force  # skip the confirmation prompt (deploy scripts)

The command clears the secret, the recovery codes and the replay marker, removes the user's trusted devices, and reports what it did. Soft-deleted users are included, and --all also picks up accounts that only have leftover recovery codes or trusted devices. Without a terminal (--no-interaction, cron, deploy hooks) it refuses to run unless --force is given, so it never silently skips the prompt. The user can then sign in with their password alone and enrol again (immediately, if enrolment is enforced).

A user who still has a recovery code does not need this: in a session opened with a recovery code, Disable and Manage recovery codes accept the account password in place of an authenticator code, so the device can be replaced without an override. In every other session — including one opened by a trusted device — only a fresh authenticator code is accepted.

If even the console is out of reach, the same result by hand (back the database up first):

UPDATE backend_users
   SET tfa_secret = NULL,
       tfa_enabled_at = NULL,
       tfa_recovery_codes = NULL,
       tfa_last_otp_ts = NULL
 WHERE login = 'admin';

DELETE FROM backend_user_trusted_devices
 WHERE user_id = (SELECT id FROM backend_users WHERE login = 'admin');

Scheduler

Expired trusted-device records are removed by a daily scheduled job. It only runs if October's scheduler is running (php artisan schedule:run every minute, which Laravel Forge configures by default). Without it, expired rows stay in backend_user_trusted_devices; they are still refused at sign-in, so this is housekeeping rather than a security issue.

Permissions

Plugin registers a single permission:

  • renatio.twofactorauth.access_settings — required to manage the plugin's settings page.

Events

The plugin hooks into standard October backend events:

  • backend.page.beforeDisplay — intercepts the sign-in form to route users through 2FA when required.
  • backend.form.extendFields — adds the Two-Factor Authentication tab to backend user profiles.

It also fires its own events, so another plugin or the project can send a mail, write an audit entry or react to a lockout. Every payload starts with the Backend\Models\User concerned. The names are available as constants on Renatio\TwoFactorAuth\Classes\Events.

Event Fired when Payload
renatio.tfa.enabled a user confirms enrolment $user
renatio.tfa.disabled 2FA is switched off in My Account, by a super user, or by tfa:disable $user, $actor — the user who did it, null from the console
renatio.tfa.verified a second factor is accepted, or the step-up in My Account passes $user, $methodtotp, recovery, trusted_device or password
renatio.tfa.failed a code or step-up password is rejected $user, $methodtotp, recovery or password
renatio.tfa.lockedOut a failed attempt exhausts the rate limit $user, $ip
renatio.tfa.recoveryCodesRotated recovery codes are rotated in My Account; the initial set is covered by enabled $user
renatio.tfa.recoveryCodeUsed a recovery code is consumed $user, $remaining — codes left
renatio.tfa.deviceTrusted a browser is remembered for the first time $user, $device — the TrustedDevice model
renatio.tfa.deviceForgotten a trusted device is removed, one event per device $user, $device

password appears only for the step-up in My Account after a recovery-code sign-in; it is never a second factor at sign-in. Events fire after the plugin has finished its own work, so a listener that throws aborts the request like any other error but cannot leave 2FA half-configured or an attempt uncounted. Listen in your plugin's boot():

use Backend\Models\User;
use Renatio\TwoFactorAuth\Classes\Events;

Event::listen(Events::LOCKED_OUT, function (User $user, ?string $ip): void {
    Mail::send('acme.audit::mail.tfa_lockout', ['user' => $user, 'ip' => $ip], function ($message) use ($user) {
        $message->to($user->email);
    });
});
  • Found the plugin useful on 23 Jun, 2026

    Works great! I will use this again for other projects!

  • author

    Replied on 5 Sep, 2026

    Thanks, Mark — glad it's doing its job! Good to hear you'd use it again on other projects.

    If you ever hit a problem or have an idea for a feature, please open an issue on the GitHub Issues page.

1.1.1

Name the two-factor column on the administrators list 2FA and add a 2FA filter to show only enrolled or only unenrolled users.

Sep 06, 2026

1.1.0

Fire renatio.tfa.* events for enrolment, verification, lockouts, recovery codes and trusted devices; fix a crash when a trusted-device cookie is sent as an array; raise static analysis to PHPStan level 7.

Sep 05, 2026

1.0.8

Fix the QR code losing the brand colour when the backend colour mode is set to auto, and add static analysis to the plugin.

Sep 03, 2026

1.0.7

Colour the backend buttons by the weight of their action — disabling two-factor authentication and forgetting trusted devices now stand out.

Sep 03, 2026

1.0.6

Password as an alternative to the code in My Account, per-user trusted-device cookies, server-side settings validation, the tfa:disable command, and a refreshed backend UI.

Sep 03, 2026

1.0.5

Security release. Fix two ways to bypass 2FA with the password alone, honour the October login throttle, and require ext-gd.

Sep 03, 2026

1.0.4

Lower minimum PHP requirement to 8.2 for broader project compatibility.

Apr 28, 2026

1.0.3

Replay-protect TOTP codes via verifyKeyNewer and add a configurable validation window.

Apr 28, 2026

1.0.2

Polish enrolment popups and standalone layout.

Apr 28, 2026

1.0.1

Fix sign-in bounce behind proxies, harden cookies, and polish the UI.

Apr 28, 2026

1.0.0

First version of Renatio.TwoFactorAuth

Apr 28, 2026

Versions not listed here need no action. Back up the database before upgrading.

Upgrading To 1.0.0

Requires October CMS 4.x and PHP 8.4 (8.2 from 1.0.4). Run php artisan october:migrate. Enforcement is off by default; review Settings → System → Two-Factor Authentication and decide whether to enforce 2FA for super users or for everyone.

Upgrading To 1.0.3

Run php artisan october:migrate. TOTP codes are now single use within the validation window, and the new Verification window setting on the Enforcement tab defaults to 1 (±30 seconds).

Upgrading To 1.0.4

The minimum PHP version drops from 8.4 to 8.2.

Upgrading To 1.0.5

Security release. Upgrade every installation running 1.0.0 – 1.0.4. Two flaws let anyone who knew a backend user's password bypass 2FA entirely; both are fixed.

The plugin now requires the GD extension (ext-gd), so run composer update. Run php artisan october:migrate to record the version. Open the plugin settings once and confirm the Verification window value, which now has a stored default.

Upgrading To 1.0.6

Run php artisan october:migrate.

Settings are validated on save against the ranges the form shows. A value outside them is reported on the first save of the settings page; correct it and save again.

Trusted-device cookies are now issued per user. Existing cookies are moved to the new name on first use, so nobody has to re-trust a browser.

After signing in with a recovery code, disabling 2FA and rotating recovery codes in My Account accept the account password instead of the authenticator code, so a user who lost their device can re-enrol. php artisan tfa:disable <login> (or --all) disables 2FA from the console.

Upgrading To 1.1.0

Run php artisan october:migrate to record the version. The plugin now fires renatio.tfa.* events (see the Events section of the README); nothing changes unless you listen to them.