Skip to main content

Overview

The validateOTP() function validates one-time passwords (OTP) with built-in attempt tracking and security features. It compares user input against the correct OTP code and enforces maximum attempt limits to prevent brute force attacks.

Common Use Cases

  • Two-factor authentication (2FA)
  • Email verification codes
  • SMS verification codes
  • Password reset flows
  • Account recovery
  • Login verification

Function Signature

validateOTP(otp, correctOTP, options)

Parameters

otp
string
required
The OTP code entered by the user.
correctOTP
string
required
The correct OTP code to validate against.
options
object
Configuration options for OTP validation.
options.attempts
number
default:"0"
The current number of failed attempts. Used to track and limit validation attempts.
options.maxAttempts
number
default:"3"
Maximum number of attempts allowed before blocking further validation.
options.details
boolean
default:"false"
Whether to return detailed validation information instead of a boolean.

Return Value

result
boolean | object
When details is false: Returns true if OTP is valid, false otherwise.When details is true: Returns an object with the following properties:
valid
boolean
Whether the OTP is valid.
errors
string[] | null
Array of validation error messages, or null if valid.
otp
string
The OTP code that was provided.
correctOTP
string
The correct OTP code (for debugging - be careful with logging).
attempts
number
Current number of attempts.
remainingAttempts
number
Number of attempts remaining before lockout.
maxAttempts
number
Maximum attempts allowed.

Examples

Basic OTP Validation

import { validateOTP } from 'validauth';

// Correct OTP
const isValid = validateOTP('123456', '123456');
// Returns: true

// Incorrect OTP
const isInvalid = validateOTP('123456', '654321');
// Returns: false

Two-Factor Authentication Example

import { validateOTP } from 'validauth';

class TwoFactorAuth {
  constructor() {
    this.sessions = new Map();
  }

  // Generate and send OTP (simplified)
  async sendOTP(userId, method = 'email') {
    // Generate 6-digit OTP
    const otp = Math.floor(100000 + Math.random() * 900000).toString();
    
    // Store OTP with session data
    this.sessions.set(userId, {
      otp: otp,
      attempts: 0,
      expiresAt: Date.now() + 10 * 60 * 1000, // 10 minutes
      method: method
    });

    // Send OTP via email/SMS
    // await sendEmail(userId, otp);
    
    return {
      success: true,
      message: `OTP sent via ${method}`
    };
  }

  // Validate OTP with attempt tracking
  verifyOTP(userId, userOTP) {
    const session = this.sessions.get(userId);

    if (!session) {
      return {
        success: false,
        message: 'No OTP session found. Please request a new code.'
      };
    }

    // Check expiration
    if (Date.now() > session.expiresAt) {
      this.sessions.delete(userId);
      return {
        success: false,
        message: 'OTP has expired. Please request a new code.'
      };
    }

    // Validate OTP with attempt tracking
    const result = validateOTP(userOTP, session.otp, {
      attempts: session.attempts,
      maxAttempts: 3,
      details: true
    });

    // Increment attempts
    session.attempts++;

    if (!result.valid) {
      if (result.errors.includes('Max attempts exceeded.')) {
        this.sessions.delete(userId);
        return {
          success: false,
          message: 'Maximum attempts exceeded. Please request a new code.',
          remainingAttempts: 0
        };
      }

      return {
        success: false,
        message: 'Invalid OTP code.',
        remainingAttempts: result.remainingAttempts
      };
    }

    // Success - clean up session
    this.sessions.delete(userId);
    return {
      success: true,
      message: 'OTP verified successfully'
    };
  }
}

// Usage
const twoFA = new TwoFactorAuth();

// Send OTP
await twoFA.sendOTP('user123', 'email');

// User enters OTP
const result = twoFA.verifyOTP('user123', '123456');
if (result.success) {
  console.log('2FA successful');
} else {
  console.error(result.message);
  console.log('Remaining attempts:', result.remainingAttempts);
}

Email Verification Flow

import { validateOTP } from 'validauth';

class EmailVerification {
  constructor(database) {
    this.db = database;
  }

  // Generate and send verification code
  async sendVerificationCode(email) {
    // Generate 6-digit code
    const code = Math.floor(100000 + Math.random() * 900000).toString();
    
    // Store in database
    await this.db.verificationCodes.create({
      email: email,
      code: code,
      attempts: 0,
      expiresAt: new Date(Date.now() + 30 * 60 * 1000) // 30 minutes
    });

    // Send email
    await this.sendEmail(email, code);
    
    return { success: true };
  }

  // Verify the code
  async verifyCode(email, userCode) {
    // Get verification record
    const record = await this.db.verificationCodes.findOne({ email });

    if (!record) {
      return {
        verified: false,
        message: 'No verification code found for this email'
      };
    }

    // Check expiration
    if (new Date() > record.expiresAt) {
      await this.db.verificationCodes.delete({ email });
      return {
        verified: false,
        message: 'Verification code has expired'
      };
    }

    // Validate OTP
    const result = validateOTP(userCode, record.code, {
      attempts: record.attempts,
      maxAttempts: 5,
      details: true
    });

    // Update attempts
    await this.db.verificationCodes.update(
      { email },
      { attempts: record.attempts + 1 }
    );

    if (!result.valid) {
      if (result.remainingAttempts === 0) {
        await this.db.verificationCodes.delete({ email });
        return {
          verified: false,
          message: 'Too many failed attempts. Please request a new code.'
        };
      }

      return {
        verified: false,
        message: `Invalid code. ${result.remainingAttempts} attempts remaining.`
      };
    }

    // Success - mark email as verified
    await this.db.users.update(
      { email },
      { emailVerified: true }
    );
    await this.db.verificationCodes.delete({ email });

    return {
      verified: true,
      message: 'Email verified successfully'
    };
  }

  async sendEmail(email, code) {
    // Email sending implementation
    console.log(`Sending code ${code} to ${email}`);
  }
}

// Usage
const verification = new EmailVerification(database);

// Send code
await verification.sendVerificationCode('[email protected]');

// Verify code
const result = await verification.verifyCode('[email protected]', '123456');
console.log(result.message);

Password Reset Flow

import { validateOTP } from 'validauth';
import { isPassword } from 'validauth';

class PasswordReset {
  constructor() {
    this.resetCodes = new Map();
  }

  // Step 1: Request password reset
  async requestReset(email) {
    // Generate 6-digit reset code
    const code = Math.floor(100000 + Math.random() * 900000).toString();
    
    this.resetCodes.set(email, {
      code: code,
      attempts: 0,
      expiresAt: Date.now() + 15 * 60 * 1000, // 15 minutes
      verified: false
    });

    // Send reset code via email
    // await sendEmail(email, code);
    
    return {
      success: true,
      message: 'Reset code sent to your email'
    };
  }

  // Step 2: Verify reset code
  verifyResetCode(email, userCode) {
    const resetData = this.resetCodes.get(email);

    if (!resetData) {
      return {
        verified: false,
        message: 'No reset request found'
      };
    }

    if (Date.now() > resetData.expiresAt) {
      this.resetCodes.delete(email);
      return {
        verified: false,
        message: 'Reset code has expired'
      };
    }

    const result = validateOTP(userCode, resetData.code, {
      attempts: resetData.attempts,
      maxAttempts: 3,
      details: true
    });

    resetData.attempts++;

    if (!result.valid) {
      if (result.remainingAttempts === 0) {
        this.resetCodes.delete(email);
      }
      return {
        verified: false,
        message: 'Invalid reset code',
        remainingAttempts: result.remainingAttempts
      };
    }

    // Mark as verified
    resetData.verified = true;
    return {
      verified: true,
      message: 'Code verified. You can now reset your password.'
    };
  }

  // Step 3: Reset password
  resetPassword(email, newPassword) {
    const resetData = this.resetCodes.get(email);

    if (!resetData || !resetData.verified) {
      return {
        success: false,
        message: 'Invalid or unverified reset request'
      };
    }

    // Validate new password
    const passwordValid = isPassword(newPassword, {
      minLength: 8,
      details: true
    });

    if (!passwordValid.valid) {
      return {
        success: false,
        message: 'Password does not meet requirements',
        errors: passwordValid.errors
      };
    }

    // Update password in database
    // await db.users.updatePassword(email, newPassword);

    // Clean up
    this.resetCodes.delete(email);

    return {
      success: true,
      message: 'Password reset successfully'
    };
  }
}

// Usage
const resetService = new PasswordReset();

// Step 1: Request reset
await resetService.requestReset('[email protected]');

// Step 2: Verify code
const verification = resetService.verifyResetCode('[email protected]', '123456');
if (verification.verified) {
  // Step 3: Reset password
  const reset = resetService.resetPassword('[email protected]', 'NewSecureP@ss123');
  console.log(reset.message);
}

Security Best Practices

Critical Security Considerations
  • Never log or expose the correctOTP value in production
  • Always set an expiration time for OTP codes (5-30 minutes typical)
  • Implement rate limiting at the API level to prevent brute force
  • Use secure random number generation for OTP codes
  • Clear OTP data after successful validation
  • Consider using time-based OTP (TOTP) for enhanced security

Generating Secure OTP Codes

import crypto from 'crypto';

function generateSecureOTP(length = 6) {
  // Use cryptographically secure random number generation
  const buffer = crypto.randomBytes(length);
  const otp = Array.from(buffer)
    .map(byte => byte % 10)
    .join('');
  
  return otp.padStart(length, '0');
}

// Generate 6-digit OTP
const otp = generateSecureOTP(6);
console.log(otp); // e.g., "472916"

Rate Limiting Example

import { validateOTP } from 'validauth';

class RateLimitedOTP {
  constructor() {
    this.attempts = new Map();
  }

  isRateLimited(identifier, windowMs = 60000, maxRequests = 5) {
    const now = Date.now();
    const userAttempts = this.attempts.get(identifier) || [];
    
    // Remove old attempts outside the time window
    const recentAttempts = userAttempts.filter(
      timestamp => now - timestamp < windowMs
    );
    
    if (recentAttempts.length >= maxRequests) {
      return true;
    }
    
    // Record this attempt
    recentAttempts.push(now);
    this.attempts.set(identifier, recentAttempts);
    
    return false;
  }

  validateWithRateLimit(identifier, otp, correctOTP, options = {}) {
    // Check rate limit (5 attempts per minute)
    if (this.isRateLimited(identifier)) {
      return {
        valid: false,
        message: 'Too many attempts. Please try again later.',
        rateLimited: true
      };
    }

    // Validate OTP
    const result = validateOTP(otp, correctOTP, {
      ...options,
      details: true
    });

    return {
      valid: result.valid,
      message: result.valid ? 'OTP valid' : 'Invalid OTP',
      remainingAttempts: result.remainingAttempts,
      rateLimited: false
    };
  }
}

Validation Rules

OTP Validation Requirements
  • OTP and correctOTP must match exactly (case-sensitive)
  • Attempts are tracked independently (you manage the counter)
  • Default maximum attempts: 3
  • Validation fails if attempts exceed maxAttempts
  • No automatic expiration (implement separately)

Error Messages

The validator returns the following error messages:
  • "Invalid OTP." - The OTP does not match the correct code
  • "Max attempts exceeded." - The number of attempts has exceeded the limit
  • "Max attempts must be greater than 0." - Invalid maxAttempts configuration

Best Practices

Recommended Implementation
  • Set OTP expiration time (10-30 minutes)
  • Use 6-digit codes for user convenience
  • Limit to 3-5 validation attempts
  • Implement rate limiting at API level
  • Clear OTP data after successful validation
  • Send OTP via secure channels (email/SMS)
  • Allow users to request new codes
  • Log failed attempts for security monitoring

Testing OTP Flows

import { validateOTP } from 'validauth';

// In development/testing, you might want to use fixed codes
const isDevelopment = process.env.NODE_ENV === 'development';

function getTestOTP() {
  return isDevelopment ? '123456' : generateSecureOTP();
}

// For testing, always accept a specific test code
function validateOTPWithTestMode(userOTP, correctOTP, options = {}) {
  if (isDevelopment && userOTP === '000000') {
    return true; // Test code always works in dev
  }
  
  return validateOTP(userOTP, correctOTP, options);
}

Build docs developers (and LLMs) love