Secure OTP Lifecycle
A secure OTP flow has five distinct phases, each requiring specific protections:
- Request: User initiates OTP via login, registration, or transaction
- Generation: Cryptographically secure random number created server-side
- Delivery: SMS sent via direct carrier route with minimal latency
- Verification: User submits code; server validates against stored hash
- 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:
- Attempt Counter: Track consecutive failed verifications per OTP
- Max Attempts: Lock after 3-5 failed attempts maximum
- Progressive Delay: Add exponential backoff between allowed retries (1s → 5s → 30s → 5min)
- Account Lockout: After max attempts, lock account for 15-30 minutes and notify user via email
- 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:
- Voice Call OTP: Automated voice call as backup after 2 failed SMS deliveries
- Email OTP: Secondary channel for users with verified email addresses
- Authenticator App: TOTP (Time-based One-Time Password) as permanent alternative to SMS
- Push Notification: In-app approval for users with installed mobile applications
- 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