Building a Secure OTP Verification API with Go Fiber and Twilio: A Step-by-Step Guide

Harsh Kumar Raghav
7 min readJun 1, 2023

--

When working on my personal project, I encountered the need for OTP verification to ensure secure authentication in my application. While searching for a solution in Go, I discovered Twilio — an excellent tool that simplifies OTP authentication.

In this post, we will learn how to build APIs that authenticate users with their phone numbers using Go and Twilio’s Verification Service. For this post, we will be using Go Fiber, the fastest HTTP framework for Go, to build our API. However, the same approach also applies to any Go-based framework.

To get started with Twilio, the first step is to create an account. Creating an account is straightforward and hassle-free. Visit the Twilio website and navigate to the account creation page. Follow the prompts to provide the necessary information, such as your email address, username, and password.

Prerequisites:

  • Understanding of Go
  • Go installation (Version 1.18 or above)
  • Go Fiber (the fastest HTTP engine for Go)
  • A Twilio account (a trial account is completely free)

Now, let’s walk through the steps to implement OTP verification in Go using Twilio:

Step 1:Creating the Project and Initializing Go.

Step 2: Installing the required packages.

Step 3: Setting up Twilio.

Step 4: Creating OTP Verification APIs in Go.

Creating the Project and Initializing Go

To begin, navigate to the desired directory in your terminal and run the following command:

mkdir my-otp-project

This command will create a “my-otp-project” folder. Then, navigate into the project directory using the command:

cd my-otp-project

Next, initialize a Go module to manage project dependencies by running the following command:

go mod init my-otp-project

This command will create a “go.mod” file for managing project dependencies.

Installing the Required Packages

We need to install the required packages for our project. Run the following commands in your terminal:

go get -u github.com/gofiber/fiber/v2
go get -u github.com/twilio/twilio-go
go get -u github.com/joho/godotenv
go get -u github.com/go-playground/validator/v10

These commands will install the necessary packages: Fiber (a web application framework), Twilio Go (a Go package for communicating with Twilio), Godotenv (a library for managing environment variables), and Validator (a library for validating structs and fields).

Setting up Twilio

To enable OTP verification in our API, we need to sign into the Twilio Console to obtain our Account SID and Auth Token. These parameters are required to configure and build our APIs.

Additionally, we need the Twilio Services ID or Verify ID to complete the setup. Store these environment variables in a .env file as follows:

PORT=5555
TWILIO_ACCOUNT_SID=<ACCOUNT SID>
TWILIO_AUTHTOKEN=<AUTH TOKEN>
TWILIO_SERVICES_ID=<SERVICE ID>

Creating OTP Verification APIs in Go

Establishing a well-structured project is vital for maintaining the readability of the codebase and ensuring smooth development, not just for ourselves but also for potential collaborators. So, let’s organize our project directory by creating three essential folders: “api,” “routes” (inside “api”), and a file named “main.go” (inside “routes”).

This structure will resemble the following:

By adopting this structure, we can ensure a cleaner and more organized project setup.

Now, let’s dive into the main.go file and follow the given steps to get our application up and running:

  1. Begin by importing the necessary packages and modules required for our application, which include Fiber and custom packages for authentication and configuration.
import (
"context"
"log"
"os"
"my-otp-project/api/routes"
"my-otp-project/pkg/auth"
"my-otp-project/pkg/configuration"

"github.com/gofiber/fiber/v2"
"github.com/joho/godotenv"
)

2. The main() function serves as the starting point of our application, where the execution begins.

3. Create an instance of the Fiber web framework using the fiber.New() function. This instance will handle HTTP requests and responses for our application.

4. Load the environment variables from a .env file using the godotenv.Load() function. These variables store configuration values for our application.

5. Define a route for the root URL (“/”) of our application. When a GET request is made to the root URL, respond with a JSON message indicating that the server is up and running.

6. Create and register routes for phone OTP generation and authentication using the CreatePhoneOtpRoutes function from the routes package. These routes will handle the necessary logic for OTP generation and verification.

Here is the complete code for the main.go file:

package main

import (
"context"
"log"
"os"
"my-otp-project/api/routes"

"github.com/gofiber/fiber/v2"
"github.com/joho/godotenv"
)

func main() {
app := fiber.New()

app.Get("/", func(c *fiber.Ctx) error {
return c.Status(200).JSON(fiber.Map{
"ping": "pong",
})
})
routes.CreatePhoneOtpRoutes(app)
log.Panic(app.Listen(":" + os.Getenv("PORT")))
}

7. Create a new file in the “routes” folder, let’s call it “phoneotp.go”.

8. Begin by importing the necessary packages and declaring constants/types/modules required for our application. These include Fiber, as well as custom packages for authentication and configuration.

9. Next, create a function that will generate two routes for sending and verifying phone OTPs within a Fiber app.

10. Now, let’s implement three functions responsible for loading environment variables. These functions are:

func ENVACCOUNTSID() string {
println(godotenv.Unmarshal(".env"))
err := godotenv.Load(".env")
if err != nil {
log.Fatalln(err)
log.Fatal("Error loading .env file")
}
return os.Getenv("TWILIO_ACCOUNT_SID")
}

func ENVAUTHTOKEN() string {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
return os.Getenv("TWILIO_AUTHTOKEN")
}

func ENVSERVICESID() string {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
return os.Getenv("TWILIO_SERVICES_ID")
}

11. Additionally, let’s define a function to validate the request body and another function to write JSON responses.

var validate = validator.New()

func validateBody(c *fiber.Ctx, data interface{}) error {
if err := c.BodyParser(&data); err != nil {
return err
}
if err := validate.Struct(data); err != nil {
return err
}
return nil
}

func writeJSON(c *fiber.Ctx, status int, data interface{}) {
c.JSON(jsonResponse{Status: status, Message: "success", Data: data})
}

12. Finally, we’ll create functions for sending and verifying OTPs via Twilio, completing the implementation.

Here is the complete code for phoneotp.go file:

package routes

import (
"context"
"log"
"net/http"
"os"
"time"

"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/joho/godotenv"
"github.com/twilio/twilio-go"
twilioApi "github.com/twilio/twilio-go/rest/verify/v2"
)
const appTimeout = time.Second * 10

type OTPData struct {
PhoneNumber string `json:"phoneNumber,omitempty" validate:"required"`
}

type VerifyData struct {
User *OTPData `json:"user,omitempty" validate:"required"`
Code string `json:"code,omitempty" validate:"required"`
}

type jsonResponse struct {
Status int `json:"status"`
Message string `json:"message"`
Data interface{} `json:"data"`
}

var validate = validator.New()

func validateBody(c *fiber.Ctx, data interface{}) error {
if err := c.BodyParser(&data); err != nil {
return err
}
if err := validate.Struct(data); err != nil {
return err
}
return nil
}

func writeJSON(c *fiber.Ctx, status int, data interface{}) {
c.JSON(jsonResponse{Status: status, Message: "success", Data: data})
}

func errorJSON(c *fiber.Ctx, err error, status ...int) {
statusCode := fiber.StatusBadRequest
if len(status) > 0 {
statusCode = status[0]
}
c.Status(statusCode).JSON(jsonResponse{Status: statusCode, Message: err.Error()})
}
func ENVACCOUNTSID() string {
println(godotenv.Unmarshal(".env"))
err := godotenv.Load(".env")
if err != nil {
log.Fatalln(err)
log.Fatal("Error loading .env file")
}
return os.Getenv("TWILIO_ACCOUNT_SID")
}

func ENVAUTHTOKEN() string {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
return os.Getenv("TWILIO_AUTHTOKEN")
}

func ENVSERVICESID() string {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
return os.Getenv("TWILIO_SERVICES_ID")
}
var client *twilio.RestClient = twilio.NewRestClientWithParams(twilio.ClientParams{
Username: envACCOUNTSID(),
Password: envAUTHTOKEN(),
})

func twilioSendOTP(c *fiber.Ctx, phoneNumber string) (string, error) {
params := &twilioApi.CreateVerificationParams{}
params.SetTo(phoneNumber)
params.SetChannel("sms")

resp, err := client.VerifyV2.CreateVerification(envSERVICESID(), params)
if err != nil {
return "", err
}

return *resp.Sid, nil
}

func twilioVerifyOTP(c *fiber.Ctx, phoneNumber string, code string) error {
params := &twilioApi.CreateVerificationCheckParams{}
params.SetTo(phoneNumber)
params.SetCode(code)

resp, err := client.VerifyV2.CreateVerificationCheck(envSERVICESID(), params)
if err != nil {
return err
} else if *resp.Status == "approved" {
return nil
}

return nil
}
func sendSMS() fiber.Handler {
return func(c *fiber.Ctx) error {
_, cancel := context.WithTimeout(context.Background(), appTimeout)
defer cancel()
var payload OTPData
if err := c.BodyParser(&payload); err != nil {
return err
}
newData := OTPData{
PhoneNumber: payload.PhoneNumber,
}
_, err := twilioSendOTP(c, newData.PhoneNumber)
if err != nil {
errorJSON(c, err)
return err
}
writeJSON(c, http.StatusAccepted, "OTP sent successfully")
return nil
}
}

func verifySMS() fiber.Handler {
return func(c *fiber.Ctx) error {
_, cancel := context.WithTimeout(c.Context(), appTimeout)
defer cancel()
var payload VerifyData

if err := c.BodyParser(&payload); err != nil {
return err
}
newData := VerifyData{
User: payload.User,
Code: payload.Code,
}

err = twilioVerifyOTP(c, newData.User.PhoneNumber, newData.Code)
if err != nil {
errorJSON(c, err)
return err
}
return c.JSON(fiber.Map{
"status": http.StatusOK,
"message": "OTP verified successfully",
"token": token,
})

}
}

func CreatePhoneOtpRoutes(app *fiber.App) {
app.Post("/api/auth/sendotp", sendSMS())
app.Post("/api/auth/verifyotp", verifySMS(svc))
}

With that done, we can start a development server using the command below:

go run main.go

Now we will test the APIs:

First API is for sending the OTP:

Now the first API will send the OTP on you device:

The second API is for verifying the phone Using OTP which is sent to your phone:

Conclusion

This post discussed how to create APIs that check and verify users with their phone numbers using Go and Twilio’s Verification Service. Beyond SMS-based verification, Twilio ships multiple services to seamlessly integrate authentication into a new or existing codebase.

These resources might be helpful:

--

--

Harsh Kumar Raghav

Skilled in Front-end Development, Algorithms, C, C++, Strong media and communication professional with a Bachelor’s degree focused in Information Technology.