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
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
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
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.
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.
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.
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.
Upon creation, we need to copy the Service SID. It will also come in handy when building our API.
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.
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>
As mentioned earlier, we can get the required credentials from Twilio console and Service Setting.
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")
}
The snippet above does the following:
- Imports the required dependencies
- Create an
envACCOUNTSID
,envAUTHTOKEN
, andenvSERVICESID
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"`
}
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
}
The snippet above does the following:
- Imports the required dependency
- Creates a
Config
struct with aRouter
property to configure the application methods - Creates a
Routes
function that takes in theConfig
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()})
}
The snippet above does the following:
- Imports the required dependencies
- Creates a
jsonResponse
struct andvalidate
variable to describe the API response and to validate the API fields - Creates a
validateBody
function that takes in theConfig
struct as a pointer and returns anerror
. Inside the function, we validate that requestdata
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 theConfig
struct as a pointer and uses thejsonResponse
struct to construct API response when there’s no error - Creates a
errorJSON
function that takes in theConfig
struct as a pointer and uses thejsonResponse
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
}
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 aphoneNumber
, takes in theConfig
struct as a pointer, and returns either astring
or anerror
. Inside the function, we created aparams
variable by adding thephoneNumber
and setting the channel for sending the OTP assms
. Finally, we use theclient
variable to create verification by using the Service SID andparams
and then return the appropriate response - Creates a
twilioVerifyOTP
function that accepts aphoneNumber
andcode
, takes in theConfig
struct as a pointer, and returns anerror
. Inside the function, we created aparams
variable by adding thephoneNumber
andcode
. Finally, we use theclient
variable to check authenticity of the OTP by using the Service SID andparams
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")
}
}
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 theConfig
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 theConfig
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())
}
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")
}
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 theRouter
- 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
We can also verify the message logs by navigating to the Verify tab of the Logs on Twilio
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: