OTP Security Implementation

Build tamper-proof authentication flows that protect users against SIM swap, interception, and brute-force attacks.

Secure OTP Lifecycle

A secure OTP flow has five distinct phases, each requiring specific protections:

  1. Request: User initiates OTP via login, registration, or transaction
  2. Generation: Cryptographically secure random number created server-side
  3. Delivery: SMS sent via direct carrier route with minimal latency
  4. Verification: User submits code; server validates against stored hash
  5. Completion: Successful verification triggers session/token issuance; failed attempts increment counters

Critical Rule

Never generate OTPs client-side or transmit them via URL parameters. Always generate server-side using CSPRNG and store hashed values only.

Generation Best Practices

Practice Recommended Avoid
Length 6 digits minimum 4-digit codes (10,000 combinations)
Character Set Numeric only for SMS Alphanumeric (causes Unicode encoding issues)
Randomness Source CSPRNG (crypto.randomBytes, secrets.token_hex) Math.random(), time-based seeds
Storage Bcrypt/Argon2 hashed in database Plaintext storage or reversible encryption
Uniqueness One active OTP per user at a time Multiple concurrent OTPs for same action
// Secure OTP generation example (Node.js) const crypto = require('crypto'); function generateOTP(length = 6) { const min = Math.pow(10, length - 1); const max = Math.pow(10, length) - 1; // Use cryptographically secure random number const buffer = crypto.randomBytes(4); const num = buffer.readUInt32BE(0); return (min + (num % (max - min))).toString(); } // Hash before storing - NEVER store plaintext const bcrypt = require('bcrypt'); async function storeOTP(userId, otp) { const hash = await bcrypt.hash(otp, 12); await db.query( 'INSERT INTO otp_codes (user_id, hash, expires_at) VALUES (?, ?, NOW() + INTERVAL 5 MINUTE)', [userId, hash] ); }

Expiry Strategy

Balance security with usability through intelligent expiry:

  • Standard Expiry: 5 minutes for most authentication flows
  • High-Risk Transactions: 2-3 minutes for M-Pesa transfers, password resets
  • Low-Risk Actions: 10 minutes for newsletter confirmations, non-sensitive verifications
  • Auto-Invalidation: Generating new OTP immediately invalidates previous unverified codes
  • Server-Side Enforcement: Never trust client-side timers; always validate timestamp server-side

Kenya Context

Carrier network congestion can add 30-90 seconds delay during peak hours. Set minimum expiry at 3 minutes to accommodate delivery latency without compromising security.

Rate Limiting & Throttling

Prevent abuse through layered rate limits:

Limit Type Threshold Action
Per Phone Number 3 requests per 10 minutes Block further requests; show cooldown timer
Per IP Address 10 requests per hour Temporary IP ban; log for review
Per User Account 5 requests per 24 hours Require additional verification (email, security questions)
Global System Monitor anomaly spikes Trigger alert; enable CAPTCHA on request form

Brute-Force Protection

Defend against automated guessing attacks:

  1. Attempt Counter: Track consecutive failed verifications per OTP
  2. Max Attempts: Lock after 3-5 failed attempts maximum
  3. Progressive Delay: Add exponential backoff between allowed retries (1s → 5s → 30s → 5min)
  4. Account Lockout: After max attempts, lock account for 15-30 minutes and notify user via email
  5. Constant-Time Comparison: Use timing-safe comparison functions to prevent side-channel attacks
// Timing-safe verification example const crypto = require('crypto'); async function verifyOTP(userId, submittedOtp) { const record = await db.query( 'SELECT hash, attempts, expires_at FROM otp_codes WHERE user_id = ? ORDER BY created_at DESC LIMIT 1', [userId] ); if (!record || Date.now() > record.expires_at) { return { success: false, reason: 'expired_or_invalid' }; } if (record.attempts >= 5) { return { success: false, reason: 'max_attempts_exceeded' }; } // Constant-time comparison prevents timing attacks const isValid = crypto.timingSafeEqual( Buffer.from(await bcrypt.hash(submittedOtp, 12)), Buffer.from(record.hash) ); if (!isValid) { await db.query('UPDATE otp_codes SET attempts = attempts + 1 WHERE user_id = ?', [userId]); return { success: false, reason: 'invalid_code', remainingAttempts: 5 - record.attempts - 1 }; } // Success - invalidate OTP await db.query('DELETE FROM otp_codes WHERE user_id = ?', [userId]); return { success: true }; }

SIM Swap Detection

SIM swap fraud is prevalent in Kenya. Implement these safeguards:

  • Porting Date Check: Query carrier APIs for recent SIM porting events (within last 7 days)
  • Device Fingerprinting: Compare current device ID with historical login devices
  • Behavioral Analysis: Flag unusual location, time-of-day, or transaction amount patterns
  • Cooling Period: Enforce 24-48 hour waiting period after detected SIM change before allowing sensitive actions
  • Multi-Factor Escalation: Require additional verification (voice call, email, security questions) when risk score is elevated

M-Pesa Integration Note

If integrating with M-Pesa STK Push or B2C, always verify the registered phone number matches the SIM card currently receiving the OTP. Mismatched numbers indicate potential SIM swap.

Secure Message Content

Craft OTP messages that resist social engineering:

  • Never Include Full OTP in Preview: Some carriers display message previews on lock screens. Consider partial masking if supported
  • Include Brand Name: "[MACRA] Your verification code is 123456. Valid for 5 minutes." builds trust
  • Add Warning Text: "Do not share this code with anyone. Macra staff will never ask for it."
  • Avoid Sensitive Context: Don't mention transaction amounts, account balances, or personal details in OTP SMS
  • GSM 7-bit Only: Use numeric codes and basic ASCII to avoid Unicode encoding issues and ensure fastest delivery

Fallback Mechanisms

Plan for SMS delivery failures without compromising security:

  1. Voice Call OTP: Automated voice call as backup after 2 failed SMS deliveries
  2. Email OTP: Secondary channel for users with verified email addresses
  3. Authenticator App: TOTP (Time-based One-Time Password) as permanent alternative to SMS
  4. Push Notification: In-app approval for users with installed mobile applications
  5. Graceful Degradation: Clearly communicate fallback option availability without revealing which channel succeeded

Need Security Audit?

Our security team can review your OTP implementation and recommend enhancements based on latest threat intelligence.

Request Security Review