TOTP, WebAuthn and Magento Two-Factor Auth in Detail
A single stolen admin password today is enough to cause total damage in a Magento backend. This article shows how TOTP per RFC 6238, hashed backup codes, and phishing resistant WebAuthn/FIDO2 are implemented correctly at the technical level, and how Magento's native Two-Factor Auth module can be enforced for every admin user to close this gap.
Table of Contents
- 1. Why passwords alone are no longer enough
- 2. TOTP mechanics per RFC 6238: secret, time window, HMAC-SHA1
- 3. Implementing TOTP with PHP: otphp and QR code
- 4. Backup codes: generation, hashing, invalidation
- 5. WebAuthn/FIDO2: the phishing resistant alternative
- 6. Implementing WebAuthn registration and login technically
- 7. Magento_TwoFactorAuth: module structure and providers
- 8. Enforcing admin MFA: configuration and CLI
- 9. MFA methods compared directly
- 10. Summary
- 11. FAQ
1. Why passwords alone are no longer enough
Passwords alone have not been an adequate safeguard for administrative access for years. Credential stuffing attacks exploit billions of leaked credentials from previous breaches fully automatically, and phishing kits now imitate login forms pixel perfectly. For a Magento backend with access to customer data, payment information, and the entire shop configuration, a compromised admin password is a total damage scenario, not a footnote.
Multi-Factor Authentication (MFA) addresses this problem by combining at least two independent factors from different categories: knowledge (password), possession (smartphone, hardware token), and inherence (fingerprint, facial recognition). NIST SP 800-63B explicitly requires at least two of these categories for higher assurance levels, because an attacker rarely possesses both a stolen password and physical access to a second device at the same time. The following sections cover the technical implementation of the two most practically relevant methods, TOTP and WebAuthn, as well as their native integration into Magento's Two-Factor Auth module.
2. TOTP mechanics per RFC 6238: secret, time window, HMAC-SHA1
TOTP (Time-based One-Time Password) is specified in RFC 6238 and builds on the older HOTP algorithm from RFC 4226. Instead of a monotonically increasing counter as with HOTP, TOTP uses the current Unix time divided by a fixed time window, usually 30 seconds: T = floor(unix_time / 30). This counter T replaces the counter value in the HOTP algorithm.
Server and client share a random secret in advance, typically 160 bits long and Base32 encoded for manual entry. From the secret and time window, the algorithm computes an HMAC-SHA1 hash, from whose last four bits a dynamic truncation function extracts four bytes, which are then reduced modulo 10^6 to a 6-digit code. Because server and client clocks are never exactly in sync, implementations usually accept a tolerance window of one time step in either direction. The shared secret must never leave the secured backend in plaintext and must be stored encrypted, since it is the only factor that permanently enables an attacker to generate codes.
3. Implementing TOTP with PHP: otphp and QR code provisioning
For practical implementation in PHP, the library spomky-labs/otphp has become the standard, fully implementing RFC 6238 and installable via Composer: composer require spomky-labs/otphp. The class OTPHP\TOTP automatically generates a cryptographically secure secret when created, unless one is passed in, and provides the otpauth:// URI via getProvisioningUri(), which authenticator apps like Google Authenticator or Authy read via QR code.
The QR code itself is not generated server side by otphp, but by a separate library such as endroid/qr-code, which renders the provisioning URI as PNG or SVG. Important: the URI contains the plaintext secret and must only be transmitted once, during the enrollment phase, over an authenticated, TLS secured connection, never in logs or emails. During verification in the login flow, verify() is called with a tolerance window so that small time deviations between server and smartphone do not lead to false negatives.
<?php
declare(strict_types=1);
namespace Mironsoft\SecurityDemo\Service;
use OTPHP\TOTP;
/**
* Handles TOTP secret generation, provisioning URI creation and code verification.
*/
final class TotpService
{
/**
* Generate a new TOTP secret and provisioning URI for QR-code enrollment.
*
* @param string $accountName Usually the user's email address.
* @param string $issuer Application/brand name shown in the authenticator app.
* @return array{secret: string, uri: string}
*/
public function generateSecret(string $accountName, string $issuer = 'Mironsoft Admin'): array
{
$totp = TOTP::create(
secret: null, // let otphp generate a cryptographically secure Base32 secret
period: 30, // RFC 6238 default time-step in seconds
digest: 'sha1', // RFC 6238 default HMAC algorithm
digits: 6
);
$totp->setLabel($accountName);
$totp->setIssuer($issuer);
return [
'secret' => $totp->getSecret(),
'uri' => $totp->getProvisioningUri(), // otpauth://totp/... for QR-code rendering
];
}
/**
* Verify a user-submitted 6-digit code against the stored shared secret.
*
* @param string $secret Base32-encoded shared secret from the database.
* @param string $code User-submitted one-time code.
* @return bool True if the code is valid within the allowed clock-drift window.
*/
public function verify(string $secret, string $code): bool
{
$totp = TOTP::create($secret, period: 30, digest: 'sha1', digits: 6);
// Window of 1 tolerates +/-30s clock drift between server and device
return $totp->verify($code, time(), 1);
}
}
4. Backup codes: generation, hashing and single-use invalidation
Anyone who loses their smartphone or reinstalls the authenticator app needs a recovery path that does not depend on the second factor itself. Backup codes solve this chicken and egg problem: upon MFA activation, the backend generates a fixed number, usually 8 to 10, of random one-time codes with sufficient entropy, generated via random_bytes() instead of the insecure rand() family.
The codes are shown to the user exactly once in plaintext, for example to print or save in a password manager, and are stored exclusively as a hash in the database, ideally with password_hash() and the algorithm PASSWORD_ARGON2ID. After a code is successfully used, the corresponding record is immediately marked as consumed, for example via a used_at timestamp field, so the same code cannot work a second time. This single-use invalidation must happen within the same database transaction as the login authorization, to rule out race conditions on parallel requests. Once all codes are used up, the system should proactively prompt for regeneration.
<?php
declare(strict_types=1);
namespace Mironsoft\SecurityDemo\Service;
/**
* Generates and validates single-use MFA backup codes.
*/
final class BackupCodeService
{
private const int CODE_COUNT = 10;
private const int CODE_LENGTH = 10;
/**
* Generate a fresh batch of backup codes for a user.
* Returns the plaintext codes once (shown to the user) plus their hashes for storage.
*
* @return array<int, array{plain: string, hash: string}>
*/
public function generate(): array
{
$codes = [];
for ($i = 0; $i < self::CODE_COUNT; $i++) {
// random_bytes is CSPRNG-backed; bin2hex avoids ambiguous characters
$plain = bin2hex(random_bytes((int) (self::CODE_LENGTH / 2)));
$codes[] = [
'plain' => $plain,
'hash' => password_hash($plain, PASSWORD_ARGON2ID),
];
}
return $codes;
}
/**
* Verify a submitted backup code against stored hashes and invalidate it on success.
*
* @param string $submittedCode Plaintext code entered by the user.
* @param array<int, array{id: int, hash: string, used_at: ?string}> $storedCodes
* @return int|null ID of the matched, now-invalidated code, or null if no match.
*/
public function verifyAndInvalidate(string $submittedCode, array $storedCodes): ?int
{
foreach ($storedCodes as $stored) {
if ($stored['used_at'] !== null) {
continue; // already consumed, single-use enforcement
}
if (password_verify($submittedCode, $stored['hash'])) {
return $stored['id']; // caller sets used_at = NOW() in the same transaction
}
}
return null;
}
}
5. WebAuthn/FIDO2: the phishing resistant alternative to TOTP
WebAuthn, standardized by the W3C and part of the broader FIDO2 stack together with the CTAP2 protocol, replaces shared secrets with asymmetric cryptography. During registration, the authenticator generates a key pair, keeps the private key securely stored on the device, and transmits only the public key to the server. During login, the authenticator signs a random challenge generated by the server with the private key, and the server verifies the signature with the stored public key.
The decisive phishing protection lies in origin binding: the browser cryptographically binds every signature to the actual domain (relying party ID), so a credential registered for mironsoft.de simply does not work on a phishing domain such as mironsoft-de.example. TOTP codes, on the other hand, can be phished in real time on a fake login page and forwarded to the real server. A distinction is made between platform authenticators, built into the device such as Touch ID or Windows Hello, and roaming authenticators, external hardware tokens such as YubiKeys connected via USB, NFC, or Bluetooth.
6. Implementing WebAuthn registration and login technically
Registering a WebAuthn credential starts in the browser with navigator.credentials.create(), to which a PublicKeyCredentialCreationOptions object is passed. Key fields include the server generated, cryptographically random challenge, the rp.id as the relying party domain, a unique user.id, and the list of allowed signature algorithms in pubKeyCredParams, usually ES256 (algorithm ID -7) and RS256 (-257). Via authenticatorSelection.userVerification, it can be enforced that the authenticator additionally requests biometrics or a PIN, not just mere presence.
On the server side in PHP, the library web-auth/webauthn-lib handles the complete verification of the attestation returned by the browser: signature check, challenge comparison, and extraction of the public key. The public key, the credential ID, and a signature counter are stored, which is compared with the value reported by the authenticator on every login to detect cloned authenticator hardware. A login request then uses navigator.credentials.get() with the same challenge logic.
{
"rp": {
"name": "Mironsoft Admin",
"id": "mironsoft.de"
},
"user": {
"id": "MTIzNDU2Nzg5MA",
"name": "admin@mironsoft.de",
"displayName": "Admin User"
},
"challenge": "Y2hhbGxlbmdlLWZyb20tc2VydmVyLWNzcHJuZw",
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"residentKey": "preferred",
"userVerification": "required"
},
"attestation": "none",
"timeout": 60000
}
7. Magento_TwoFactorAuth: module structure and providers
Magento has natively shipped the module Magento_TwoFactorAuth since version 2.3.7; in Magento 2.4.x it is enabled by default and covers exclusively the Adminhtml area, not the storefront login. The module defines a ProviderInterface, through which several providers are registered in parallel: google (TOTP based, compatible with Google Authenticator and Authy), authy (SMS and push based via the Twilio Authy API), duo_security (push based verification via Duo), and webauthn, which has replaced the older U2F module since Magento 2.4.4 and supports full FIDO2 including platform authenticators.
The provider selection and enforcement logic resides in vendor/magento/module-two-factor-auth/etc/di.xml as well as the associated console commands. Every admin user goes through an enrollment flow on first login after activation, in which they must set up the enforced provider before they can enter the backend. For production shops, the combination of a TOTP provider as the base and WebAuthn as a phishing resistant addition is the most robust configuration.
<!-- app/code/Mironsoft/SecuritySuite/etc/config.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
<default>
<twofactorauth>
<general>
<!-- Providers enforced for every admin user, comma-separated -->
<force_providers>google,webauthn</force_providers>
</general>
<google>
<!-- Issuer label shown in the authenticator app -->
<issuer>Mironsoft Admin</issuer>
</google>
<webauthn>
<!-- Relying Party ID must match the admin domain -->
<rp_id>mironsoft.de</rp_id>
</webauthn>
</twofactorauth>
</default>
</config>
8. Enforcing admin MFA: configuration and CLI
Configuration takes place under Stores > Configuration > Security > 2FA in the admin area or directly via CLI using bin/magento config:set. The central configuration path twofactorauth/general/force_providers accepts a comma separated list of enforced providers; if a user is not registered for any of the listed providers, Magento blocks the login until enrollment is complete. Provider specific settings such as the issuer name for TOTP apps or the relying party ID for WebAuthn are located under their own configuration paths per provider.
Because force_providers applies globally to all admin users, a test with a second account is recommended before rollout, to avoid accidentally locking out all administrators. For emergencies, Magento offers the command bin/magento admin:user:unlock as well as database side recovery via the table tfa_user_config, in which the enrollment status per user and provider is stored. After every configuration change, bin/magento cache:flush is required so the new values take effect in the admin login flow.
# Enable the built-in Two-Factor Auth module (ships since Magento 2.3.7)
bin/magento module:enable Magento_TwoFactorAuth
bin/magento setup:upgrade
# Force Google Authenticator (TOTP) and WebAuthn for every admin user
bin/magento config:set twofactorauth/general/force_providers google,webauthn
# Set the issuer label shown inside the authenticator app
bin/magento config:set twofactorauth/google/issuer "Mironsoft Admin"
# Restrict the WebAuthn Relying Party ID to the admin domain
bin/magento config:set twofactorauth/webauthn/rp_id mironsoft.de
bin/magento cache:flush
9. MFA methods compared directly
SMS based one-time codes, TOTP authenticator apps, and WebAuthn/FIDO2 differ significantly in security level, implementation effort, and user experience. The following overview summarizes the most important decision criteria for use in the Magento admin area.
| Criterion | SMS-OTP | TOTP (Authenticator App) | WebAuthn/FIDO2 |
|---|---|---|---|
| Phishing resistance | No protection (interceptable) | No protection (code phishable) | Complete, thanks to origin binding |
| SIM swapping risk | High | No risk | No risk |
| Ongoing costs | SMS gateway fees per login | No ongoing costs | Hardware key optional (10-50 EUR) |
| Offline capability | Requires cellular network | Fully offline | Fully offline |
| Implementation effort | Low (SMS API) | Medium (library + QR code) | High (client and server logic) |
| User experience | High (familiar) | Medium (app required) | Very high (biometrics, one tap) |
In practice, the combination of TOTP as a cost efficient standard method and WebAuthn as an enforced addition for highly privileged accounts such as administrators is the most pragmatic solution. SMS-OTP should generally be avoided in new implementations, since NIST SP 800-63B has already classified it as restricted for several years, and SIM swapping attacks regularly succeed in practice.
Mironsoft
Magento Security Audits, MFA Implementation and Admin Hardening
Introduce multi-factor authentication professionally?
We implement TOTP, backup codes, and WebAuthn/FIDO2 for your Magento admin area, configure Magento_TwoFactorAuth correctly, and set up a secure recovery concept for emergencies.
MFA Implementation
TOTP, backup codes, and WebAuthn per RFC 6238 and the FIDO2 standard
Magento 2FA Configuration
Provider enforcement, recovery processes, and admin hardening
Security Audit
Complete review of authentication and session handling
10. Summary
Correctly implementing multi-factor authentication technically means combining several complementary building blocks cleanly. TOTP per RFC 6238 provides a cost efficient entry point with a library like spomky-labs/otphp: generate a shared secret, provision it via QR code, and verify codes with a tolerance window. Backup codes close the gap in case of device loss, but must be consistently hashed with password_hash() and invalidated immediately after use, so as not to become a vulnerability themselves. WebAuthn/FIDO2 goes a decisive step further and eliminates the fundamental problem of shared secrets through asymmetric cryptography and origin binding, rendering phishing attacks on the second factor technically ineffective.
For Magento shops, native integration via Magento_TwoFactorAuth is the most pragmatic starting point: the module has existed since 2.3.7, supports TOTP, WebAuthn, and other providers in parallel, and can be enforced for all admin users via force_providers. Anyone who consistently combines these building blocks, TOTP as the base, WebAuthn as a phishing resistant addition for critical accounts, and backup codes as a controlled recovery path, drastically reduces the risk of a compromised admin account without unnecessarily complicating the user experience.
Implementing Multi-Factor Authentication Technically - The Most Important Points at a Glance
TOTP per RFC 6238
HMAC-SHA1 over a 30 second time window, implemented with spomky-labs/otphp and provisioned via QR code.
Backup Codes
Generated with random_bytes(), stored via password_hash(ARGON2ID), invalidated immediately after use.
WebAuthn/FIDO2
Asymmetric cryptography with origin binding renders phishing attacks on the second factor ineffective.
Magento_TwoFactorAuth
Native since 2.3.7, force_providers enforces TOTP and WebAuthn for all admin users.