Brazil's biggest MU Online portal — since 2003
Tutorial Advanced Infrastructure

How to protect your MU Online server's admin panel with two-factor authentication (2FA)

Implement TOTP-based two-factor authentication (2FA) in your MU Online server's PHP admin panel, with secret generation, QR Code, code verification, and an access recovery plan in case the player loses their device.

BR Bruno · Updated on Jul 28, 2024 · ⏱ 15 min read
Quick answer

The admin panel of a MU Online server is an attacker's most valuable target: a single compromised Administrator-level account can generate items, alter balances, ban players, or take down the entire database. Protecting this access with just a password — even a strong one — leaves the server vulnera

The admin panel of a MU Online server is an attacker's most valuable target: a single compromised Administrator-level account can generate items, alter balances, ban players, or take down the entire database. Protecting this access with just a password — even a strong one — leaves the server vulnerable to phishing, password reuse from another site's breach, and keyloggers. TOTP-based two-factor authentication (2FA) adds a second barrier that an attacker can't reproduce with just the stolen password. This tutorial shows how to implement 2FA in the server's legacy PHP panel, from the database side to the player's verification screen.

Why a password alone isn't enough

Passwords leak through several paths that have nothing to do with the password's strength: reuse of the same password on another site that suffered a breach, phishing disguised as an identical-looking login page, or keylogger malware on the administrator's machine. In any of these cases, the attacker gets the correct password and passes authentication normally. 2FA breaks this scenario because it requires a second factor — something the administrator has (the code generated on the phone), not just something they know (the password).

How TOTP (Time-based One-Time Password) works

TOTP generates a 6-digit numeric code that changes every 30 seconds, calculated from a secret shared between the server and the authenticator app (Google Authenticator, Authy, Microsoft Authenticator) and the current time. The server and the phone never exchange this code over the network at generation time — both calculate the same value independently, which makes the method resistant to network interception.

ComponentRole
Shared secretGenerated once, stored in the database and in the user's app
Current timestampSynced between server and device (30s window)
HMAC-SHA1 algorithm (RFC 6238)Combines secret + timestamp to generate the 6-digit code
Tolerance windowAccepts the code from the previous/next interval to tolerate small clock drift

Step 1 — Prepare the secrets table in the database

Add a column to store each admin account's TOTP secret, and a separate table for recovery codes:

ALTER TABLE admin_accounts ADD COLUMN totp_secret VARCHAR(64) NULL;
ALTER TABLE admin_accounts ADD COLUMN totp_enabled TINYINT(1) NOT NULL DEFAULT 0;

CREATE TABLE admin_recovery_codes (
    id INT AUTO_INCREMENT PRIMARY KEY,
    admin_id INT NOT NULL,
    code_hash VARCHAR(255) NOT NULL,
    used TINYINT(1) NOT NULL DEFAULT 0,
    FOREIGN KEY (admin_id) REFERENCES admin_accounts(id)
);

Never store the TOTP secret in publicly accessible plain text, and never store recovery codes without hashing them — treat them like single-use passwords.

Step 2 — Install a TOTP library in PHP

Use a well-established open source library instead of implementing the algorithm from scratch:

composer require spomky-labs/otphp

This avoids subtle RFC 6238 implementation bugs that could generate codes incompatible with standard authenticator apps.

Step 3 — Generate the secret and the activation QR Code

use OTPHP\TOTP;

$totp = TOTP::generate();
$totp->setLabel('PainelAdmin-ViciadosMU');
$totp->setIssuer('ViciadosMU');

$secret = $totp->getSecret();
// Save $secret (encrypted) in the logged-in admin's totp_secret column

$qrCodeUrl = $totp->getQrCodeUri(
    'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=[DATA]',
    '[DATA]'
);
// Display $qrCodeUrl on screen for the admin to scan with the authenticator app

Show the QR Code only once, on the activation screen, and never log the secret in an application log — it's equivalent to a master password for the second factor.

Step 4 — Verify the code at activation before confirming

Before setting totp_enabled = 1, require the administrator to type the code generated by the app, confirming the QR Code was scanned correctly:

$codigoDigitado = $_POST['codigo'];

if ($totp->verify($codigoDigitado)) {
    // Activate totp_enabled = 1 in the database
    // Generate and display recovery codes (one time only)
} else {
    // Error: invalid code, do not activate yet
}

This step avoids the scenario where the administrator thinks they activated 2FA, but the app wasn't configured correctly, and they get locked out on the next login.

Step 5 — Generate and display recovery codes

Upon successful activation, generate 8 to 10 single-use codes and display them only once, instructing the administrator to store them offline:

function gerarCodigosRecuperacao(int $quantidade = 10): array {
    $codigos = [];
    for ($i = 0; $i < $quantidade; $i++) {
        $codigos[] = strtoupper(bin2hex(random_bytes(4))); // e.g.: A1B2C3D4
    }
    return $codigos;
}

// Save only the hash of each code in the admin_recovery_codes table
foreach ($codigos as $codigo) {
    $hash = password_hash($codigo, PASSWORD_BCRYPT);
    // INSERT INTO admin_recovery_codes (admin_id, code_hash) VALUES (?, ?)
}

Step 6 — Require the second factor at login

In the login flow, after the password is validated correctly, show the TOTP code screen before granting the full admin session:

if ($senhaValida) {
    if ($admin['totp_enabled']) {
        // Redirect to the code verification screen,
        // without yet granting an administrative-level session
        $_SESSION['pending_2fa_admin_id'] = $admin['id'];
    } else {
        // Grant the session normally (or force 2FA activation, if mandatory)
    }
}
// On the verification screen
$codigo = $_POST['codigo'];
$totp = TOTP::createFromSecret($segredoDoBanco);

if ($totp->verify($codigo)) {
    // Grant the full administrative session
} else {
    // Check whether it's a valid recovery code (hash match and used = 0)
    // Otherwise, deny access and log the attempt
}

Step 7 — Define the mandatory-use policy by account level

Not every account needs the same level of rigor. Set the policy according to the access level's risk:

Account level2FA mandatory?Justification
OwnerYesFull access, greatest possible damage if compromised
AdministratorYesSensitive commands (additem, setmoney, ban)
Event moderatorRecommendedLimited access, but still distributes items
Junior supportOptionalLower economic-impact commands
Regular player accountOptional/encouragedPersonal account protection, not administrative

Step 8 — Recovery plan without compromising security

When an administrator loses their device and doesn't have recovery codes, define a manual, documented process instead of simply disabling 2FA on an informal Discord request:

  1. Require out-of-band identity verification (video call with another trusted administrator, or confirmation via a previously registered email).
  2. A second administrator (never the requester themselves) resets totp_enabled = 0 directly in the database.
  3. Force 2FA re-activation immediately on the next login.
  4. Log the incident internally, including who authorized the reset.

Common errors and fixes

SymptomLikely causeFix
App code doesn't match the serverPhone or server clock out of syncSync NTP on the server and check the phone's time
Admin lost access and has no recovery codesCodes never generated or not saved at activationImplement a manual reset process with cross-verification
TOTP secret exposed in application logsDebug/logging capturing the sensitive variableRemove secret variables from logs and rotate exposed secrets
2FA enabled but the session never asks for the second factorLogin flow doesn't check totp_enabled correctlyReview pending-session logic before granting full access
2FA reset performed with no identity verificationNo documented recovery processFormalize the process with a mandatory second administrator

2FA implementation checklist for the panel

  • TOTP secret and recovery codes table created in the database.
  • Trusted TOTP library integrated (e.g., spomky-labs/otphp).
  • Activation flow with QR Code and code verification before confirming.
  • Recovery codes generated, hashed, and shown only once.
  • Login requiring the second factor before granting a full admin session.
  • Mandatory-use policy defined by account level.
  • Documented access recovery process with cross-verification.

With the panel protected by 2FA, also review the rest of the admin team's access controls — the MU Online server setup guide covers the infrastructure foundation this extra security layer should be built on.

Frequently asked questions

Is SMS 2FA secure enough for the admin panel?

It's better than nothing, but SMS is vulnerable to SIM swapping and interception. For Administrator and Owner-level accounts, prefer TOTP (Google Authenticator, Authy) or a physical key (FIDO2/WebAuthn), keeping SMS at most as a secondary recovery method.

What happens if the administrator loses the phone with the authenticator?

That's why it's essential to generate and store recovery codes (backup codes) at the moment 2FA is activated, kept somewhere safe offline. Without them, the only option is a manual identity verification process to reset 2FA directly in the database.

Should 2FA be mandatory for every panel account level?

It's recommended as mandatory for levels with sensitive administrative commands (Admin, Owner) and optional but encouraged for regular player accounts. Making it mandatory for everyone at once can generate support friction if the community isn't prepared.

Does TOTP work without internet on the administrator's phone?

Yes. The TOTP algorithm generates the code locally from the shared secret and the device's clock, with no need for an internet connection at the moment of generation — only the phone's clock needs to be properly synced.

Do I need a paid library to implement TOTP in PHP?

No. Open source libraries like spomky-labs/otphp or simple implementations of the RFC 6238 algorithm can be integrated into the existing PHP panel for free, with no dependency on a paid external service.

BR
Events, maps & items editor

Bruno specializes in MU Online events, maps, bosses and item economy. He documents every detail based on real gameplay.

Keep reading

Related articles