Implementing Multi-Factor Authentication Technically
AI generated
OWASP
0x00
Security · MFA · TOTP · WebAuthn
Implementing Multi-Factor Authentication Technically
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.

14 min read RFC 6238 · Backup Codes · WebAuthn/FIDO2 Magento_TwoFactorAuth · PHP 8.4

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.

11. FAQ: Implementing Multi-Factor Authentication Technically

1What is the difference between TOTP and HOTP?
HOTP uses an increasing counter, TOTP replaces this with the current Unix time divided by a fixed time window, usually 30 seconds. The code thus changes automatically without synchronization effort.
2How securely must the TOTP shared secret be stored?
Encrypted in the database, never in plaintext or in logs. A leak equals permanent loss of the second factor for all affected accounts.
3Which PHP library is suitable for TOTP?
spomky-labs/otphp fully implements RFC 6238, including provisioning URI and verify() with a tolerance window for time deviations.
4How should backup codes be stored?
Shown in plaintext only once, then stored exclusively as a hash with password_hash(ARGON2ID). Mark as consumed immediately after use.
5Why is WebAuthn phishing resistant, but not TOTP?
WebAuthn cryptographically binds signatures to the domain, a credential does not work on phishing domains. TOTP codes are plain numbers and can be forwarded in real time.
6Platform vs. roaming authenticator?
Platform authenticators are built into the device (Touch ID, Windows Hello). Roaming authenticators are external hardware tokens like YubiKeys via USB, NFC, or Bluetooth.
7Since when does Magento include Two-Factor Auth?
Since Magento 2.3.7 in the core, active by default in 2.4.x. Covers only the Adminhtml area, not the storefront login.
8How do I enforce MFA for all admin users?
Via twofactorauth/general/force_providers, through the admin area or bin/magento config:set. Users without a registered provider are forced into enrollment.
9What happens if an admin loses their device?
Backup codes allow the user to keep logging in. Without these, recovery via tfa_user_config or bin/magento admin:user:unlock by another administrator helps.
10Should SMS-OTP still be used?
Not recommended for new implementations. NIST SP 800-63B classifies SMS-OTP as restricted, SIM swapping attacks regularly succeed in practice.