One-Time Passcode (OTP) Verification in Go with Twilio Verify

Nikhil Shrestha
readytowork, Inc.
Published in
9 min readFeb 23, 2023

Many of us are already familiar with OTP verification. We have used it on multiple platforms to make our experience more secure. In this article, we will set up OTP verification in the Go programming language with Twilio.

This article can be divided into two main sections. First, we will focus on the required setup for Twilio, then jump into the coding part.

The setup for Twilio has been further broken down into three different steps. Let’s check each of them.

Step 1: Twilio Account Setup

The first thing we need to do is sign-up for Twilio. Twilio provides a limited amount of balance for the trial period. Please follow the link to sign-up: https://www.twilio.com/try-twilio

After the registration, you can activate one phone number for your trial period. On the home page, you will see the option to activate the phone number.

Click on the button Get a Twilio phone number and follow the further instructions.

After that, there is a section on the home page on the lower left side for Account Info. From there you can get your Account SID (1) and Auth Token (2), which are important credentials for the OTP verification service.

Step 2: Verification Service in Twilio

Twilio already has a service for verification. This service not only generates one-time password but also verifies it with the related phone number. There are options for using SMS, voice call, or email for verification. In this article, we will be particularly looking at the SMS option.

On the home page, click on Explore Products and navigate to the Account security section and select Verify.

Click on Create Service Now button

On the option to Create a new Service, give a Friendly Name for the service and click Create button. In the image above, the Friendly Name has been set as OTP.

Now, the Verify service has been set up. The Service SID (3) will be later used in our program to access this service. Also, make sure that in the SMS section(4), the channel is enabled since we will be using SMS as the verification channel.

Step 3: Geographic Permissions

Another important feature of Twilio is Geographic Permissions. With this, the user can filter and select the specific locations where the concerned twilio service can be used for the user. Depending on the user selection, a country can or cannot receive voice calls or SMS from the twilio account.

To enable Geo permissions for our service, navigate to Verify service through Explore Products as we did in Step 2. Now, select Geo Permissions to enable the required permission.

Here, you can navigate to the countries for which you want to enable this service and enable the SMS channel and voice channel as required. Don’t forget to save your settings.

Finally, the required setup for Twilio is complete. We can now jump into the coding section.

Project Structure

verify-otp (Project folder)
-> api (folder)
- controller.go
- service.go

-> config (folder)
- twilio.go

-> model (folder)
- models.go

.env

main.go

Let’s dive into the files and respective codes:

Here is a look at the .env file:

TWILIO_ACCOUNT_SID=Add your account Service ID(1) here
TWILIO_SERVICE_SID=Add your Service SID(3) from verify service
TWILIO_AUTH_TOKEN=Add your authorization token(2) here

Inside model folder, let’s create a models.go file

package model

import "github.com/go-playground/validator/v10"

type OTPData struct {
PhoneNumber string `json:"phone_number" validate:"required"`
}

type VerifyOTP struct {
VerificationCode string `json:"verification_code" validate:"required"`
OTPData
}


func ValidateData(data any) error {
var validate = validator.New()
if err := validate.Struct(data); err != nil {
//handle error
}
return nil
}

In the models.go file, we have created two structures that we will be using in the files to deal with the request to our API.

OTPData is a struct type that contains PhoneNumber a field and VerifyOTP contains OTPData as well as VerificationCode of string type. We will be implementing this struct in upcoming files.

Then, we initialized a new validator to validate the field in the structs above.

ValidateData() the function is declared for the sole purpose of validating data, passed to the function, using the initialized validator.

Next, inside a folder named config, we will create a file, twilio.go

package config

import (
"github.com/joho/godotenv"
"github.com/twilio/twilio-go"
"os"
)

func TwilioClient() (*twilio.RestClient, string, error) {
if err := godotenv.Load(); err != nil {
return nil, "", err
}
serviceSID := os.Getenv("TWILIO_SERVICE_SID")
authToken := os.Getenv("TWILIO_AUTH_TOKEN")
accountSID := os.Getenv("TWILIO_ACCOUNT_SID")

client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSID,
Password: authToken,
})
return client, serviceSID, nil
}

In this file, we have created a function TwilioClient() , which uses the Twilio credentials from .env file to create Twilio client and returns the generated client along with the service ID.

Let’s move on to the api folder.

Here, let’s create a file service.go

package api

import (
"errors"
"fmt"
twilioApi "github.com/twilio/twilio-go/rest/verify/v2"
"verify-otp/config"
"verify-otp/model"
"strings"
)

func sendOTP(otpData model.OTPData) (string, error) {
params := &twilioApi.CreateVerificationParams{}
params.SetTo(otpData.PhoneNumber)
params.SetChannel("sms")

client, serviceID, err := config.TwilioClient()
if err != nil {
return "", err
}

resp, err := client.VerifyV2.CreateVerification(serviceID, params)
if err != nil {
if strings.Contains(err.Error(), "ApiError 60200") {
err = errors.New("please enter country code in the phone number")
}
return "", err
}

return *resp.Status, nil
}

func VerifyOtp(otpNew model.VerifyOTP) (string, error) {
client, serviceID, err := config.TwilioClient()
if err != nil {
return "", err
}

params := &twilioApi.CreateVerificationCheckParams{}
params.SetTo(otpNew.PhoneNumber)
params.SetCode(otpNew.VerificationCode)

resp, err := client.VerifyV2.CreateVerificationCheck(serviceID, params)
if err != nil {
if strings.Contains(err.Error(), "ApiError 60200") {
err = errors.New("please add country code in the phone number")
}
return "", err
}

if *resp.Status != "approved" {
err := errors.New(*resp.Status)
return "", err
}

return *resp.Status, nil
}

This file deals with the handling of all the operations associated with Twilio. It mainly contains two functions to request and verify the OTP. Let’s take a look at them.

sendOTP():

params := &twilioApi.CreateVerificationParams{}
params.SetTo(otpData.PhoneNumber))
params.SetChannel("sms")

At first, we initialized a variable named params a verification param and set the required value, i.e., the user phone number and the channel to it. Since we will be using SMS service, we have set the channel as sms , however, call be also used as a channel. It determines how the user will be receiving the generated OTP.

client, serviceID, err := config.TwilioClient()
if err != nil {
return "", err
}

The function to create twilio client in the config has been called.

resp, err := client.VerifyV2.CreateVerification(serviceID, params)
if err != nil {
if strings.Contains(err.Error(), "ApiError 60200") {
err = errors.New("please enter country code in the phone number")
}
return "", err
}
return *resp.Status, nil

Now, we use the client to create verification bypassing the setup params and serviceID to the CreateVerification() function. One of the most usual errors that we may receive is if the user has not added a country code to the phone number which is mandatory to create verification.

Twilio has different error codes labeled for various types of errors. In this case, we check if the error is ApiError 60200 , which means the country code is not included in the phone number while making the request. There are other different error codes that you may use to handle errors in a similar manner.

Finally, we return the status of the response generated by CreateVerification() function.

verifyOTP():

   client, serviceID, err := config.TwilioClient()
if err != nil {
return "", err
}
params := &twilioApi.CreateVerificationCheckParams{}
params.SetTo(otpNew.PhoneNumber)
params.SetCode(otpNew.VerificationCode)

Here, we have used the TwilioClient() function from config to create a client and then, set up the parameters required to check the created password for verification.

   resp, err := client.VerifyV2.CreateVerificationCheck(serviceID, params)
if err != nil {
if strings.Contains(err.Error(), "ApiError 60200") {
err = errors.New("Please enter country code in the phone number")
}
return "", err
}

if *resp.Status != "approved" {
err := errors.New(*resp.Status)
return "", err
}

After setting up params , we use the client to check for verification with CreateVerficationCheck() the function. And then, if the response obtained contains approved its status, the verification is successful.

Next in api folder, we will create controller.go

package api

import (
"github.com/gin-gonic/gin"
"net/http"
"verify-otp/model"
)

func RequestOTP(c *gin.Context) {
var phone model.OTPData
if err := c.BindJSON(&phone); err != nil {
//handle error
return
}

if err := model.ValidateData(&phone); err != nil {
//handle error
return
}

resp, err := sendOTP(phone)
if err != nil {
//handle error
return
}
c.JSON(http.StatusOK, gin.H{
"result": "success",
"response": resp,
})
}

func ConfirmOTP(c *gin.Context) {
var data model.VerifyOTP
if err := c.BindJSON(&data); err != nil {
//handle error
return
}

if err := model.ValidateData(data); err != nil {
//handle error
return
}

resp, err := VerifyOtp(data)
if err != nil {
//handle error
return
}
c.JSON(http.StatusOK, gin.H{
"result": "success",
"response": resp,
})
}

The controller.go contains two different functions to request the OTP from Twilio and then, verify it. Since we already have the code, let’s break down the happenings of these two functions.

func RequestOTP(c *gin.Context) {
var phone model.OTPData
if err := c.BindJSON(&phone); err != nil {
//handle error
return
}

if err := model.ValidateData(&phone); err != nil {
//handle error
return
}

resp, err := sendOTP(phone)
if err != nil {
//handle error
return
}
c.JSON(http.StatusOK, gin.H{
"result": "success",
"response": resp,
})
}

RequestOTP():

This function handles the initial request made for the OTP. When the user request for the OTP, this function binds the data sent to make a request and validates if all the fields, required to make the requests, are present using the ValidateData() function from the model. Then, it calls the sendOTP() function from the service file and returns the response accordingly.

func ConfirmOTP(c *gin.Context) {
var data model.VerifyOTP
if err := c.BindJSON(&data); err != nil {
//handle error
return
}

if err := model.ValidateData(data); err != nil {
//handle error
return
}

resp, err := VerifyOtp(data)
if err != nil {
//handle error
return
}
c.JSON(http.StatusOK, gin.H{
"result": "success",
"response": resp,
})
}

ComfirmOTP():

This function handles the request to verify the obtained OTP. After the user receives the OTP and sends it back for verification, this function binds the data sent and validates if all the fields, required to make the requests, are present using the ValidateData() function from the model. Then, using the verifyOTP() function from the service.go file, it returns the generated response.

Finally, let’s look at our main.go file

package main

import (
"github.com/gin-gonic/gin"
"verify-otp/api"
)

func main() {
router := gin.Default()
router.POST("/", api.RequestOTP)
router.POST("/verify", api.ConfirmOTP)
router.Run()
}

main():

In the main.go, we basically have a main() function, where we have initialized and run a router with gin . The router has two different routes setup for RequestOTP() and ConfirmOTP() functions from the controller.

Result

Run the program with the following command:

$ go run main.go

You can test this via Postman or another similar platform by creating a POST request for the given routes.

Requesting for verification code

Response from request to get OTP

An SMS will be sent to the phone number added to the request.

Verifying obtained code

Response from verifying received OTP successfully

Thank you for reading, and I hope this article has been useful.

--

--