How to build a One-Time-Password(OTP) Verification API with Go and Twilio

Demola Malomo - Oct 29 '22 - - Dev Community

Organizations are always looking for innovative ways to tackle threats and vulnerabilities on their platform. They are constantly investing in human resources and technologies to help build and ship secure applications. Two-factor authentication, Authenticator app, and Biometrics, among others, are some of the innovative methods organizations are adopting to keep their platform safe.

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 Gin-gonic to build our API. However, the same approach also applies to any Go-based framework.

Prerequisites

To fully grasp the concepts presented in this tutorial, the following requirements apply:

  • Basic understanding of Go
  • Go installation ( Version 1.18 above)
  • A Twilio account; signup for a trial account is completely free.

Getting started

To get started, we need to navigate to the desired directory and run the command below in our terminal:



mkdir go-sms-verification && cd go-sms-verification


Enter fullscreen mode Exit fullscreen mode

This command creates a go-sms-verification folder and navigates into the project directory.

Next, we need to initialize a Go module to manage project dependencies by running the command below:



go mod init go-sms-verification


Enter fullscreen mode Exit fullscreen mode

This command will create a go.mod file for tracking project dependencies.

We proceed to install the required dependencies with:



go get github.com/gin-gonic/gin github.com/twilio/twilio-go github.com/joho/godotenv github.com/go-playground/validator/v10


Enter fullscreen mode Exit fullscreen mode

github.com/gin-gonic/gin is a framework for building web application.

github.com/twilio/twilio-go is a Go package for communicating with Twilio.

github.com/joho/godotenv is a library for managing environment variable.

github.com/go-playground/validator/v10 is a library for validating structs and fields.

Structuring our application

It is essential to have a good project structure as it makes the project maintainable and makes it easier for us and others to read our codebase.
To do this, we need to create an api, cmd, and data folder in our project directory.

project directory

api is for structuring our API-related files

cmd is for structuring our application entry point

data is for structuring our application data

Setting up Twilio

To enable OTP verification in our API, we need to sign into our Twilio Console to get our Account SID and an Auth Token. We need to keep these parameters handy as we need them to configure and build our APIs.

Twilio credentials

Create a Verification Service
Twilio ships with secure and robust services for seamlessly validating users with SMS, Voice, and Email. In our case, we will use the SMS option to verify users through phone numbers. To do this, navigate to the Explore Products tab, scroll to the Account security section, and click on the Verify button.

Verify account

Navigate to the Services tab, click on the Create new button, input sms-service as the friendly name, toggle-on the SMS option, and Create.

Create new service
Input details

Upon creation, we need to copy the Service SID. It will also come in handy when building our API.

Service SID

Enable geographical permission
Geographical Permissions are mechanisms put in place by Twilio to control the use of their services. It provides a tool for enabling and disabling countries receiving voice calls and SMS messages from a Twilio account.

To enable SMS, we need to search for SMS Geographic Permissions in the search bar, click on the SMS Geographic Permissions result, and then check the country where the SMS provider operates.

Search
Check country of operation

Creating OTP Verification APIs in Go

With the configuration done, we can start building our APIs by following the steps.

Add environment variables
Next, we need to create a .env file in the root directory and add the snippet below:



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


Enter fullscreen mode Exit fullscreen mode

As mentioned earlier, we can get the required credentials from Twilio console and Service Setting.

twilio console
service settings

Finally, we need to create helper functions for loading the environment variables into our application. To do this, we need to create a config.go file inside the api folder and add the snippet below:



package api

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)

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")
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Create an envACCOUNTSID, envAUTHTOKEN, and envSERVICESID functions that check if the environment variable is correctly loaded and returns the environment variable.

Create the API models
Next, we need to create models to represent our application data. To do this, we need to navigate to the data folder and, in this folder, create a model.go file and add the snippet below:



package data

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"`
}


Enter fullscreen mode Exit fullscreen mode

Create the API routes, helpers, service, and handlers
With the models to send and verify OTP fully set up, we need to navigate to the api folder and do the following:

First, we need to create a route.go file for configuring the API routes and add the snippet below:



package api

import "github.com/gin-gonic/gin"

type Config struct {
    Router *gin.Engine
}

func (app *Config) Routes() {
    //routes will come here
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Creates a Config struct with a Router property to configure the application methods
  • Creates a Routes function that takes in the Config struct as a pointer

Secondly, we need to create a helper.go file and add the snippet below:



package api

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)

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

var validate = validator.New()

func (app *Config) validateBody(c *gin.Context, data any) error {
    //validate the request body
    if err := c.BindJSON(&data); err != nil {
        return err
    }
    //use the validator library to validate required fields
    if err := validate.Struct(&data); err != nil {
        return err
    }
    return nil
}

func (app *Config) writeJSON(c *gin.Context, status int, data any) {
    c.JSON(status, jsonResponse{Status: status, Message: "success", Data: data})
}

func (app *Config) errorJSON(c *gin.Context, err error, status ...int) {
    statusCode := http.StatusBadRequest
    if len(status) > 0 {
        statusCode = status[0]
    }
    c.JSON(statusCode, jsonResponse{Status: statusCode, Message: err.Error()})
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a jsonResponse struct and validate variable to describe the API response and to validate the API fields
  • Creates a validateBody function that takes in the Config struct as a pointer and returns an error. Inside the function, we validate that request data in the correct format and also use the validator library to also validate and check for the required fields
  • Creates a writeJSON function that takes in the Config struct as a pointer and uses the jsonResponse struct to construct API response when there’s no error
  • Creates a errorJSON function that takes in the Config struct as a pointer and uses the jsonResponse struct to construct API response when there’s an error

Thirdly, we need to create a service.go file for abstracting the application logic and add the snippet below:




package api

import (
    "github.com/twilio/twilio-go"
    twilioApi "github.com/twilio/twilio-go/rest/verify/v2"
)

var client *twilio.RestClient = twilio.NewRestClientWithParams(twilio.ClientParams{
    Username: envACCOUNTSID(),
    Password: envAUTHTOKEN(),
})

func (app *Config) twilioSendOTP(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 (app *Config) twilioVerifyOTP(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
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a client variable to configure Twilio client using the Account SID and Auth Token
  • Creates a twilioSendOTP function that accepts a phoneNumber, takes in the Config struct as a pointer, and returns either a string or an error. Inside the function, we created a params variable by adding the phoneNumber and setting the channel for sending the OTP as sms. Finally, we use the client variable to create verification by using the Service SID and params and then return the appropriate response
  • Creates a twilioVerifyOTP function that accepts a phoneNumber and code, takes in the Config struct as a pointer, and returns an error. Inside the function, we created a params variable by adding the phoneNumber and code. Finally, we use the client variable to check authenticity of the OTP by using the Service SID and params and then return the appropriate response

Fourthly, we need to create a handler.go file for modifying the incoming request and add the snippet below:



package api

import (
    "context"
    "go-sms-verification/data"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

const appTimeout = time.Second * 10

func (app *Config) sendSMS() gin.HandlerFunc {
    return func(c *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        var payload data.OTPData
        defer cancel()

        app.validateBody(c, &payload)

        newData := data.OTPData{
            PhoneNumber: payload.PhoneNumber,
        }

        _, err := app.twilioSendOTP(newData.PhoneNumber)
        if err != nil {
            app.errorJSON(c, err)
            return
        }

        app.writeJSON(c, http.StatusAccepted, "OTP sent successfully")
    }
}

func (app *Config) verifySMS() gin.HandlerFunc {
    return func(c *gin.Context) {
        _, cancel := context.WithTimeout(context.Background(), appTimeout)
        var payload data.VerifyData
        defer cancel()

        app.validateBody(c, &payload)

        newData := data.VerifyData{
            User: payload.User,
            Code: payload.Code,
        }

        err := app.twilioVerifyOTP(newData.User.PhoneNumber, newData.Code)
        if err != nil {
            app.errorJSON(c, err)
            return
        }

        app.writeJSON(c, http.StatusAccepted, "OTP verified successfully")
    }
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates an appTimeout variable to set request timeout
  • Creates a sendSMS function that returns a Gin-gonic handler and takes in the Config struct as a pointer. Inside the returned handler, we defined the API timeout, used the helper functions and the service created earlier to verify the request body and send the OTP
  • Creates a verifySMS function that returns a Gin-gonic handler and takes in the Config struct as a pointer. Inside the returned handler, we defined the API timeout, used the helper functions and the service created earlier to verify the request body and the OTP

Finally, we need to update the routes.go files the API route and corresponding handler



package api

import "github.com/gin-gonic/gin"

type Config struct {
    Router *gin.Engine
}

//modify below
func (app *Config) Routes() {
    app.Router.POST("/otp", app.sendSMS())
    app.Router.POST("/verifyOTP", app.verifySMS())
}


Enter fullscreen mode Exit fullscreen mode

Putting it all together
With our API fully set up, we need to create the application entry point. To do this, we need to navigate to the cmd folder and, in this folder, create a main.go file and add the snippet below:



package main

import (
    "go-sms-verification/api"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    //initialize config
    app := api.Config{Router: router}

    //routes
    app.Routes()

    router.Run(":80")
}


Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependencies
  • Creates a Gin router using the Default configuration
  • Initialize the Config struct by passing in the Router
  • Adds the route and run the application on port :80

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



go run cmd/main.go 


Enter fullscreen mode Exit fullscreen mode

send OTP
OTP
Verifying OTP

We can also verify the message logs by navigating to the Verify tab of the Logs on Twilio

Message log

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:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .