How to Implement a Robust Voice Authentication with Go and Twilio

April 22, 2025
Written by
Oluseye Jeremiah
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Implementing A Robust Voice Authentication with Twilio and Go

Voice OTP (One-Time Password) authentication is an increasingly popular method for secure user verification — especially in situations where SMS-based OTPs might not be ideal.

This approach generates a temporary password and then delivers it via voice call, which helps provide an additional layer of security to the user account.

Leveraging Twilio’s powerful voice API alongside Go, this tutorial outlines a streamlined process to create a voice-based OTP system. With it, developers can enhance their applications with a robust, accessible, and secure verification method.

Requirements

To follow along with this tutorial, ensure you have the following:

Create a new Go project

To get started, you need to create a new Go project. To do that, open your terminal and run the following commands where you create your Go projects:

mkdir go-registration-app
cd go-registration-app
go mod init go-registration-app

After running the commands above, open the newly created project folder named go-registration-app in your preferred code editor.

Create the environment variables

Let’s create environment variables to store your Twilio credentials. To do this, create a .env file inside the project folder's top-level directory, and add the following code to the file.

TWILIO_ACCOUNT_SID="<twilio_sid>"
TWILIO_AUTH_TOKEN="<twilio_token>"
TWILIO_PHONE_NUMBER="<twilio_number>"

Retrieve your Twilio credentials

Now, let’s retrieve your Twilio credentials. To do this, log in to your Twilio Console dashboard. In the Account Info section, you’ll find your Account SID, Auth Token, and Twilio phone number, as shown in the screenshot below.

Screenshot of Twilio account details including Account SID, Auth Token, and Twilio phone number.

Copy them and replace the corresponding Twilio environment variables in .env, respectively, with them.

Install Twilio's Go Helper Library

To interact with Twilio Programmable Voice in your Go project, let’s install Twilio's Go Helper Library using the command below:

go get github.com/twilio/twilio-go

Create the database schema

Let’s create a database schema to store registered user information. To do this, log in to your MySQL database, create a new database named "userinfo", and then create a "users"table with the following schema.

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    Username VARCHAR(255) NOT NULL,
    Phone VARCHAR(20) NOT NULL,
    Password VARCHAR(255) NOT NULL,
    Status_ VARCHAR(10) NOT NULL DEFAULT 'active'
);

After creating the table, your table structure should look like the screenshot below.

Screenshot of the table structure for a users table in phpMyAdmin with five fields listed.

Install other packages

In our application, we will use the mysql package to interact with the database, gorilla/sessions to manage user sessions, and godotenv to load environment variables into the application. To install these Go packages, run the commands below.

go get -u github.com/go-sql-driver/mysql
go get github.com/gorilla/sessions
go get github.com/joho/godotenv

Create the authentication logic

Now, let’s create the application’s registration, login, and verificationpages and implement OTP verificationusing Twilio Programmable Voice to confirm user accounts. To do this, create a file named main.go in the project's top-level directory and add the following code to the file.

package main

import (
	"database/sql"
	"fmt"
	"html/template"
	"log"
	"math/rand"
	"net/http"
	"os"
	"strconv"
	"time"
	_ "github.com/go-sql-driver/mysql"
	"github.com/gorilla/sessions"
	"github.com/joho/godotenv"
	"github.com/twilio/twilio-go"
	openapi "github.com/twilio/twilio-go/rest/api/v2010"
)

type User struct {
	Username string
	Phone    string
	Password string
	Status_  string
}

var accountSID, authToken, twilioFrom string
var store = sessions.NewCookieStore([]byte("your-secret-key"))

func InitializeTwilioClient() (*twilio.RestClient, error) {
	client := twilio.NewRestClientWithParams(twilio.ClientParams{
		Username: accountSID,
		Password: authToken,
	})
	log.Println("Twilio client initialized.")
	return client, nil
}

func SendSMS(client *twilio.RestClient, to, otp string) error {
	params := &openapi.CreateMessageParams{}
	params.SetTo(to)
	params.SetFrom(twilioFrom)
	params.SetBody(fmt.Sprintf("Your OTP code is: %s", otp))
	_, err := client.Api.CreateMessage(params)
	if err != nil {
		return fmt.Errorf("failed to send SMS: %v", err)
	}
	log.Printf("SMS sent to %s", to)
	return nil
}

func InitializeDB() (*sql.DB, error) {
	dsn := "<db_user>:<db_password>@tcp(127.0.0.1:3306)/users"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return nil, fmt.Errorf("failed to connect to database: %v", err)
	}
	if err = db.Ping(); err != nil {
		return nil, fmt.Errorf("failed to ping database: %v", err)
	}
	log.Println("Connected to MySQL database.")
	return db, nil
}

func SaveUserToDB(db *sql.DB, user User) error {
	query := "INSERT INTO users (Username, Phone, Password, Status_) VALUES (?, ?, ?, 'unverified')"
	_, err := db.Exec(query, user.Username, user.Phone, user.Password)
	if err != nil {
		return fmt.Errorf("failed to insert user into database: %v", err)
	}
	log.Printf("User %s saved to database.", user.Username)
	return nil
}

func UpdateUserStatus(db *sql.DB, phone string) error {
	query := "UPDATE users SET Status_ = 'verified' WHERE Phone = ?"
	_, err := db.Exec(query, phone)
	if err != nil {
		return fmt.Errorf("failed to update user status: %v", err)
	}
	log.Printf("User with phone %s status updated to verified.", phone)
	return nil
}

func parseTemplate(w http.ResponseWriter, templateFile string, data interface{}) {
	tmpl, err := template.ParseFiles(templateFile)
	if err != nil {
		log.Printf("Error loading template %s: %v", templateFile, err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}
	tmpl.Execute(w, data)
}

func generateOTP() string {
	rand.Seed(time.Now().UnixNano())
	return strconv.Itoa(rand.Intn(9000) + 1000)
}

func requireLoginAndVerified(db *sql.DB, next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		session, err := store.Get(r, "otp-session")
		if err != nil {
			log.Println("Error retrieving session:", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		phone, ok := session.Values["phone"].(string)
		if !ok {
			http.Redirect(w, r, "/register", http.StatusSeeOther)
			return
		}

		var status string
		query := "SELECT Status_ FROM users WHERE Phone = ?"
		err := db.QueryRow(query, phone).Scan(&status)
		if err != nil || status != "verified" {
			http.Redirect(w, r, "/verify-otp", http.StatusSeeOther)
			return
		}
		next.ServeHTTP(w, r)
	}
}

func registerHandler(db *sql.DB, client *twilio.RestClient) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet {
			w.Header().Set("Content-Type", "text/html")
			parseTemplate(w, "templates/register.html", nil)
			return
		}
		if r.Method == http.MethodPost {
			if err := r.ParseForm(); err != nil {
				http.Error(w, "Unable to parse form", http.StatusBadRequest)
				return
			}
			user := User{
				Username: r.FormValue("username"),
				Phone:    r.FormValue("phone"),
				Password: r.FormValue("password"),
			}
			log.Printf("New registration: %+v\n", user)
			if err := SaveUserToDB(db, user); err != nil {
				log.Printf("Error saving user to database: %v", err)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
				return
			}

			otp := generateOTP()
			log.Printf("Generated OTP for phone %s: %s", user.Phone, otp)
			session, _ := store.Get(r, "otp-session")
			session.Values["phone"] = user.Phone
			session.Values["otp"] = otp
			session.Save(r, w)

			if err := SendSMS(client, user.Phone, otp); err != nil {
				log.Printf("Failed to send SMS: %v", err)
				http.Error(w, "Failed to send OTP", http.StatusInternalServerError)
				return
			}
			http.Redirect(w, r, "/verify-otp", http.StatusSeeOther)
			return
		}
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func verifyOTPHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodGet {
			w.Header().Set("Content-Type", "text/html")
			parseTemplate(w, "templates/otp.html", nil)
			return
		}

		if r.Method == http.MethodPost {
			if err := r.ParseForm(); err != nil {
				http.Error(w, "Unable to parse form", http.StatusBadRequest)
				return
			}
			inputOTP := r.FormValue("otp")
			session, err := store.Get(r, "otp-session")
			if err != nil {
				log.Println("Error retrieving session:", err)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
				return
			}

			storedOTP, ok := session.Values["otp"].(string)
			phone, phoneOk := session.Values["phone"].(string)
			if !ok || !phoneOk || storedOTP != inputOTP {
				http.Error(w, "Invalid OTP", http.StatusUnauthorized)
				return
			}

			if err := UpdateUserStatus(db, phone); err != nil {
				http.Error(w, "Failed to update user status", http.StatusInternalServerError)
				return
			}
			delete(session.Values, "otp")

			session.Save(r, w)
			http.Redirect(w, r, "/profile", http.StatusSeeOther)
			return
		}
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func profileHandler(db *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		session, _ := store.Get(r, "otp-session")
		phone, ok := session.Values["phone"].(string)
		if !ok {
			http.Error(w, "Unauthorized access", http.StatusUnauthorized)
			return
		}

		user := User{}
		query := "SELECT Username, Phone, Status_ FROM users WHERE Phone = ?"
		err := db.QueryRow(query, phone).Scan(&user.Username, &user.Phone, &user.Status_)
		if err != nil {
			log.Printf("Error fetching user profile: %v", err)
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "text/html")
		parseTemplate(w, "templates/profile.html", user)
	}
}

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatalf("Error loading .env file: %v", err)
	}
	accountSID = os.Getenv("TWILIO_ACCOUNT_SID")
	authToken = os.Getenv("TWILIO_AUTH_TOKEN")
	twilioFrom = os.Getenv("TWILIO_PHONE_NUMBER")
	db, err := InitializeDB()
	if err != nil {
		log.Fatalf("Error connecting to the database: %v", err)
	}
	defer db.Close()

	client, err := InitializeTwilioClient()
	if err != nil {
		log.Fatalf("Error initializing Twilio client: %v", err)
	}

	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
	http.HandleFunc("/register", registerHandler(db, client))
	http.HandleFunc("/verify-otp", verifyOTPHandler(db))
	http.HandleFunc("/profile", requireLoginAndVerified(db, profileHandler(db)))
	log.Println("Server started at :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

From the code above:

  • The InitializeDB() function is used to connect to the MySQL database where we store the user information. Make sure you replace the <bd_user> and <db_password> with the corresponding database value.
  • The registerHandler() function handles the user registration details and saves them into the database using the SaveUserToDB()function
  • The verifyOTPHandler() function processes the user OTP code and changes the status of the user to "verified" if the OTP code is valid
  • The profileHandler() function loads the user profile details after the user successfully verifies their account
  • The main() function defines the application routes and connects the necessary functions together

Create the HTML form template

Next, we'll create the HTML templates for the application's pages. In the project's root folder, create a templates directory. Inside this folder, create a file named register.html and add the following code to it.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Registration Page</title>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <form action="/register" method="post">
        <h1>Register</h1>        
        <label for="phone">Phone:</label>
        <input type="tel" id="phone" name="phone" required>
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required>
        <button type="submit">Register</button>
    </form>
</body>
</html>

Once the user successfully registers, they are redirected to the account verification page to verify their phone number by entering the OTP sent to it. To create the OTP HTML template, in the templates folder, create a file named otp.html, and add the following code to the file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>OTP Verification</title>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <form action="/verify-otp" method="post">
        <h1>OTP Verification</h1>
        <p>Please enter the OTP sent to your phone number.</p> <br><br>
        <label for="otp">Enter OTP:</label>
        <input type="text" id="otp" name="otp" required>
        <button type="submit">Verify OTP</button>
    </form>
</body>
</html>

To create the profile HTML template, in the templates folder create a file named profile.html, and add the following code to the file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Profile</title>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <div class="profile-container">
        <h1>Welcome, {{.Username}}!</h1>
        <p><strong>Phone:</strong> {{.Phone}}</p>
        <p><strong>Status:</strong> {{.Status_}}</p>
    </div>
</body>
</html>

Add style to the application

Lastly, let’s style the application pages. To do this, create a static folder in the top-level directory of the project. Inside the folder, create a file named styles.css and add the following code to it.

/* Basic reset for the page */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: Arial, sans-serif;
    background-color: #f0f2f5;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.profile-container {
    background-color: #ffffff;
    padding: 25px;
    border-radius: 10px;
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
    width: 350px;
    text-align: center;
    transition: transform 0.3s ease-in-out;
}

.profile-container:hover {
    transform: translateY(-5px);
}

/* Container for the registration form */
form {
    background-color: #ffffff;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    width: 300px;
}

h1 {
    font-size: 24px;
    margin-bottom: 15px;
    text-align: center;
    color: #333;
}

label {
    font-size: 14px;
    color: #555;
    margin-bottom: 5px;
    display: block;
}

input[type="text"],
input[type="phone"],
input[type="password"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 15px;
    border: 1px solid #ddd;
    border-radius: 5px;
}

button {
    width: 100%;
    padding: 10px;
    background-color: #007bff;
    border: none;
    border-radius: 5px;
    color: #ffffff;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s;
}

button:hover {
    background-color: #0056b3;
}

p {
    font-size: 18px;
    color: #555;
    margin-bottom: 12px;
    line-height: 1.5;
}

strong {
    color: #333;
}

.profile-button {
    display: inline-block;
    margin-top: 15px;
    padding: 10px 15px;
    background-color: #007bff;
    color: #ffffff;
    font-size: 16px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease-in-out;
}

.profile-button:hover {
    background-color: #0056b3;
}

Start the application

Let’s start the application . To do that, run the command below in your terminal.

go run main.go

Test the application

Let’s now test the application to confirm it’s working as expected. Open your preferred browser and navigate to http://localhost:8080/register. The user registration page should appear, as shown in the screenshot below.

Screenshot of a registration form with fields for username, phone, and password, and a Register button.

After completing the registration, the user will be redirected to the account verification page to enter the OTP code received via phone call, as shown in the image below.

Web page with OTP verification prompt asking user to enter a code sent to their phone number.

Once the OTP is entered correctly, the user's account status will be updated to "verified",' and they will be redirected to their profile page, as shown in the image below.

A welcome message for user jerry101, showing phone 2347057236368 and status as verified.

That's how to implement a robust voice authentication with Twilio and Go

Building a voice authentication system using Twilio and Go provides a secure and efficient way to verify users. By combining Twilio's Voice API with Go's speed and simplicity, you can develop a solution that is both reliable and scalable for real-world applications.

This approach enhances security without compromising user experience, whether it's securing user accounts or enabling voice-based multi-factor authentication. By following this guide, you’ve taken the first step toward implementing voice authentication in your applications.

Oluseye Jeremiah is a dedicated technical writer with a proven track record in creating engaging and educational content.

Voice recognition icons created by Smashicons on Flaticon.