Instatic Authentication Flow with TOTP and Account Lockout: Security Implementation Guide

Instatic implements a three-layered defense combining per-IP rate limiting, exponential back-off account lockout, and optional TOTP-based MFA, orchestrated by server/handlers/cms/auth.ts with constant-time password verification and encrypted secret storage.

Instatic is a CMS that protects administrative interfaces through a hardened authentication pipeline. This article examines how the CoreBunch/Instatic repository implements time-based one-time password (TOTP) multi-factor authentication alongside intelligent account lockout mechanisms to prevent brute-force attacks and credential stuffing.

Three-Layered Defense Architecture

The authentication system relies on three specialized modules working in concert:

  • server/auth/lockout.ts – Calculates exponential back-off windows via evaluateFailedAttempt and checks lock status via evaluateLockState.
  • server/auth/mfa.ts – Generates secrets, builds provisioning URIs, and validates codes through verifyTotpCode.
  • server/auth/totpSecrets.ts – Encrypts and decrypts TOTP secrets using the server master key, exposing verifyEncryptedTotpCode for safe verification.

These modules support the main auth handler in server/handlers/cms/auth.ts, which coordinates rate limiting, credential verification, and session management.

Initial Login and Rate Limiting

The flow begins at POST /admin/api/cms/login in server/handlers/cms/auth.ts. The endpoint applies two rate-limiting layers before credential verification:

  • IP-wide limiting – loginPerIpRateLimit.consume(ip) tracks all login attempts from a single IP address.
  • IP + email tuple limiting – loginRateLimit.consume(rateLimitKey) tracks attempts targeting specific credentials.

If either limiter triggers, the server responds with HTTP 429 and a Retry-After header indicating the cooldown period.

To prevent timing attacks, verifyPassword executes in constant time regardless of whether the user exists in the database. This ensures attackers cannot enumerate valid email addresses through response timing analysis.

Account Lockout Policy

After password verification, the system checks the user's locked_until column via evaluateLockState in server/auth/lockout.ts (lines 95-115). If the account is locked, the server returns HTTP 423 (Locked) with a Retry-After header specifying the remaining lockout duration.

The lockout policy implements exponential back-off:

  • Threshold – LOCKOUT_THRESHOLD = 5 consecutive failures.
  • Initial duration – LOCKOUT_INITIAL_MS = 15 minutes.
  • Escalation – Each subsequent lockout doubles the duration.
  • Maximum – LOCKOUT_CAP_MS = 24 hours.

When a failure occurs, evaluateFailedAttempt(prevCount) returns whether to trigger a lockout, the new lockedUntil timestamp, and the updated failure count. This function is called after both failed password attempts and failed MFA attempts, ensuring brute-force resistance across both authentication factors.

TOTP MFA Verification

If the user has MFA enabled, the login response returns { mfaRequired: true } and creates a pending session with mfaPassedAt: null. The client must then submit the TOTP code to POST /admin/api/cms/auth/mfa/verify.

The MFA endpoint enforces additional protections:

  1. Account lockout check – evaluateLockState runs again to prevent session cookie brute-forcing (lines 19-25).
  2. MFA rate limiting – mfaRateLimit.consume(rateLimitKey) applies a third layer of rate limiting specific to code verification attempts.

The verification process in server/auth/totpSecrets.ts decrypts the stored secret using decryptTotpSecret, then validates the code through verifyTotpCode in server/auth/mfa.ts (lines 68-76).

Recovery Codes and Failure Handling

If TOTP verification fails, the system checks for a valid recovery code via findMatchingRecoveryCodeHash. These one-time codes provide account recovery when TOTP devices are unavailable.

Failed MFA attempts trigger the same lockout logic as password failures. The handler calls evaluateFailedAttempt to potentially extend the lockout window (lines 63-70), preventing attackers from bypassing lockouts by switching from password attacks to MFA brute-forcing.

Session Rotation

Upon successful MFA verification, the server calls rotateSessionToken to issue a fresh session token, sets the mfaPassedAt timestamp, and invalidates the pending session. This prevents session fixation attacks by ensuring the post-MFA session differs from the pre-MFA pending session. Each decision point logs events via createAuditEvent for forensic visibility.

Step-Up Re-Authentication

Sensitive operations (such as user deletion) require elevated privileges obtained through POST /admin/api/cms/auth/step-up. This endpoint mirrors the login flow:

  • Applies IP and account rate limiting.
  • Verifies the password via constant-time comparison.
  • Checks account lockout status via evaluateLockState.
  • Optionally verifies MFA through verifyStepUpMfa.

Upon successful step-up authentication, the server issues a new session token with a stepUpExpiresAt timestamp controlled by stepUpWindowMs. This short-lived elevated session restricts the time window for privileged actions.

Complete Authentication Examples

The following examples demonstrate client-side interaction with the authentication API:

Initial Login Request

await fetch('/admin/api/cms/login', {
  method: 'POST',
  body: JSON.stringify({ email, password }),
  headers: { 'Content-Type': 'application/json' },
})
  .then(r => r.json())
  .then(data => {
    if (data.mfaRequired) {
      // Display TOTP input dialog, then submit to /auth/mfa/verify
    } else {
      // Authentication complete; session cookie set automatically
    }
  });

MFA Verification

await fetch('/admin/api/cms/auth/mfa/verify', {
  method: 'POST',
  body: JSON.stringify({ code: totpCode }),
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include', // Required to send the pending session cookie
});

Step-Up Authentication

await fetch('/admin/api/cms/auth/step-up', {
  method: 'POST',
  body: JSON.stringify({ password, mfaCode: totpCode }),
  headers: { 'Content-Type': 'application/json' },
  credentials: 'include',
});

Server-Side Lockout Implementation

import { evaluateFailedAttempt, LOCKOUT_THRESHOLD } from '@/server/auth/lockout';

const lockout = evaluateFailedAttempt(user.failedLoginCount);
await recordFailedLoginAttempt(db, user.id, lockout.lockedUntil);

if (lockout.triggered) {
  // Return HTTP 423 with Retry-After header
}

Summary

  • Three-layered defense combines IP rate limiting, account lockout with exponential back-off (15 minutes to 24 hours), and optional TOTP MFA.
  • Constant-time password verification prevents user enumeration through timing analysis.
  • Encrypted TOTP storage in server/auth/totpSecrets.ts protects secrets at rest using the server master key.
  • Unified lockout policy applies to both password and MFA failures, preventing bypass attempts.
  • Session rotation after MFA verification prevents fixation attacks.
  • Step-up authentication provides short-lived elevated sessions for sensitive operations.

Frequently Asked Questions

How does Instatic prevent brute-force attacks against the login endpoint?

Instatic implements dual rate limiting through loginPerIpRateLimit and loginRateLimit, restricting both IP-wide traffic and per-credential guessing. Additionally, the account lockout system in server/auth/lockout.ts tracks consecutive failures and enforces exponential back-off, starting at 15 minutes and capping at 24 hours.

What happens if a user loses their TOTP device?

The system supports recovery codes via findMatchingRecoveryCodeHash. During MFA verification, if verifyTotpCode fails, the server checks the submitted code against stored recovery code hashes. Valid recovery codes bypass TOTP requirements but can only be used once.

Why does the MFA verification endpoint check account lockout status again?

The MFA endpoint applies evaluateLockState to prevent session brute-forcing. If an attacker steals a pending session cookie (created after correct password entry), they could attempt to guess the TOTP code. The secondary lockout check ensures these attempts still trigger account lockouts and rate limiting.

How does step-up authentication differ from standard login?

Step-up authentication (POST /admin/api/cms/auth/step-up) mirrors the login flow but issues a short-lived elevated session with a stepUpExpiresAt timestamp. Unlike the standard session, this token grants elevated privileges only for the duration specified in stepUpWindowMs, requiring re-authentication for subsequent sensitive operations.

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 →