Home/Blog/Secure Password & Authentication Flow Workflow
Workflows

Secure Password & Authentication Flow Workflow

Master the complete secure password and authentication workflow used by security teams worldwide. This comprehensive guide covers NIST 800-63B password guidelines, Argon2id hashing, multi-factor authentication, session management, brute force protection, and account recovery with practical implementation examples.

By InventiveHQ Team

Introduction {#introduction}

Authentication remains the primary attack vector for cybercriminals in 2025. According to the Verizon Data Breach Investigations Report 2024, 70% of data breaches involve compromised credentials, while Microsoft's security analysis reveals that 99.9% of account compromises are preventable with multi-factor authentication. Yet the authentication landscape has fundamentally changed—passwords alone are no longer sufficient, and legacy security practices like forced password expiration and arbitrary complexity rules have been proven counterproductive.

In this evolving threat landscape, organizations face multiple authentication challenges: distributed brute force campaigns leveraging 2.8 million unique IPs daily, credential stuffing attacks exploiting password reuse across sites, and sophisticated phishing operations bypassing traditional MFA methods. The average cost of a data breach now exceeds $4.45 million (IBM Security 2024), with regulatory compliance requirements from GDPR, CCPA, HIPAA, SOC 2, and PCI-DSS mandating stronger authentication controls.

This guide walks you through the complete secure password and authentication workflow used by security engineers, full-stack developers, and DevOps teams to implement production-grade authentication systems. We'll cover the full implementation from NIST-compliant password policies through phishing-resistant multi-factor authentication, following the NIST Special Publication 800-63B Digital Identity Guidelines that have revolutionized modern authentication practices.

The NIST Password Guidelines Revolution {#the-nist-password-guidelines-revolution}

The National Institute of Standards and Technology (NIST) has fundamentally shifted from complexity-focused to usability-focused security with their 800-63B guidelines. Key principle changes include:

  • Eliminate mandatory password expiration - Only require resets when compromise is detected
  • Remove arbitrary complexity requirements - No forced special characters or mixed-case rules
  • Support long passwords - Minimum 8 characters for standard accounts, 15+ for privileged access, support up to 64 characters
  • Block compromised passwords - Real-time screening against breach databases
  • Deprecate security questions - Knowledge-based authentication is easily exploited
  • Encourage password managers - Strongly recommended for secure storage
  • Support passphrases - Memorable phrases over complex passwords

These evidence-based guidelines recognize that length matters more than complexity, and that user-hostile policies like forced monthly password changes actually reduce security by encouraging predictable patterns like "Password123!" → "Password124!".

Authentication Workflow Overview {#authentication-workflow-overview}

This comprehensive 7-stage workflow covers everything needed for production-grade authentication:

  1. Password Policy Design & Validation (1-2 hours) - NIST-compliant requirements, breach detection, passphrase support
  2. Secure Password Hashing Implementation (2-3 hours) - Argon2id configuration, migration strategies, secure storage
  3. Multi-Factor Authentication (MFA) (3-4 hours) - WebAuthn/FIDO2, TOTP, backup codes
  4. Session & Token Management (2-3 hours) - JWT implementation, refresh token rotation, secure cookies
  5. OAuth 2.1 & OIDC Integration (3-4 hours) - Authorization Code + PKCE, social login providers
  6. Account Recovery & Password Reset (2-3 hours) - Secure tokens, rate limiting, enumeration prevention
  7. Brute Force Protection & Monitoring (2-3 hours) - Rate limiting, bot detection, behavioral analysis, alerting

Who This Guide Is For:

  • Full-stack developers implementing authentication systems
  • Security engineers conducting security audits and penetration tests
  • Backend engineers migrating from legacy authentication schemes
  • DevOps teams building security-first deployment pipelines
  • Compliance teams preparing for SOC 2, ISO 27001, and PCI-DSS audits

Let's begin with password policy design—establishing the foundation for secure authentication.


Stage 1: Password Policy Design & Validation (1-2 hours) {#stage-1-password-policy-design-validation-1-2-hours}

Effective password policies balance security and usability. Overly restrictive policies frustrate users and paradoxically reduce security by encouraging password reuse, predictable patterns, and insecure workarounds. NIST's evidence-based approach provides clear guidance for creating policies that genuinely improve security.

Step 1.1: NIST-Compliant Password Requirements (30 minutes) {#step-11-nist-compliant-password-requirements-30-minutes}

Goal: Implement password length and validation requirements aligned with NIST SP 800-63B.

Minimum Length Configuration:

// Password validation following NIST SP 800-63B
const PASSWORD_MIN_LENGTH = 8;    // Standard accounts
const PASSWORD_MIN_LENGTH_HIGH_SECURITY = 15;  // Admin/privileged accounts
const PASSWORD_MAX_LENGTH = 64;   // NIST recommends supporting up to 64

function validatePasswordLength(password, isHighSecurity = false) {
  const minLength = isHighSecurity ? PASSWORD_MIN_LENGTH_HIGH_SECURITY : PASSWORD_MIN_LENGTH;

  if (password.length < minLength) {
    return {
      valid: false,
      error: `Password must be at least ${minLength} characters`
    };
  }

  if (password.length > PASSWORD_MAX_LENGTH) {
    return {
      valid: false,
      error: `Password must not exceed ${PASSWORD_MAX_LENGTH} characters`
    };
  }

  return { valid: true };
}

Tool Integration: Password Strength Checker

Use our Password Strength Checker to evaluate password strength with:

  • Visual strength meter (weak, fair, good, strong, excellent)
  • Real-time entropy calculation (measure unpredictability)
  • Dictionary word detection (identify common passwords)
  • Pattern analysis (qwerty, 12345, abc123)
  • Personalization check (username, email fragments in password)
  • Character diversity scoring
  • Actionable improvement recommendations

NIST-Prohibited Practices (Do NOT Implement):

// ❌ WRONG - Arbitrary composition rules (NIST discourages)
function validatePasswordComplexity(password) {
  const hasUppercase = /[A-Z]/.test(password);
  const hasLowercase = /[a-z]/.test(password);
  const hasNumber = /[0-9]/.test(password);
  const hasSpecial = /[!@#$%^&*]/.test(password);

  // NIST says: Don't force this! Leads to predictable passwords
  return hasUppercase && hasLowercase && hasNumber && hasSpecial;
}

// ❌ WRONG - Mandatory password expiration
const PASSWORD_EXPIRATION_DAYS = 90; // NIST: Only reset on compromise

// ❌ WRONG - Password hints
const passwordHint = "Your childhood pet's name"; // NIST: Easily exploited

// ❌ WRONG - Security questions
const securityQuestions = [
  "Mother's maiden name",
  "First pet's name"
]; // NIST: Deprecated (social engineering risk)

✅ CORRECT - NIST-Aligned Validation:

// ✅ Focus on length and breach detection, not arbitrary rules
async function validatePassword(password, userContext = {}) {
  // 1. Length check
  const lengthCheck = validatePasswordLength(password);
  if (!lengthCheck.valid) return lengthCheck;

  // 2. Check against breached password database
  const isBreached = await checkPasswordBreach(password);
  if (isBreached) {
    return {
      valid: false,
      error: 'This password has been exposed in a data breach. Please choose a different password.'
    };
  }

  // 3. Check for context-specific information (username, email)
  if (userContext.username && password.toLowerCase().includes(userContext.username.toLowerCase())) {
    return {
      valid: false,
      error: 'Password must not contain your username'
    };
  }

  if (userContext.email) {
    const emailLocal = userContext.email.split('@')[0];
    if (password.toLowerCase().includes(emailLocal.toLowerCase())) {
      return {
        valid: false,
        error: 'Password must not contain parts of your email address'
      };
    }
  }

  // 4. Dictionary word check (common passwords)
  const isCommonPassword = await checkCommonPassword(password);
  if (isCommonPassword) {
    return {
      valid: false,
      error: 'This password is too common. Please choose a more unique password.'
    };
  }

  return { valid: true };
}

Step 1.2: Compromised Password Detection Integration (30-60 minutes) {#step-12-compromised-password-detection-integration-30-60-minutes}

Why This Matters: 86% of breaches use stolen credentials (Verizon DBIR). Preventing reuse of passwords exposed in data breaches is one of the most effective security controls available.

Have I Been Pwned API Integration:

The Have I Been Pwned service maintains a database of over 11 billion breached passwords. The k-Anonymity model allows you to check passwords without revealing them to the API.

import crypto from 'crypto';

/**
 * Check if password exists in breached password database
 * Uses k-Anonymity model - only first 5 chars of SHA-1 hash sent to API
 */
async function checkPasswordBreach(password) {
  // 1. Hash password with SHA-1
  const sha1Hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();

  // 2. k-Anonymity: Send only first 5 characters
  const hashPrefix = sha1Hash.substring(0, 5);
  const hashSuffix = sha1Hash.substring(5);

  // 3. Query HIBP API
  const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`, {
    headers: {
      'Add-Padding': 'true'  // Prevent response size analysis
    }
  });

  if (!response.ok) {
    // Fail open - don't block legitimate users if API unavailable
    console.error('Breach API unavailable, allowing password');
    return false;
  }

  const hashList = await response.text();

  // 4. Check if suffix appears in response
  const hashes = hashList.split('\n');
  for (const line of hashes) {
    const [suffix, count] = line.split(':');
    if (suffix === hashSuffix) {
      console.warn(`Password found in ${count} breaches`);
      return true;  // Password is breached
    }
  }

  return false;  // Password not found in breaches
}

Privacy-Preserving Design:

  • k-Anonymity model - Only 5-character hash prefix sent to API
  • Full password never transmitted - Verification happens client-side
  • Response padding - Prevents timing attacks based on response size
  • No logging - User passwords never recorded

Alternative: Self-Hosted Breach Database

For organizations requiring on-premise solutions or handling classified information:

// Download Pwned Passwords hash file (38GB+ as of 2025)
// https://haveibeenpwned.com/Passwords

import fs from 'fs';
import readline from 'readline';

async function checkPasswordBreachLocal(password) {
  const sha1Hash = crypto.createHash('sha1').update(password).digest('hex').toUpperCase();
  const hashPrefix = sha1Hash.substring(0, 5);

  // Binary search pre-sorted hash file
  const fileStream = fs.createReadStream('pwned-passwords-sha1-ordered.txt');
  const rl = readline.createInterface({ input: fileStream });

  for await (const line of rl) {
    if (line.startsWith(hashPrefix)) {
      if (line.startsWith(sha1Hash)) {
        return true;  // Breached
      }
    } else if (line > sha1Hash) {
      break;  // Hash not found
    }
  }

  return false;
}

Step 1.3: Passphrase Support & Generation (20-30 minutes) {#step-13-passphrase-support-generation-20-30-minutes}

Why Passphrases: NIST recommends passphrases over complex passwords. The famous xkcd comic illustrates this perfectly: "correct-horse-battery-staple" (44 bits of entropy) is significantly stronger and more memorable than "Tr0ub4dor&3" (28 bits of entropy).

Tool Integration: Secure Password Generator

Use our Secure Password Generator to create:

  • Cryptographically random passwords (8-128 characters)
  • Customizable character sets (uppercase, lowercase, numbers, symbols)
  • Passphrase generation (4-8 random words from Diceware dictionary)
  • Diceware word lists for maximum entropy
  • Pronunciation hints for memorability
  • One-click copy to clipboard with auto-clear
  • Bulk generation with CSV/JSON export for team password managers

Passphrase Generation Implementation:

// Diceware word list (7776 words = 6 dice rolls = 12.9 bits entropy per word)
const dicewareWords = [
  'ability', 'able', 'about', 'above', 'accept', 'account', 'achieve',
  // ... 7770 more words
];

function generatePassphrase(wordCount = 6, separator = '-') {
  const words = [];

  for (let i = 0; i < wordCount; i++) {
    // Cryptographically secure random word selection
    const randomIndex = crypto.randomInt(0, dicewareWords.length);
    words.push(dicewareWords[randomIndex]);
  }

  const passphrase = words.join(separator);

  // Calculate entropy
  const entropyBits = Math.log2(Math.pow(dicewareWords.length, wordCount));

  return {
    passphrase,
    entropy: entropyBits,
    wordCount,
    strength: entropyBits > 77 ? 'excellent' : entropyBits > 64 ? 'strong' : 'good'
  };
}

// Example output:
// {
//   passphrase: 'correct-horse-battery-staple-mountain-ocean',
//   entropy: 77.5,  // 6 words × 12.9 bits
//   wordCount: 6,
//   strength: 'excellent'
// }

Passphrase Strength Comparison:

  • 4 words: 51.6 bits entropy (good for low-security applications)
  • 6 words: 77.5 bits entropy (strong for most use cases)
  • 8 words: 103.2 bits entropy (excellent for high-security environments)

Step 1.4: Password Strength Meter Implementation (20-30 minutes) {#step-14-password-strength-meter-implementation-20-30-minutes}

Goal: Provide real-time feedback to users as they create passwords, guiding them toward stronger choices.

Password Strength Calculation:

function calculatePasswordStrength(password) {
  let score = 0;
  const feedback = [];

  // 1. Length scoring (most important factor)
  if (password.length >= 8) score += 10;
  if (password.length >= 12) score += 10;
  if (password.length >= 16) score += 10;
  if (password.length >= 20) score += 10;

  // 2. Character diversity
  const hasLowercase = /[a-z]/.test(password);
  const hasUppercase = /[A-Z]/.test(password);
  const hasNumbers = /[0-9]/.test(password);
  const hasSpecial = /[^a-zA-Z0-9]/.test(password);

  const charTypesCount = [hasLowercase, hasUppercase, hasNumbers, hasSpecial]
    .filter(Boolean).length;
  score += charTypesCount * 5;

  // 3. Entropy calculation
  let charsetSize = 0;
  if (hasLowercase) charsetSize += 26;
  if (hasUppercase) charsetSize += 26;
  if (hasNumbers) charsetSize += 10;
  if (hasSpecial) charsetSize += 32;

  const entropy = Math.log2(Math.pow(charsetSize, password.length));
  score += Math.min(entropy / 2, 30);  // Cap at 30 points from entropy

  // 4. Pattern detection (reduce score for patterns)
  const commonPatterns = [
    /^(.)\1+$/,              // All same character (aaaaaa)
    /^(.)(.)\1\2\1\2/,       // Repeating pairs (ababab)
    /^[0-9]+$/,              // All numbers
    /qwerty|asdf|zxcv/i,     // Keyboard patterns
    /password|admin|user/i,  // Common words
    /123|abc|111|000/i       // Sequential patterns
  ];

  for (const pattern of commonPatterns) {
    if (pattern.test(password)) {
      score -= 20;
      feedback.push('Avoid common patterns and keyboard sequences');
    }
  }

  // 5. Determine strength level
  let strength;
  if (score >= 70) strength = 'excellent';
  else if (score >= 50) strength = 'strong';
  else if (score >= 30) strength = 'good';
  else if (score >= 15) strength = 'fair';
  else strength = 'weak';

  // 6. Generate feedback
  if (password.length < 12) feedback.push('Use at least 12 characters for better security');
  if (charTypesCount < 3) feedback.push('Include a mix of letters, numbers, and symbols');
  if (entropy < 40) feedback.push('Consider using a passphrase (e.g., "correct-horse-battery-staple")');

  return {
    score: Math.max(0, Math.min(100, score)),
    strength,
    entropy: Math.round(entropy),
    feedback,
    estimatedCrackTime: calculateCrackTime(entropy)
  };
}

function calculateCrackTime(entropy) {
  const guessesPerSecond = 1e9; // 1 billion guesses/second (GPU)
  const totalCombinations = Math.pow(2, entropy);
  const secondsToCrack = totalCombinations / guessesPerSecond / 2; // Average case

  if (secondsToCrack < 60) return 'Instant';
  if (secondsToCrack < 3600) return `${Math.round(secondsToCrack / 60)} minutes`;
  if (secondsToCrack < 86400) return `${Math.round(secondsToCrack / 3600)} hours`;
  if (secondsToCrack < 31536000) return `${Math.round(secondsToCrack / 86400)} days`;
  if (secondsToCrack < 31536000 * 100) return `${Math.round(secondsToCrack / 31536000)} years`;
  return 'Centuries';
}

Visual Feedback Component (React Example):

function PasswordStrengthMeter({ password }: { password: string }) {
  const strength = calculatePasswordStrength(password);

  const colors = {
    weak: 'bg-red-500',
    fair: 'bg-orange-500',
    good: 'bg-yellow-500',
    strong: 'bg-green-500',
    excellent: 'bg-green-600'
  };

  return (
    <div className="space-y-2">
      {/* Progress bar */}
      <div className="h-2 w-full bg-gray-200 rounded-full overflow-hidden">
        <div
          className={`h-full transition-all ${colors[strength.strength]}`}
          style={{ width: `${strength.score}%` }}
        />
      </div>

      {/* Strength label */}
      <div className="flex justify-between items-center">
        <span className="text-sm font-medium">
          Strength: <span className="capitalize">{strength.strength}</span>
        </span>
        <span className="text-xs text-gray-600">
          {strength.entropy} bits entropy
        </span>
      </div>

      {/* Crack time estimate */}
      <p className="text-xs text-gray-600">
        Estimated crack time: <strong>{strength.estimatedCrackTime}</strong>
      </p>

      {/* Feedback messages */}
      {strength.feedback.length > 0 && (
        <ul className="text-xs text-gray-700 space-y-1">
          {strength.feedback.map((msg, i) => (
            <li key={i} className="flex items-start gap-2">
              <span className="text-blue-500">ℹ</span>
              <span>{msg}</span>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Stage 1 Deliverables:

After completing this stage, you should have:

  • NIST-compliant password validation functions
  • Compromised password detection integration (HIBP API or self-hosted)
  • Passphrase generation capabilities
  • Real-time password strength meter with user feedback
  • Documented password policy aligned with NIST SP 800-63B

Time Investment: 1-2 hours Next Step: Proceed to Stage 2 to implement secure password hashing with Argon2id.


Stage 2: Secure Password Hashing Implementation (2-3 hours) {#stage-2-secure-password-hashing-implementation-2-3-hours}

Password hashing transforms passwords into irreversible cryptographic hashes, ensuring that even if your database is compromised, attackers cannot retrieve plaintext passwords. The choice of hashing algorithm directly impacts your application's resistance to brute force attacks.

Modern Password Hashing Algorithms Comparison {#modern-password-hashing-algorithms-comparison}

2025 Algorithm Ranking:

AlgorithmSecurity LevelMemory HardnessAuth TimeRecommendation
Argon2idExcellent128MB (configurable)220-280msFirst choice for new systems
bcryptGoodFixed 4KB250-350msSolid fallback, legacy support
scryptGood128MB (configurable)180-300msUse when Argon2 unavailable
PBKDF2FairNone (weakness)200-280msCompliance only (FIPS-140)

Why Argon2id Wins:

  • Winner of Password Hashing Competition (2015)
  • Memory-hard algorithm (resistant to GPU and ASIC attacks)
  • Configurable parameters (memory, time, parallelism)
  • Hybrid mode combines data-dependent and data-independent approaches
  • Protection against side-channel attacks

Step 2.1: Argon2id Configuration & Implementation (45-90 minutes) {#step-21-argon2id-configuration-implementation-45-90-minutes}

Goal: Implement Argon2id password hashing with production-ready configuration.

Installation:

# Node.js
npm install argon2

# Python
pip install argon2-cffi

# Go
go get golang.org/x/crypto/argon2

# PHP (built-in as of PHP 7.2)
# Use password_hash($password, PASSWORD_ARGON2ID)

Production Configuration (2025 Recommendations):

import argon2 from 'argon2';

const ARGON2_CONFIG = {
  type: argon2.argon2id,     // Hybrid mode (recommended)
  memoryCost: 65536,         // 64 MB (2^16 KB)
  timeCost: 3,               // 3 iterations
  parallelism: 2,            // 2 threads
  hashLength: 32,            // 256-bit hash output
  saltLength: 16             // 128-bit salt
};

/**
 * Hash password using Argon2id
 */
async function hashPassword(password) {
  try {
    const hash = await argon2.hash(password, ARGON2_CONFIG);
    return hash;
    // Example output:
    // $argon2id$v=19$m=65536,t=3,p=2$randomsalt$hashedpassword
  } catch (err) {
    console.error('Password hashing failed:', err);
    throw new Error('Failed to hash password');
  }
}

/**
 * Verify password against stored hash
 */
async function verifyPassword(password, hash) {
  try {
    return await argon2.verify(hash, password);
  } catch (err) {
    console.error('Password verification failed:', err);
    return false;
  }
}

Parameter Tuning Guide:

Benchmark Argon2 on your production hardware to determine optimal parameters. Target authentication time: 250-500ms.

/**
 * Benchmark Argon2 on your production hardware
 */
async function benchmarkArgon2() {
  const testPassword = 'TestPassword123!';
  const configs = [
    { memoryCost: 32768, timeCost: 2, parallelism: 1 },  // Low
    { memoryCost: 65536, timeCost: 3, parallelism: 2 },  // Medium (recommended)
    { memoryCost: 131072, timeCost: 4, parallelism: 4 }, // High
  ];

  for (const config of configs) {
    const start = Date.now();
    await argon2.hash(testPassword, { ...config, type: argon2.argon2id });
    const duration = Date.now() - start;

    console.log(`Memory: ${config.memoryCost}KB, Time: ${config.timeCost}, Parallelism: ${config.parallelism}`);
    console.log(`Authentication time: ${duration}ms\n`);
  }
}

// Run on production hardware to determine optimal parameters
// benchmarkArgon2();

Tool Integration: Hash Generator

Use our Hash Generator to:

  • Generate Argon2, bcrypt, PBKDF2, and scrypt hashes
  • Verify passwords against stored hashes
  • Configure parameters (memory cost, time cost, parallelism)
  • Benchmark performance on your hardware
  • Compare hash outputs across algorithms
  • Generate migration hashes (dual hashing during algorithm migration)

Step 2.2: Algorithm Migration Strategy (30-60 minutes) {#step-22-algorithm-migration-strategy-30-60-minutes}

Scenario: Migrating from MD5/SHA-1/bcrypt to Argon2id without forcing password resets.

Approach: Lazy Migration (Transparent Upgrade)

This strategy automatically upgrades users to Argon2id when they next log in, avoiding the user friction of forced password resets.

/**
 * Unified password verification with automatic migration
 */
async function verifyPasswordWithMigration(password, storedHash, userId) {
  let isValid = false;
  let needsMigration = false;

  // 1. Detect hash algorithm
  if (storedHash.startsWith('$argon2id$')) {
    // Already using Argon2id
    isValid = await argon2.verify(storedHash, password);

  } else if (storedHash.startsWith('$2b$') || storedHash.startsWith('$2a$')) {
    // Legacy bcrypt
    isValid = await bcrypt.compare(password, storedHash);
    needsMigration = true;

  } else if (storedHash.startsWith('pbkdf2_sha256$')) {
    // Legacy PBKDF2
    isValid = await verifyPasswordPBKDF2(password, storedHash);
    needsMigration = true;

  } else if (storedHash.length === 32 && /^[a-f0-9]+$/.test(storedHash)) {
    // Legacy MD5 (INSECURE - migrate immediately)
    const md5Hash = crypto.createHash('md5').update(password).digest('hex');
    isValid = crypto.timingSafeEqual(Buffer.from(md5Hash), Buffer.from(storedHash));
    needsMigration = true;

  } else {
    console.error('Unknown hash format:', storedHash.substring(0, 20));
    return false;
  }

  // 2. If valid and needs migration, rehash with Argon2id
  if (isValid && needsMigration) {
    const newHash = await hashPassword(password);
    await updateUserPasswordHash(userId, newHash);
    console.log(`Migrated user ${userId} from legacy hash to Argon2id`);
  }

  return isValid;
}

/**
 * Database update function
 */
async function updateUserPasswordHash(userId, newHash) {
  await db.query(
    'UPDATE users SET password_hash = $1, password_updated_at = NOW() WHERE id = $2',
    [newHash, userId]
  );
}

Migration Monitoring:

Track migration progress to understand how many users still need to log in for automatic migration:

/**
 * Track migration progress
 */
async function getMigrationStats() {
  const stats = await db.query(`
    SELECT
      CASE
        WHEN password_hash LIKE '$argon2id$%' THEN 'argon2id'
        WHEN password_hash LIKE '$2b$%' THEN 'bcrypt'
        WHEN password_hash LIKE 'pbkdf2_%' THEN 'pbkdf2'
        WHEN LENGTH(password_hash) = 32 THEN 'md5'
        ELSE 'unknown'
      END AS algorithm,
      COUNT(*) AS count,
      ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS percentage
    FROM users
    GROUP BY algorithm
    ORDER BY count DESC
  `);

  return stats.rows;
}

// Example output:
// [
//   { algorithm: 'argon2id', count: 12543, percentage: 62.15 },  // 62% migrated
//   { algorithm: 'bcrypt', count: 7234, percentage: 35.84 },     // 36% pending
//   { algorithm: 'md5', count: 412, percentage: 2.04 }           // 2% legacy (URGENT)
// ]

Step 2.3: Secure Storage & Database Schema (20-30 minutes) {#step-23-secure-storage-database-schema-20-30-minutes}

Password Hash Storage Schema:

CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash TEXT NOT NULL,  -- Store full hash with algorithm prefix
  password_updated_at TIMESTAMPTZ DEFAULT NOW(),
  password_history JSONB DEFAULT '[]'::jsonb,  -- For password reuse prevention
  account_locked BOOLEAN DEFAULT FALSE,
  failed_login_attempts INT DEFAULT 0,
  last_failed_login TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_account_locked ON users(account_locked) WHERE account_locked = TRUE;

Password History (Prevent Reuse):

Implement password history tracking to prevent users from cycling through a small set of passwords:

const PASSWORD_HISTORY_COUNT = 5;  // Remember last 5 passwords

/**
 * Check if password was previously used
 */
async function isPasswordReused(userId, newPassword) {
  const user = await db.query(
    'SELECT password_hash, password_history FROM users WHERE id = $1',
    [userId]
  );

  const currentHash = user.rows[0].password_hash;
  const history = user.rows[0].password_history || [];

  // Check current password
  if (await verifyPasswordWithMigration(newPassword, currentHash, userId)) {
    return true;  // Same as current password
  }

  // Check password history
  for (const oldHash of history) {
    if (await argon2.verify(oldHash, newPassword)) {
      return true;  // Password was used before
    }
  }

  return false;  // Password not reused
}

/**
 * Update password with history tracking
 */
async function updatePassword(userId, newPassword) {
  // 1. Check for reuse
  if (await isPasswordReused(userId, newPassword)) {
    throw new Error('Cannot reuse previous passwords');
  }

  // 2. Hash new password
  const newHash = await hashPassword(newPassword);

  // 3. Get current hash for history
  const user = await db.query('SELECT password_hash, password_history FROM users WHERE id = $1', [userId]);
  const currentHash = user.rows[0].password_hash;
  let history = user.rows[0].password_history || [];

  // 4. Update history (keep last N passwords)
  history.unshift(currentHash);
  history = history.slice(0, PASSWORD_HISTORY_COUNT - 1);

  // 5. Update database
  await db.query(
    `UPDATE users
     SET password_hash = $1,
         password_history = $2,
         password_updated_at = NOW()
     WHERE id = $3`,
    [newHash, JSON.stringify(history), userId]
  );
}

Stage 2 Deliverables:

After completing this stage, you should have:

  • Argon2id password hashing implementation with production configuration
  • Lazy migration strategy for seamless algorithm upgrades
  • Password history tracking to prevent password reuse
  • Secure database schema for password storage
  • Migration monitoring dashboard

Time Investment: 2-3 hours Next Step: Proceed to Stage 3 to implement multi-factor authentication.


Stage 3: Multi-Factor Authentication (MFA) Implementation (3-4 hours) {#stage-3-multi-factor-authentication-mfa-implementation-3-4-hours}

Multi-factor authentication (MFA) is the single most effective security control available, preventing 99.9% of automated attacks according to Microsoft's security analysis. Modern MFA has evolved beyond SMS codes to include phishing-resistant methods like WebAuthn/FIDO2 hardware keys and platform authenticators (Face ID, Touch ID, Windows Hello).

MFA Method Security Hierarchy {#mfa-method-security-hierarchy}

Tier 1: Phishing-Resistant (Recommended for High-Risk)

  • FIDO2/WebAuthn hardware keys (YubiKey, Titan Security Key)
  • Platform authenticators (Face ID, Touch ID, Windows Hello)
  • Passkeys (FIDO2 credentials synced via iCloud Keychain, Google Password Manager)
  • PIV/CAC smart cards

Tier 2: TOTP & Push Notifications (Acceptable for Medium-Risk)

  • TOTP authenticator apps (Google Authenticator, Authy, Microsoft Authenticator)
  • Push notifications with number matching (reduces push fatigue attacks)
  • Email-based codes (short-lived, 10-minute expiration)

Tier 3: SMS (Deprecated, Use Only as Last Resort)

  • SMS one-time codes (vulnerable to SIM swapping, SS7 attacks)
  • NIST no longer recommends SMS as MFA
  • Acceptable only as fallback for account recovery

Step 3.1: WebAuthn/FIDO2 Registration (60-90 minutes) {#step-31-webauthn-fido2-registration-60-90-minutes}

Why WebAuthn:

  • 93% login success rate vs. 63% for traditional passwords
  • Phishing-resistant - Origin-bound credentials prevent credential reuse across domains
  • Public key cryptography - No shared secrets stored on server
  • Privacy-preserving - No cross-site tracking
  • Standardized - W3C standard supported by all modern browsers

Server-Side Setup (Node.js with SimpleWebAuthn):

npm install @simplewebauthn/server @simplewebauthn/browser
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';

const RP_NAME = 'InventiveHQ';
const RP_ID = 'inventivehq.com';  // Domain
const ORIGIN = 'https://inventivehq.com';

/**
 * Step 1: Generate registration challenge
 */
async function getWebAuthnRegistrationOptions(userId, username) {
  const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

  const options = await generateRegistrationOptions({
    rpName: RP_NAME,
    rpID: RP_ID,
    userID: userId,
    userName: username,
    userDisplayName: user.rows[0].email,

    // Attestation (optional - verify authenticator)
    attestationType: 'none',  // 'direct' for enterprise

    // Authenticator selection
    authenticatorSelection: {
      authenticatorAttachment: 'cross-platform',  // External key (YubiKey)
      // OR 'platform' for built-in (Touch ID, Windows Hello)

      requireResidentKey: false,
      residentKey: 'discouraged',
      userVerification: 'preferred'
    },

    // Exclude already registered authenticators
    excludeCredentials: await getUserAuthenticators(userId),

    // Challenge timeout
    timeout: 60000  // 60 seconds
  });

  // Store challenge for verification
  await storeChallenge(userId, options.challenge);

  return options;
}

/**
 * Step 2: Verify registration response
 */
async function verifyWebAuthnRegistration(userId, credential) {
  const challenge = await getStoredChallenge(userId);

  const verification = await verifyRegistrationResponse({
    response: credential,
    expectedChallenge: challenge,
    expectedOrigin: ORIGIN,
    expectedRPID: RP_ID,
    requireUserVerification: true
  });

  if (!verification.verified) {
    throw new Error('WebAuthn registration verification failed');
  }

  // Store credential
  const { credentialID, credentialPublicKey, counter } = verification.registrationInfo;

  await db.query(
    `INSERT INTO webauthn_credentials
     (user_id, credential_id, public_key, counter, transports, created_at)
     VALUES ($1, $2, $3, $4, $5, NOW())`,
    [
      userId,
      Buffer.from(credentialID).toString('base64'),
      Buffer.from(credentialPublicKey).toString('base64'),
      counter,
      JSON.stringify(credential.response.transports)
    ]
  );

  return { success: true };
}

Client-Side (Browser):

import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

/**
 * Register WebAuthn credential
 */
async function registerWebAuthnCredential() {
  try {
    // 1. Get registration options from server
    const optionsResponse = await fetch('/api/auth/webauthn/register/options', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include'
    });
    const options = await optionsResponse.json();

    // 2. Trigger browser WebAuthn API
    const credential = await startRegistration(options);

    // 3. Send credential to server for verification
    const verifyResponse = await fetch('/api/auth/webauthn/register/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ credential })
    });

    const result = await verifyResponse.json();

    if (result.success) {
      alert('Security key registered successfully!');
    } else {
      alert('Registration failed: ' + result.error);
    }

  } catch (error) {
    console.error('WebAuthn registration error:', error);
    alert('Failed to register security key. Please try again.');
  }
}

Database Schema:

CREATE TABLE webauthn_credentials (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  credential_id TEXT UNIQUE NOT NULL,  -- Base64-encoded credential ID
  public_key TEXT NOT NULL,             -- Base64-encoded public key
  counter BIGINT NOT NULL DEFAULT 0,    -- Signature counter (clone detection)
  transports JSONB,                     -- ['usb', 'nfc', 'ble', 'internal']
  friendly_name VARCHAR(100),           -- User-assigned name ("YubiKey 5C", "Touch ID")
  created_at TIMESTAMPTZ DEFAULT NOW(),
  last_used_at TIMESTAMPTZ
);

CREATE INDEX idx_webauthn_user_id ON webauthn_credentials(user_id);
CREATE INDEX idx_webauthn_credential_id ON webauthn_credentials(credential_id);

Step 3.2: TOTP Authenticator App Setup (45-60 minutes) {#step-32-totp-authenticator-app-setup-45-60-minutes}

Why TOTP:

  • Widely supported (Google Authenticator, Authy, Microsoft Authenticator)
  • Works offline (time-based, no network required)
  • Better than SMS (no SIM swapping vulnerability)
  • Industry standard (RFC 6238)

TOTP Implementation:

npm install otpauth qrcode
import { TOTP } from 'otpauth';
import qrcode from 'qrcode';

/**
 * Generate TOTP secret and QR code
 */
async function setupTOTP(userId, username) {
  // 1. Generate random secret (160 bits / 32 base32 characters)
  const secret = TOTP.Secret();

  // 2. Create TOTP instance
  const totp = new TOTP({
    issuer: 'InventiveHQ',
    label: username,
    algorithm: 'SHA1',      // Most compatible (SHA256 supported by newer apps)
    digits: 6,              // 6-digit codes
    period: 30,             // 30-second validity
    secret: secret
  });

  // 3. Generate QR code
  const otpauthURL = totp.toString();  // otpauth://totp/...
  const qrCodeDataURL = await qrcode.toDataURL(otpauthURL);

  // 4. Store secret (encrypted at rest)
  await db.query(
    `INSERT INTO totp_secrets (user_id, secret, created_at, verified)
     VALUES ($1, $2, NOW(), FALSE)`,
    [userId, secret.base32]
  );

  return {
    secret: secret.base32,
    qrCode: qrCodeDataURL,
    manualEntry: secret.base32.match(/.{4}/g).join(' ')  // Pretty format: ABCD EFGH IJKL
  };
}

/**
 * Verify TOTP code
 */
async function verifyTOTP(userId, code) {
  const result = await db.query(
    'SELECT secret FROM totp_secrets WHERE user_id = $1 AND verified = TRUE',
    [userId]
  );

  if (result.rows.length === 0) {
    return { valid: false, error: 'TOTP not configured' };
  }

  const secret = result.rows[0].secret;

  const totp = new TOTP({
    algorithm: 'SHA1',
    digits: 6,
    period: 30,
    secret: secret
  });

  // Allow ±1 time window (90 seconds total) for clock drift
  const delta = totp.validate({ token: code, window: 1 });

  if (delta !== null) {
    // Update last used timestamp
    await db.query(
      'UPDATE totp_secrets SET last_used_at = NOW() WHERE user_id = $1',
      [userId]
    );

    return { valid: true };
  }

  return { valid: false, error: 'Invalid code' };
}

Step 3.3: Backup Codes Generation (20-30 minutes) {#step-33-backup-codes-generation-20-30-minutes}

Why Backup Codes: Users lose devices, hardware keys malfunction, and phones run out of battery. Backup codes provide a secure recovery method when primary MFA factors are unavailable.

Backup Code Implementation:

/**
 * Generate backup codes
 */
async function generateBackupCodes(userId, count = 10) {
  const codes = [];

  for (let i = 0; i < count; i++) {
    // Generate cryptographically secure 8-character code
    const code = crypto.randomBytes(4).toString('hex').toUpperCase();
    const formattedCode = code.match(/.{4}/g).join('-');  // Format: ABCD-1234

    // Hash code for storage (like passwords)
    const hash = await hashPassword(formattedCode);

    codes.push({
      code: formattedCode,
      hash: hash
    });
  }

  // Store hashed codes
  await db.query(
    `INSERT INTO backup_codes (user_id, code_hash, created_at)
     VALUES ${codes.map((_, i) => `($1, $${i + 2}, NOW())`).join(', ')}`,
    [userId, ...codes.map(c => c.hash)]
  );

  // Return plaintext codes to user (only shown once)
  return codes.map(c => c.code);
}

/**
 * Verify and consume backup code
 */
async function verifyBackupCode(userId, code) {
  const result = await db.query(
    'SELECT id, code_hash FROM backup_codes WHERE user_id = $1 AND used = FALSE',
    [userId]
  );

  for (const row of result.rows) {
    if (await verifyPassword(code, row.code_hash)) {
      // Mark code as used
      await db.query(
        'UPDATE backup_codes SET used = TRUE, used_at = NOW() WHERE id = $1',
        [row.id]
      );

      return { valid: true };
    }
  }

  return { valid: false, error: 'Invalid or already used backup code' };
}

Database Schema:

CREATE TABLE backup_codes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  code_hash TEXT NOT NULL,
  used BOOLEAN DEFAULT FALSE,
  used_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_backup_codes_user_id ON backup_codes(user_id);
CREATE INDEX idx_backup_codes_unused ON backup_codes(user_id, used) WHERE used = FALSE;

Stage 3 Deliverables:

After completing this stage, you should have:

  • WebAuthn/FIDO2 registration and authentication flow
  • TOTP authenticator app enrollment with QR codes
  • Backup code generation and verification
  • Database schema for MFA credentials
  • Multi-factor authentication enforcement logic

Time Investment: 3-4 hours Next Step: Proceed to Stage 4 to implement session and token management.


Stage 4: Session & Token Management (2-3 hours) {#stage-4-session-token-management-2-3-hours}

Session management determines how long users remain authenticated and how their authentication state is maintained across requests. Poor session management leads to session hijacking, token theft, and unauthorized access.

Modern Token Standards {#modern-token-standards}

JWT (JSON Web Tokens):

  • Use Case: Stateless authentication for APIs, microservices, SPAs
  • Advantages: Self-contained, no database lookups, scalable
  • Disadvantages: Cannot revoke individual tokens (use short expiration)

Refresh Tokens:

  • Use Case: Long-lived sessions without exposing long-lived JWTs
  • Pattern: Short-lived access tokens (15 minutes) + long-lived refresh tokens (7-14 days)
  • Security: Token rotation detects compromise

Step 4.1: JWT Access Token Implementation (45-60 minutes) {#step-41-jwt-access-token-implementation-45-60-minutes}

Goal: Implement secure JWT access tokens with appropriate expiration and claims.

JWT Configuration:

import jwt from 'jsonwebtoken';
import crypto from 'crypto';

// Generate strong JWT secret (256 bits minimum)
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(32).toString('base64');
const JWT_ALGORITHM = 'HS256';  // Or RS256 for asymmetric keys
const JWT_EXPIRATION = '15m';   // 15 minutes

/**
 * Generate JWT access token
 */
function generateAccessToken(user) {
  const payload = {
    sub: user.id,           // Subject (user ID)
    email: user.email,
    roles: user.roles || [],
    iat: Math.floor(Date.now() / 1000),  // Issued at
    exp: Math.floor(Date.now() / 1000) + (15 * 60)  // Expires in 15 minutes
  };

  return jwt.sign(payload, JWT_SECRET, {
    algorithm: JWT_ALGORITHM
  });
}

/**
 * Verify JWT access token
 */
function verifyAccessToken(token) {
  try {
    const decoded = jwt.verify(token, JWT_SECRET, {
      algorithms: [JWT_ALGORITHM]
    });

    return {
      valid: true,
      payload: decoded
    };
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return { valid: false, error: 'Token expired' };
    } else if (err.name === 'JsonWebTokenError') {
      return { valid: false, error: 'Invalid token' };
    }

    return { valid: false, error: 'Token verification failed' };
  }
}

Tool Integration: JWT Decoder

Use our JWT Decoder to:

  • Decode JWT header, payload, and signature
  • Verify JWT signatures with secret or public key
  • Validate expiration and not-before claims
  • Inspect token issuer, audience, and subject
  • Debug authentication issues in real-time

Step 4.2: Refresh Token Rotation (45-60 minutes) {#step-42-refresh-token-rotation-45-60-minutes}

Goal: Implement refresh token rotation to detect token theft and maintain long-lived sessions securely.

Refresh Token Rotation Pattern:

/**
 * Generate refresh token
 */
async function generateRefreshToken(userId) {
  // 1. Generate cryptographically secure token
  const token = crypto.randomBytes(32).toString('base64url');

  // 2. Hash token for database storage
  const tokenHash = crypto.createHash('sha256').update(token).digest('hex');

  // 3. Generate family ID (for rotation detection)
  const familyId = crypto.randomUUID();

  // 4. Store in database
  await db.query(
    `INSERT INTO refresh_tokens
     (user_id, token_hash, family_id, expires_at, created_at)
     VALUES ($1, $2, $3, NOW() + INTERVAL '7 days', NOW())`,
    [userId, tokenHash, familyId]
  );

  return {
    token: token,
    familyId: familyId
  };
}

/**
 * Rotate refresh token
 */
async function rotateRefreshToken(oldToken) {
  // 1. Hash incoming token
  const tokenHash = crypto.createHash('sha256').update(oldToken).digest('hex');

  // 2. Find token in database
  const result = await db.query(
    'SELECT * FROM refresh_tokens WHERE token_hash = $1',
    [tokenHash]
  );

  if (result.rows.length === 0) {
    throw new Error('Invalid refresh token');
  }

  const storedToken = result.rows[0];

  // 3. Check if already used (token reuse = compromise detected)
  if (storedToken.used) {
    console.error(`Refresh token reuse detected for user ${storedToken.user_id}`);

    // Revoke entire token family
    await db.query(
      'UPDATE refresh_tokens SET revoked = TRUE WHERE family_id = $1',
      [storedToken.family_id]
    );

    throw new Error('Token reuse detected - all tokens revoked');
  }

  // 4. Check expiration
  if (new Date(storedToken.expires_at) < new Date()) {
    throw new Error('Refresh token expired');
  }

  // 5. Mark token as used
  await db.query(
    'UPDATE refresh_tokens SET used = TRUE, used_at = NOW() WHERE id = $1',
    [storedToken.id]
  );

  // 6. Generate new tokens
  const user = await getUser(storedToken.user_id);
  const newAccessToken = generateAccessToken(user);
  const newRefreshToken = await generateRefreshToken(storedToken.user_id);

  return {
    accessToken: newAccessToken,
    refreshToken: newRefreshToken.token,
    expiresIn: 15 * 60  // 15 minutes
  };
}

Database Schema:

CREATE TABLE refresh_tokens (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token_hash VARCHAR(64) UNIQUE NOT NULL,  -- SHA-256 hash
  family_id UUID NOT NULL,                 -- Token family (for rotation detection)
  used BOOLEAN DEFAULT FALSE,
  used_at TIMESTAMPTZ,
  revoked BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  expires_at TIMESTAMPTZ NOT NULL,
  user_agent TEXT,
  ip_address INET
);

CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
CREATE INDEX idx_refresh_tokens_family_id ON refresh_tokens(family_id);
CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at) WHERE used = FALSE AND revoked = FALSE;

Goal: Store authentication tokens in secure, HttpOnly cookies to prevent XSS attacks.

Cookie Configuration:

const COOKIE_CONFIG = {
  httpOnly: true,      // Prevent XSS access
  secure: true,        // HTTPS only
  sameSite: 'strict',  // CSRF protection
  maxAge: 7 * 24 * 60 * 60 * 1000,  // 7 days
  path: '/',
  domain: '.inventivehq.com'  // Allow subdomains
};

/**
 * Set authentication cookies
 */
function setAuthCookies(res, accessToken, refreshToken) {
  // Access token (short-lived)
  res.cookie('access_token', accessToken, {
    ...COOKIE_CONFIG,
    maxAge: 15 * 60 * 1000  // 15 minutes
  });

  // Refresh token (long-lived)
  res.cookie('refresh_token', refreshToken, {
    ...COOKIE_CONFIG,
    maxAge: 7 * 24 * 60 * 60 * 1000  // 7 days
  });
}

/**
 * Clear authentication cookies (logout)
 */
function clearAuthCookies(res) {
  res.clearCookie('access_token', COOKIE_CONFIG);
  res.clearCookie('refresh_token', COOKIE_CONFIG);
}

Token Refresh Endpoint:

app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;

  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token provided' });
  }

  try {
    const tokens = await rotateRefreshToken(refreshToken);

    setAuthCookies(res, tokens.accessToken, tokens.refreshToken);

    return res.json({
      success: true,
      expiresIn: tokens.expiresIn
    });

  } catch (error) {
    console.error('Token refresh failed:', error);
    clearAuthCookies(res);
    return res.status(401).json({ error: error.message });
  }
});

Stage 4 Deliverables:

After completing this stage, you should have:

  • JWT access token generation and verification
  • Refresh token rotation with compromise detection
  • Secure cookie storage configuration
  • Token refresh API endpoint
  • Database schema for refresh tokens

Time Investment: 2-3 hours Next Step: Proceed to Stage 5 for OAuth 2.1 and OIDC integration, or Stage 6 for account recovery implementation.


Stage 5: Brute Force Protection & Monitoring (2-3 hours) {#stage-5-brute-force-protection-monitoring-2-3-hours}

Brute force attacks and credential stuffing campaigns account for 70% of password-related breaches (Verizon DBIR 2024). Modern attacks are distributed across thousands of IPs, making simple IP-based rate limiting ineffective. A layered defense strategy combining multiple countermeasures is essential.

Attack Landscape {#attack-landscape}

Common Attack Patterns:

  • Credential stuffing: Testing leaked username/password pairs from other breaches
  • Password spraying: Trying common passwords against many accounts (1 attempt per account)
  • Distributed attacks: Spreading attempts across 2.8 million unique IPs (2025 campaign data)
  • Slow and low attacks: Staying below rate limit thresholds
  • Account enumeration: Testing if email addresses exist in system

Limitations of Rate Limiting Alone:

  • Distributed attacks bypass IP-based limits
  • Password spray attacks stay below per-account thresholds
  • Proxy chains and VPNs rotate IPs continuously
  • Legitimate users can be blocked by overly restrictive limits

Step 5.1: Adaptive Rate Limiting (45-60 minutes) {#step-51-adaptive-rate-limiting-45-60-minutes}

Goal: Implement multi-layered rate limiting at account, IP, and global levels.

Rate Limiting Implementation (Express + Redis):

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'redis';

const redisClient = Redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT
});

/**
 * Per-IP rate limiter
 */
const ipRateLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:ip:'
  }),
  windowMs: 60 * 60 * 1000,  // 1 hour window
  max: 20,                    // 20 attempts per hour per IP
  standardHeaders: true,
  legacyHeaders: false,
  message: 'Too many login attempts from this IP. Please try again later.',
  skipSuccessfulRequests: true  // Only count failed attempts
});

/**
 * Per-account rate limiter
 */
async function checkAccountRateLimit(email) {
  const key = `rl:account:${email}`;
  const windowMs = 15 * 60 * 1000;  // 15 minutes
  const maxAttempts = 5;

  const attempts = await redisClient.incr(key);

  if (attempts === 1) {
    await redisClient.expire(key, Math.floor(windowMs / 1000));
  }

  if (attempts > maxAttempts) {
    const ttl = await redisClient.ttl(key);
    throw new Error(`Account temporarily locked. Try again in ${Math.ceil(ttl / 60)} minutes.`);
  }

  return { remaining: maxAttempts - attempts };
}

/**
 * Global spike detector
 */
async function checkGlobalFailedLogins() {
  const key = 'rl:global:failed_logins';
  const windowMs = 5 * 60 * 1000;  // 5 minute window
  const threshold = 100;           // 100 failed logins across all accounts

  const count = await redisClient.incr(key);

  if (count === 1) {
    await redisClient.expire(key, Math.floor(windowMs / 1000));
  }

  if (count > threshold) {
    console.error('Global brute force attack detected - failed login spike');
    // Trigger alerting, enable CAPTCHA site-wide, etc.
    return { alert: true, count: count };
  }

  return { alert: false, count: count };
}

Login Endpoint with Rate Limiting:

app.post('/api/auth/login', ipRateLimiter, async (req, res) => {
  const { email, password } = req.body;

  try {
    // 1. Check account-specific rate limit
    await checkAccountRateLimit(email);

    // 2. Check global attack detection
    const globalCheck = await checkGlobalFailedLogins();

    // 3. Retrieve user
    const user = await db.query('SELECT * FROM users WHERE email = $1', [email]);

    if (user.rows.length === 0) {
      // Don't reveal if account exists (prevent enumeration)
      await sleep(Math.random() * 200 + 100);  // Timing attack prevention
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // 4. Verify password
    const isValid = await verifyPasswordWithMigration(password, user.rows[0].password_hash, user.rows[0].id);

    if (!isValid) {
      // Record failed attempt
      await recordFailedLogin(user.rows[0].id, req.ip);

      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // 5. Check if MFA required
    if (user.rows[0].mfa_enabled) {
      // Return MFA challenge (implementation in Stage 3)
      return res.json({ mfaRequired: true, userId: user.rows[0].id });
    }

    // 6. Successful login
    await recordSuccessfulLogin(user.rows[0].id, req.ip);

    const accessToken = generateAccessToken(user.rows[0]);
    const refreshToken = await generateRefreshToken(user.rows[0].id);

    setAuthCookies(res, accessToken, refreshToken.token);

    return res.json({ success: true });

  } catch (error) {
    console.error('Login error:', error);
    return res.status(429).json({ error: error.message });
  }
});

Step 5.2: Bot Detection & CAPTCHA Integration (30-45 minutes) {#step-52-bot-detection-captcha-integration-30-45-minutes}

Goal: Distinguish between legitimate users and automated bots.

CAPTCHA Trigger Logic:

/**
 * Determine if CAPTCHA challenge required
 */
async function requiresCAPTCHA(email, ip) {
  // 1. Check recent failed login count
  const failedAttempts = await db.query(
    `SELECT COUNT(*) FROM login_attempts
     WHERE email = $1 AND success = FALSE AND created_at > NOW() - INTERVAL '1 hour'`,
    [email]
  );

  if (parseInt(failedAttempts.rows[0].count) >= 3) {
    return true;  // Require CAPTCHA after 3 failed attempts
  }

  // 2. Check IP reputation
  const ipReputation = await checkIPReputation(ip);
  if (ipReputation.score < 50) {
    return true;  // Suspicious IP
  }

  // 3. Check for bot-like behavior
  const requestPattern = await analyzeRequestPattern(ip);
  if (requestPattern.isBot) {
    return true;  // Bot detected
  }

  return false;
}

Tool Integration: IP Risk Checker

Use our IP Risk Checker to:

  • Query IP geolocation and ISP information
  • Check IP blocklist status (Spamhaus, SURBL, Project Honey Pot)
  • Detect VPN/proxy usage
  • Review historical abuse reports
  • Calculate fraud risk scores

Step 5.3: Security Monitoring & Alerting (45-60 minutes) {#step-53-security-monitoring-alerting-45-60-minutes}

Goal: Detect and respond to authentication attacks in real-time.

Login Attempt Tracking:

CREATE TABLE login_attempts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id) ON DELETE SET NULL,
  email VARCHAR(255) NOT NULL,
  ip_address INET NOT NULL,
  user_agent TEXT,
  success BOOLEAN NOT NULL,
  failure_reason VARCHAR(100),
  mfa_method VARCHAR(50),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_login_attempts_email ON login_attempts(email);
CREATE INDEX idx_login_attempts_ip ON login_attempts(ip_address);
CREATE INDEX idx_login_attempts_created_at ON login_attempts(created_at);
CREATE INDEX idx_login_attempts_failed ON login_attempts(email, success) WHERE success = FALSE;

Alert Triggers:

/**
 * Monitor for suspicious authentication patterns
 */
async function monitorAuthenticationPatterns() {
  // 1. Detect account enumeration
  const enumerationDetection = await db.query(`
    SELECT ip_address, COUNT(DISTINCT email) as unique_emails
    FROM login_attempts
    WHERE created_at > NOW() - INTERVAL '5 minutes'
      AND success = FALSE
    GROUP BY ip_address
    HAVING COUNT(DISTINCT email) > 20
  `);

  if (enumerationDetection.rows.length > 0) {
    alertSecurityTeam('Account enumeration detected', enumerationDetection.rows);
  }

  // 2. Detect credential stuffing
  const credentialStuffing = await db.query(`
    SELECT COUNT(*) as failed_logins
    FROM login_attempts
    WHERE created_at > NOW() - INTERVAL '5 minutes'
      AND success = FALSE
  `);

  if (parseInt(credentialStuffing.rows[0].failed_logins) > 100) {
    alertSecurityTeam('Credential stuffing attack detected', {
      failedLogins: credentialStuffing.rows[0].failed_logins
    });
  }

  // 3. Detect successful login from new location
  const newLocationLogins = await db.query(`
    SELECT u.id, u.email, la.ip_address, la.created_at
    FROM login_attempts la
    JOIN users u ON la.user_id = u.id
    WHERE la.success = TRUE
      AND la.created_at > NOW() - INTERVAL '1 hour'
      AND NOT EXISTS (
        SELECT 1 FROM login_attempts la2
        WHERE la2.user_id = u.id
          AND la2.ip_address = la.ip_address
          AND la2.created_at < la.created_at - INTERVAL '7 days'
      )
  `);

  for (const login of newLocationLogins.rows) {
    alertUser(login.email, 'New login location detected', {
      ip: login.ip_address,
      timestamp: login.created_at
    });
  }
}

// Run every 5 minutes
setInterval(monitorAuthenticationPatterns, 5 * 60 * 1000);

Stage 5 Deliverables:

After completing this stage, you should have:

  • Multi-layered rate limiting (per-IP, per-account, global)
  • Bot detection and CAPTCHA integration
  • Login attempt tracking and analytics
  • Real-time security monitoring and alerting
  • Automated response to authentication attacks

Time Investment: 2-3 hours


Conclusion {#conclusion}

Implementing secure authentication is not a one-time project—it's an ongoing commitment to protecting your users and your organization. This workflow has guided you through seven comprehensive stages covering password policies aligned with NIST SP 800-63B, modern password hashing with Argon2id, phishing-resistant multi-factor authentication, secure session management, and layered brute force protection.

Key Takeaways {#key-takeaways}

  1. Length over complexity - NIST guidelines prioritize password length over arbitrary composition rules
  2. Breach detection is essential - Block passwords found in data breaches using HIBP API
  3. MFA prevents 99.9% of attacks - Implement phishing-resistant WebAuthn/FIDO2 for high-security applications
  4. Argon2id is the gold standard - Memory-hard hashing algorithms resist GPU and ASIC attacks
  5. Layered defense - Combine rate limiting, bot detection, and behavioral analysis
  6. Monitor and alert - Real-time detection enables rapid response to attacks
  7. User experience matters - Security controls should enhance, not hinder, usability

Next Steps {#next-steps}

Immediate Actions:

  1. Audit your current password policy against NIST SP 800-63B guidelines
  2. Implement compromised password detection (Stage 1)
  3. Migrate to Argon2id password hashing (Stage 2)
  4. Enable multi-factor authentication for privileged accounts (Stage 3)
  5. Implement rate limiting and monitoring (Stage 5)

Long-Term Improvements:

  1. Deploy WebAuthn/FIDO2 for phishing-resistant authentication
  2. Implement refresh token rotation for enhanced session security
  3. Build security dashboards for real-time authentication monitoring
  4. Conduct quarterly security audits and penetration tests
  5. Train users on password managers and passphrase best practices

InventiveHQ Tools:

Standards & Documentation:

  • NIST SP 800-63B: Digital Identity Guidelines (Authentication and Lifecycle Management)
  • OWASP Authentication Cheat Sheet
  • FIDO Alliance WebAuthn Specifications
  • RFC 6238: TOTP Time-Based One-Time Password Algorithm

Further Reading:

Authentication security is the foundation upon which all other security controls rest. By following this workflow and continuously improving your authentication systems, you'll significantly reduce your organization's attack surface and protect your users from the most common attack vectors.

Questions or need implementation assistance? Our security engineering team at InventiveHQ specializes in authentication architecture, security audits, and compliance implementation. Contact us for a free consultation.

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.