How to Generate and Verify Time-Constraint OTPs Fast and Securely with Redis

Kelechi Onyekwere
8 min readJul 16, 2023

TABLE OF CONTENT

Introduction

Since the majority of our day-to-day operations rely on rapidly advancing technology and increasing dependence on digital platforms, numerous methods have been adopted and invented to ensure the authenticity and security of data and information.

These methods typically involve techniques such as username-password combinations, firewalls, encryption, and access controls. While these measures do provide a level of protection, they are not impervious to security breaches. Hackers and cybercriminals have developed sophisticated methods to bypass these traditional defenses, making it increasingly necessary to explore more robust security solutions. One effective method to combat unauthorized access and protect sensitive information is through the use of One-Time Passwords (OTPs). Generating OTPs provides an additional layer of security, ensuring that only authorized individuals can complete transactions or take specific actions.

What are OTPs and why are they important?

One-Time Passwords (OTPs) are dynamic and unique combinations of numeric or alphanumeric characters that are generated automatically. They serve as a powerful tool in user authentication for individual transactions or login sessions. In comparison to static passwords, particularly those created by users, OTPs offer significantly enhanced security since they serve as an additional layer. User-created passwords often suffer from weaknesses, such as being easily guessable or reused across multiple accounts, which can jeopardize data protection. In contrast, OTPs provide a higher level of security by ensuring each password is generated only for a specific instance, mitigating the risk of unauthorized access and reinforcing the integrity of digital interactions.

In this article, you are going to be learning how to generate and verify OTPs in a secure and efficient manner.

Prerequisite

  • Go
  • Redis

Visit Go and Redis for instructions on how to set them up.

We would be breaking this tutorial into two parts:

  • Generating and storing the OTP against a user identifier
  • Verifying the OTP against the user identifier

We are going to use the Redis DB to store our generated OTPs. Redis is an open-source, in-memory data structure store that can be used as a database, cache, and message broker. It provides high-performance, low-latency data access and supports a variety of data types such as strings, hashes, lists, sets, and more. With its simplicity and versatility, Redis is widely adopted in modern applications for improving scalability and responsiveness. Since our OTPs are going to be constrained by time which means they can get expired, we would leverage Redis and the Redis TTL ( Time to Live ) option to temporarily store the OTPs in a cache.

Generating and Storing the OTP

For this part, you will be setting up Redis, functions to connect to the Redis instance, and also wrapper functions for the Redis Set and Get commands. To have Redis set up, you can either connect via a docker container, learn about this setup here, or a local instance of Redis already installed, or you can choose to remotely connect to the cloud instance with Redis Lab.

For the sake of this tutorial, we would be using the remote Redis Db from Redis Lab, which gives us access to a Redis database with limited storage space for free. Navigate to Redis and signup to get a free Redis Db.

After signing up, you should be able to create a Redis database and your connection string should be available for you to use, the connection string should look like this “redis://<password>@redis-10063.c246.us-east-1–4.ec2.cloud.redislabs.com:10063”, and you can find it in the “connect” section.

For this tutorial, we will use a Go Project and also use the Redigo Library, which is a Go client library for Redis, designed to provide a simple and efficient way to interact with Redis servers. It offers a clean and intuitive API for executing Redis commands, managing connections, and handling responses, making it a popular choice for building Redis-powered applications in the Go programming language.

Provided you have created and set up your Go project, we can then proceed to write the helper and wrapper functions.

You should have a helper method or function that generates a 6-digit code, this is going to be the OTP generated.

func GenerateCode(digit int) string {
b := make([]byte, digit)
n, err := io.ReadAtLeast(rand.Reader, b, digit)
if n != digit {
panic(err)
}
for i := 0; i < len(b); i++ {
b[i] = table[int(b[i])%len(table)]
}
return string(b)
}

Connect to Redis

Connecting to the Redis DB makes it possible for us to store and retrieve data from the database as well as perform other operations.

func RedisPool() *redis.Pool {
return &redis.Pool{
MaxIdle: 50,
MaxActive: 10000,

Dial: func() (redis.Conn, error) {
c, err := redis.DialURL(os.Getenv("REDIS_HOST"), redis.DialTLSSkipVerify(true))

// Connection error handling
if err != nil {
fmt.Printf("ERROR: fail initializing the redis pool: %s", err.Error())
os.Exit(1)
}
return c, err
},
}
}

Create two additional functions to wrap the Redis Set and Get commands. The Set function basically stores a value against a key with a TTL option which acts as the expiry time for the key and value to exist, this means once the time elapses, the key is deleted, on the other hand, the Get function retrieves a value based on the key provided or returns an empty string if it does not exist.

Set

func SetValue(key string, value string, expiry int) error {
conn := RedisPool().Get()
reply, err := conn.Do("SETEX", key, expiry, value)
if err != nil {
return err
}
return nil
}

Get

func GetValue(key string) (string, error) {
conn := RedisPool().Get()
reply, err := redis.String(conn.Do("GET", key))
if err == redis.ErrNil {
return "", err
}
if err != nil {
return "", err
}
return reply, nil
}

After having these functions set up we can move to the wrapper function that generates the OTP, maps it against the user identifier, and also returns an OTP Identifier. This OTP Identifier serves as the identifier(key) for retrieving an OTP record on the Redis DB.

func GenerateOTP(userId string) (string, string, error) {
otp := GenerateCode(6)
err := SetValue(userId, otp, 300)
if err != nil {
return "", "", err
}

otpIdentifier, err := GenerateUUID()
if err != nil {
return "", "", err
}

err = SetValue(otpIdentifier, userId, 300)
if err != nil {
return "", "", err
}

return otpIdentifier, otp, nil
}

The function above accepts a “userId” parameter, which serves as an identifier mapped to a generated OTP, the function returns 3 values, the OTP Identifier, the OTP, and an error if there is any.

In the body of the function above, the helper function for generating the 6-digit code is called, the result of this call is the OTP that is saved in the Redis DB using the userId as key with an expiry time of 300 seconds, which translates to 5 minutes. We then generate a UUID which acts as the OTP identifier, we go further to bind the userId again to the OTP Identifier and save it in the Redis Db, this guarantees a layer of security as users with a different userId will not be able to use OTPs generated for some other user. We would see this clearly in the Verification of OTP section.

This means we are saving two sets of records per OTP generated.

UserId (Key) => OTP (Value)
OTP Identifier (Key) => UserId (Value)

The OTP Identifier and the OTP are sent to the client end, where they are hooked appropriately to the request during verification.

Verifying the OTP against the User Identifier

In this section, we are going to be validating if the OTP sent from the client end exists and also verify it against the user it is being hooked with.

We have a wrapper function for Verifying OTP, which includes retrieving the OTP from the Redis DB and performing a couple of validations on it.

func VerifyOTP(otpIdentifier, userId, otp string) (bool, error) {
sessionUserId, err := GetValue(otpIdentifier)
if err != nil {
return false, errors.New("Incorrect or invalid OTP")
}

if sessionUserId != userId {
return false, errors.New("Invalid OTP")
}

sessionOTP, err := GetValue(sessionUserId)
if err != nil {
return false, err
}

if sessionOTP != otp {
return false, errors.New("Invalid OTP")
}

clearValue(otpIdentifier)

return true, nil
}

Here comes the tricky part, in the function above, the function accepts three parameters back from the client end, the “OTP Identifier”, “UserId”, and the “OTP”, and return a boolean indicating the outcome of the verification and an error if any.

We then call the “GetValue” function and pass the OTP Identifier, we aim to get the first layer of record saved on the Redis DB, which is the “OTP Identifier (Key) => UserId (Value)”, we name the retrieved value here “sessionUserId”, there are two things that can happen when retrieving this record. if the 5 mins have elapsed, this particular record no longer exists which indicates the OTP has expired, secondly, the record simply does not exist which indicates that a wrong/invalid OTP Identifier was passed.

Next, we verify the “userId” passed against the “sessionUserId” saved in the Redis DB in the first record we retrieved, we are making sure that the userId that belongs to the OTP Identifier is what is being passed during the verification.

Finally, we call the GetValue function again and pass the retrieved userId (sessionUserId), we aim to get the second layer of the record saved on the Redis DB, which is the “UserId (Key) => OTP (Value)”, we name the retrieved value here “sessionOTP” and check against the OTP passed from the client. Once again, if the 5 mins have elapsed this particular record no longer exists which indicates the OTP has expired.

After a successful verification, we call the “clearValue” function to delete the OTP Identifier from the Redis Db.

func ClearValue(key string) {
conn := RedisPool().Get()
_, err := redis.String(conn.Do("DEL", key))
if err == redis.ErrNil {

}
}

Conclusion

Kudos! You have now learned how to securely generate and verify OTP efficiently. With the information provided in this article, you can now add an extra layer of security to your application sessions and actions. You can also feel free to customize the logic to fit your taste, like ditching the OTP identifier if you want a semi-stateless feel during verification.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Kelechi Onyekwere
Kelechi Onyekwere

Written by Kelechi Onyekwere

I’m a Software Engineer with experience building distributed systems, resilient and fault tolerant solutions and an advocate for event sourcing / driven system.

No responses yet

Write a response