How we protected our OTP endpoint?

How we protected our OTP endpoint?

ยท

2 min read

In the realm of cybersecurity, the choice of the size of your OTP is not to be taken lightly. When your backend system demands a 4-digit OTP, it essentially means there are 10,000 unique combinations possible (10**10**10*10).

The significance of this lies in the fact that malicious actors attempting to brute force their way into your API can do so with relative ease. A mere 10,000 combinations can be systematically tested, potentially granting unauthorized access to user accounts.

To fortify the security of phone number verification, it is crucial to implement rate limiting for OTP verification. This safeguards against repeated attempts, adding an additional layer of defense against fraudulent activities and unauthorized account access.

We've decided to enhance our API's security with Redis-based rate limiting. After five consecutive unsuccessful attempts to verify an OTP, we will temporarily block further requests for the next two minutes. Additionally, to maintain security, remember to expire the existing OTP and issue a new one to the user.

Rpush - inserts the key if it does not exist

Rpushx - Inserts only when key exists

Redis pipeline is to execute multiple redis command in a single transaction. I have used it here so that key and expiry set is atomic. There will be some race condition here when multiple go-routines will read false and try execute the multi-command, but that's okay.

const MAX_REQUESTS = 5
const OTP_REQUEST_WINDOW = 2 * time.Minute

func rateLimitOTP(phoneNumber string, otp int) error {
    ctx := context.Background()
    current, _ := redisClient.LLen(ctx, phoneNumber).Result()

    if current > MAX_REQUESTS {
        return fmt.Errorf("too many requests, try after some time")
    }

    exists, _ := redisClient.Exists(ctx, phoneNumber).Result()

    if !exists {
        pipe := redisClient.TxPipeline()
        pipe.RPush(ctx, phoneNumber, 1)
        pipe.Expire(ctx, phoneNumber, OTP_REQUEST_WINDOW)

        _, err := pipe.Exec(ctx)
        if err != nil {
            return fmt.Errorf("internal server error")
        }
        return nil
    }

    _, err := redisClient.RPushX(ctx, phoneNumber, 1).Result()
    if err != nil {
        return err
    }

    return nil
}

Happy Coding (::)

Reference: https://redis.io/commands/incr/

Did you find this article valuable?

Support Dhairya Verma by becoming a sponsor. Any amount is appreciated!