2023-03-10 18:00:00+00:00

One-Time Passwords (OTPs) sent via SMS or Email are crucial for securing critical workflows like registration, password resets, and high-value payments. However, OTP services are highly targeted by bad actors for SMS toll fraud and brute-force attempts. A secure OTP service requires strict rate limits, automated provider failovers, and low-latency storage.

Backing the service with Redis allows us to implement fast, atomic transaction checks while setting precise TTLs (Time-To-Live) on OTP codes.


1. Implementing Atomic Lockout in Redis

To prevent SMS spamming, we store two distinct keys in Redis for each mobile number: a cooling-down lock key (e.g., 60 seconds) and the actual OTP payload key (e.g., 5 minutes). We use Redis pipelines to check and set these values atomically:

func (s *OTPService) GenerateOTP(ctx context.Context, phone string) (string, error) {
    lockKey := fmt.Sprintf("otp:lock:%s", phone)
    otpKey := fmt.Sprintf("otp:code:%s", phone)
    
    // Check if lockout is active
    exists, _ := s.redis.Exists(ctx, lockKey).Result()
    if exists > 0 {
        return "", ErrLockoutActive
    }
    
    code := generateSixDigitCode()
    pipe := s.redis.TxPipeline()
    pipe.Set(ctx, lockKey, "1", 60*time.Second)
    pipe.Set(ctx, otpKey, code, 5*time.Minute)
    _, err := pipe.Exec(ctx)
    
    return code, err
}

2. Multi-Provider Failover Strategy

If an SMS gateway (e.g., Twilio or Infobip) experiences an outage, the service should automatically route messages to a secondary provider. We implement a Circuit Breaker pattern that tracks the success rate of outbound SMS calls and redirects traffic when failures exceed a set threshold.

Using Go's channels and goroutines, we can dispatch SMS requests asynchronously without blocking the user registration API response, keeping user latency minimal.