Search by

calmfox / magento-admin-two-factor-module

calmfoxpl

Two-factor authentication for Magento administrators: a passkey or an authenticator app, with a panel-wide policy and per-administrator reset.

Package info

github.com/calmfoxpl/magento-admin-two-factor-module

Type:magento2-module

pkg:composer/calmfox/magento-admin-two-factor-module

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-09-18 20:40 UTC

This package is auto-updated.

Last update: 2026-09-18 20:40:21 UTC


README

Build License: MIT

Two-factor authentication for the Magento 2 administration panel. Each administrator chooses their second factor: a passkey (fingerprint, face or device PIN) or an authenticator app (TOTP).

Features

  • A choice at setup: the passkey is offered first, because it cannot be phished and there is nothing to retype. An authenticator app, with a QR code or a key to type by hand, comes next.

  • At sign-in: after the password, a passkey takes one gesture and an app takes six digits. An administrator with both can switch between them.

  • A policy in the panel: Stores → Configuration → Calmfox → Two-Factor Authentication:

    • required: every administrator must pair something; until they do they only reach the setup page,
    • optional: each administrator decides,
    • turned off: nobody is asked, and anything paired is kept for when the policy changes back.
  • Per administrator: a Two-Step Verification tab on their page lists the paired app and passkeys, with two separate actions:

    • Reset: removes everything paired and demands a new pairing at the next sign-in, whatever the policy says. For a lost phone.
    • Turn off: removes everything paired and leaves the rest to the policy.

    The same is available as bin/magento calmfox:admin:2fa:reset <email> [--disable].

  • A column on the grid: System → Permissions → All Users shows who is covered and who is not.

  • Careful with secrets: the authenticator secret is stored encrypted with Magento's own encryptor and only lands on the account after a code it produced has been typed; a passkey is stored only after its signature has been verified, and only as a public key.

  • Careful with passkeys:

    • user verification is required,
    • challenges are used once and bound to the session,
    • origins are checked strictly rather than by suffix,
    • only the account's own keys are accepted,
    • the signature counter is remembered, so a replayed response is noticed.

    Built on lbuchs/webauthn, which has no dependencies of its own.

  • Invitations: with calmfox/magento-admin-invitation-module installed, a newly invited administrator pairs a second factor right after choosing their password.

  • The web API too: a password on its own no longer earns an administrator token. See The web API.

  • Translations: English and Polish.

Requirements

Version
Magento 2.4.6, 2.4.7 (Open Source and Adobe Commerce)
PHP 8.2, 8.3, 8.4
Browser for passkeys any current browser, over https (plain http works on localhost only)

This module does not use, and does not conflict with, Magento_TwoFactorAuth. Running both would ask administrators for two second factors; pick one.

Installation

composer require calmfox/magento-admin-two-factor-module

bin/magento module:enable Calmfox_AdminTwoFactor
bin/magento setup:upgrade
bin/magento setup:di:compile     # production mode only
bin/magento cache:flush

The upgrade adds one table, calmfox_admin_two_factor: one row per administrator, holding the encrypted authenticator secret, the paired passkeys and whether a new pairing is due. Deleting an administrator deletes the row.

Installing from a package into app/code instead? Magento does not read a module's own composer.json there, so the two libraries have to be required by the project:

composer require bacon/bacon-qr-code lbuchs/webauthn

mkdir -p app/code/Calmfox
unzip magento-admin-two-factor-module.zip -d app/code/Calmfox
mv app/code/Calmfox/magento-admin-two-factor-module app/code/Calmfox/AdminTwoFactor

bin/magento module:enable Calmfox_AdminTwoFactor
bin/magento setup:upgrade
bin/magento cache:flush

Before you turn the policy to required on a live shop, pair a second factor on your own account first. Otherwise every administrator, including you, is sent to the setup page at their next sign-in — which works, but is a surprise. bin/magento calmfox:admin:2fa:reset is the way back if something goes wrong.

Configuration

Stores → Configuration → Calmfox → Two-Factor Authentication.

Setting Default What it does
Who Must Use a Second Factor Required The policy described above.
Offer Passkeys Yes Turn off to leave only the authenticator app.
Name Shown by the Device store name What the phone or laptop calls this site when creating a passkey.
Domain Passkeys Are Bound To host of the request A bare domain. Changing it makes every paired passkey stop working.
Name Shown in the App store name The account label in the authenticator app.
Clock Drift Allowed (steps) 1 30-second steps either side of now that a code is still accepted.

How it works

The guard is an observer on controller_action_predispatch, not on the sign-in itself, because the question is not "did you just sign in" but "may you be on this page at all". A session that never answered its challenge is stopped on every request, including one opened from a bookmark hours later. Its own pages, plus signing out and password recovery, are always allowed, so nobody who cannot answer is trapped.

What counts as answered lives in a session of its own rather than on the admin session, so that signing out, or Magento renewing the admin session, cannot leave a stale "already answered" behind. It records which administrator answered, so it cannot be inherited by whoever signs in next on the same browser.

One-time passwords are RFC 6238 over RFC 4226 with the parameters every authenticator app uses: SHA-1, a 30-second step, six digits. They are implemented in this package rather than pulled in as a dependency — it is forty lines of arithmetic, and the RFC's own test vectors are in the test suite, so an app anywhere in the world and this module agree by construction.

A code is spent once. A code is right for as long as its thirty-second step lasts, and a step either side of now is accepted for clocks that drift, so the same six digits would otherwise answer twice: read off a screen share, seen over a shoulder, or replayed from a captured request. The newest step an account has used is recorded, and anything from that step or earlier is refused however right the arithmetic is. The code typed to pair the app counts as spent too, so it cannot also answer the first challenge. The record is claimed with a single conditional statement rather than a read and then a write, because two requests arriving together with the same code would both pass the latter.

Passkeys are the one second factor phishing cannot relay: the signature covers the address of the site, so a look-alike page never gets a valid one. Six digits can be retyped into a fake form; a passkey cannot. The library accepts any host that merely ends with the configured domain, so the module checks the origin itself: the exact site or a real subdomain, over https, with plain http allowed only on localhost as browsers do.

Setting up leaves nothing half-done. The authenticator secret waits in the session and only reaches the account once a code it produced has been typed; a passkey is stored only once its signature verifies. An abandoned setup leaves no secret behind, and nobody is locked out by a secret their app never had.

Appearance

  • Pages: the challenge and the setup page reuse the admin sign-in screen, logo and all, so an admin theme applies to them. Override the templates under Calmfox_AdminTwoFactor:: as with any Magento template: challenge/form.phtml, setup/form.phtml, user/two_factor.phtml.
  • CSS: the setup choices carry calmfox-choice-passkey and calmfox-choice-app; the grid column carries calmfox-2fa-on, calmfox-2fa-pending and calmfox-2fa-off.
  • Texts: override the strings in i18n/.

Recovering an account

If an administrator loses their phone, any colleague with the Two-Factor Authentication permission resets it from their page. If nobody can sign in at all:

bin/magento calmfox:admin:2fa:reset ewa@example.com

They keep their password and pair a new device at the next sign-in. --disable removes what is paired without demanding a new pairing, and the policy then decides.

The web API

Magento has a second door to the same permissions. POST /V1/integration/admin/token trades a user name and a password for a token that carries whatever the account may do, and that request never reaches an admin controller, so the panel's guard does not see it. A second factor on the panel alone would leave that door open.

The module closes it. Under Stores > Configuration > Calmfox > Two-Factor Authentication > Web API:

  • Ask for a code as well (the default). An administrator covered by the policy gets nothing from Magento's endpoint. They post to /V1/calmfox-two-factor/admin-token instead, with a code alongside the password, and get back an ordinary Magento token:

    curl -X POST https://example.com/rest/V1/calmfox-two-factor/admin-token \
      -H 'Content-Type: application/json' \
      -d '{"username":"anna","password":"...","code":"123456"}'
  • Issue no administrator tokens. Nothing is handed out, for a shop where nothing calls it.

  • Password alone, as Magento does it. The escape hatch for a shop that has to keep an older integration alive and shuts this door somewhere else.

Three things worth knowing:

  • Integrations are unaffected. Those created under System > Extensions > Integrations carry their own tokens, tied to no person's password, and none of this touches them.
  • A passkey cannot answer here. It needs a browser and someone present to make a gesture. An administrator who guards the panel with a passkey pairs an authenticator app as well if they, or a script of theirs, need the API.
  • The refusal is not an oracle. The password is checked before anything is refused, so the message that names two-factor authentication only ever reaches someone who already had the password. A wrong password falls through to Magento untouched, and is counted and locked out as always.

What this does not cover

Trusted devices. Every sign-in is challenged. There is no "remember this browser for 30 days".

Development

The unit tests cover the framework-free core — the one-time passwords against the RFC's own test vectors, the base32 they travel in, the policy decisions and the passkey bookkeeping — plus the wiring of the XML the module is assembled from. They need neither Magento nor a database:

vendor/bin/phpunit -c phpunit.xml.dist

Any PHPUnit 10 or newer works; the package needs no composer install of its own.

Security

See SECURITY.md for how to report a vulnerability.

License

MIT