How Uptime Kuma Implements 2FA Authentication: A Complete Technical Guide to TOTP

Uptime Kuma implements 2FA authentication using Time-Based One-Time Password (TOTP) through WebSocket events, storing base32-encoded secrets in the user table and verifying tokens with the notp library using a 30-second time step and single-window tolerance.

Uptime Kuma is a widely-used open-source monitoring dashboard that requires robust access controls for production environments. The 2FA authentication system in Uptime Kuma leverages a custom WebSocket-based architecture to generate secrets, verify tokens, and manage user authentication flows without external dependencies. This implementation uses cryptographic random generation for secrets and enforces replay protection through database-level token tracking.

2FA Architecture and Core Components

The system uses Time-Based One-Time Password (TOTP) as defined in RFC 6238, implemented through socket.io events rather than traditional HTTP endpoints. The architecture spans five primary files:

  • server/server.js — Contains the core socket event handlers (prepare2FA, verifyToken, save2FA, disable2FA) and TOTP verification logic
  • server/2fa.js — Provides the TwoFA.disable2FA() helper for removing 2FA requirements
  • src/util.js — Houses the genSecret() function for cryptographically random secret generation
  • src/components/TwoFADialog.vue — Vue component managing the QR code display and token input interface
  • src/mixins/socket.js — Frontend wrapper providing convenient methods like prepare2FA() and verifyToken()

Server-Side 2FA Implementation in server/server.js

Generating Secrets with prepare2FA

The prepare2FA socket event creates a new TOTP secret when users initiate 2FA setup. The server calls genSecret() from src/util.js (lines 38-45) to generate a random alphanumeric string, encodes it using base32, and strips padding characters to create a standards-compliant secret. It stores the raw secret in the user.twofa_secret column and returns an otpauth:// URI for QR code generation.

// server/server.js – inside socket.on("prepare2FA")
let newSecret = genSecret();                         // random secret
let encodedSecret = base32.encode(newSecret);        // base32 encode
encodedSecret = encodedSecret.toString().replace(/=/g, ""); // remove padding

let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;

// Persist secret for the user
await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ?", [newSecret, socket.userID]);

callback({ ok: true, uri });

Verifying TOTP Tokens

Token verification occurs in the verifyToken event handler using the notp library. The system validates tokens against the stored secret with twoFAVerifyOptions—configured with a 30-second time step and a window of 1—allowing for slight clock drift. Critical security logic checks user.twofa_last_token to prevent replay attacks by rejecting previously used tokens.

// server/server.js – inside socket.on("verifyToken")
let verify = notp.totp.verify(token, user.twofa_secret, twoFAVerifyOptions);

if (user.twofa_last_token !== token && verify) {
    callback({ ok: true, valid: true });
} else {
    callback({ ok: false, msg: "authInvalidToken", msgi18n: true, valid: false });
}

Enabling and Disabling 2FA

Once verified, the save2FA event sets user.twofa_status = 1 to enable 2FA for the account. Conversely, disable2FA calls TwoFA.disable2FA(userID) from server/2fa.js, which executes an SQL UPDATE to reset twofa_status to 0 and clear the secret.

Database Schema and Security Model

The 2FA state persists in three columns of the user table:

  • twofa_status — Integer flag (0 = disabled, 1 = enabled)
  • twofa_secret — Raw TOTP secret used for token generation
  • twofa_last_token — Stores the most recently valid token to prevent replay attacks

This schema ensures that secrets never leave the server after generation, and the twofa_last_token field provides atomic protection against token replay attempts.

Client-Side 2FA Flow in TwoFADialog.vue

The Vue component src/components/TwoFADialog.vue orchestrates the user experience through three stages. First, prepare2FA() emits the socket event to retrieve the QR code URI:

// src/components/TwoFADialog.vue – method prepare2FA()
prepare2FA() {
    this.processing = true;
    this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
        this.processing = false;
        if (res.ok) {
            this.uri = res.uri;               // show QR code
        } else {
            this.$root.toastError(res.msg);
        }
    });
}

Second, verifyToken() validates the user's 6-digit code before enabling:

// src/components/TwoFADialog.vue – method verifyToken()
verifyToken() {
    this.$root.getSocket().emit("verifyToken", this.token, (res) => {
        if (res.ok && res.valid) {
            this.tokenValid = true;           // enable Save button
        } else {
            this.$root.toastError(res.msg);
        }
    });
}

Finally, save2FA() persists the 2FA state after confirmation:

// src/components/TwoFADialog.vue – save2FA() called after confirmation
save2FA() {
    this.processing = true;
    this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
        this.processing = false;
        if (res.ok) {
            this.getStatus();                // refresh status
        } else {
            this.$root.toastError(res.msg);
        }
    });
}

2FA Integration in the Login Flow

During authentication, the login socket event checks user.twofa_status after password verification. If enabled, the server responds with { tokenRequired: true }, prompting the client for a TOTP code. The server then verifies the supplied token using notp.totp.verify with the same twoFAVerifyOptions used during setup. Successful verification updates twofa_last_token and issues the authentication JWT.

Rate Limiting and Brute Force Protection

All 2FA-related socket events route through twoFaRateLimiter defined in server/rate-limiter.js. This middleware mitigates brute-force attempts against the 6-digit TOTP codes by limiting request frequency from individual IP addresses or user sessions, adding a critical layer of protection against automated attacks.

Summary

  • Uptime Kuma uses TOTP (Time-Based One-Time Password) with WebSocket events for all 2FA operations
  • Secrets are generated via genSecret() in src/util.js, base32-encoded, and stored in the user.twofa_secret column
  • Token verification uses notp.totp.verify with a 30-second step and window of 1 to prevent brute force while allowing clock drift
  • Replay protection is enforced by checking twofa_last_token against previously used codes
  • The frontend implementation lives in src/components/TwoFADialog.vue with socket wrappers in src/mixins/socket.js
  • Rate limiting via twoFaRateLimiter protects all 2FA endpoints from brute-force attacks

Frequently Asked Questions

What 2FA method does Uptime Kuma use?

Uptime Kuma implements standard Time-Based One-Time Password (TOTP) compatible with authenticator apps like Google Authenticator and Authy. It does not use SMS, email, or hardware keys, but rather generates RFC 6238-compliant TOTP codes through the notp library with 30-second validity windows.

How does Uptime Kuma prevent replay attacks?

The system stores the last valid token in the user.twofa_last_token database column. During verification in server/server.js, the server explicitly checks that the submitted token differs from this stored value before accepting it, ensuring each 6-digit code can only be used once for authentication.

What are the TOTP verification settings in Uptime Kuma?

According to the twoFAVerifyOptions defined in server/server.js, the implementation uses a 30-second time step with a verification window of 1. This allows the server to accept the current token plus one previous or future token, accommodating minor clock differences between the server and the user's device.

Which files contain the complete 2FA implementation?

The core logic resides in server/server.js (socket events), server/2fa.js (disable helper), and src/util.js (secret generation). The user interface is built in src/components/TwoFADialog.vue, while src/mixins/socket.js provides the frontend socket wrappers. Rate limiting is configured in server/rate-limiter.js.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →