How to Build Passwordless Auth Using TOTP With Twilio Verify in Go

August 13, 2026
Written by

How to Build Passwordless Auth Using TOTP With Twilio Verify in Go

Typing or generating a unique password for every new account or website is a hassle. Although password managers help, passwordless auth (authentication) offers a more streamlined and secure alternative.

In this tutorial, you will learn about passwordless auth, and build a Go app that uses Twilio Verify to implement it.

Prerequisites

Before you begin, ensure you have the following:

  • A free Twilio account. Click here to create a free account if you are new to Twilio.
  • An authentication app, such as Twilio Authy
  • Go (at least version 1.23)
  • Your favourite text editor or IDE (such as neovim or Visual Studio Code)
  • Your favourite web browser

Architecture

This application will be a simplistic web-based application, split up over three stages.

  • Stage one: the user will submit their username to begin setting up Two-factor Authentication (2FA).
  • Stage two: they'll set up a TOTP entry in their authenticator app. They'll do this with their authenticator app by scanning a QR code or entering a unique code. Then, they'll submit the initial code that their authenticator app generates for them. If the code is valid, their account is ready to use.
  • Stage three: They can validate future codes which their authenticator app generates for them in the final form in the application.

What is passwordless auth?

Passwordless auth is an authentication method where the user does not need a password in order to log into an app or system. Rather, the user's mobile device receives a one-time code. In this authentication method, users are authenticated using other unique and more secure alternatives like Time-based One-time Passwords (TOTP, or Soft Token), SMS, Passkeys, Silent Network Authentication (SNA), Voice, or email notification.

Here's a breakdown of how it works, using TOTP:

  1. User initiation: When a user creates an account, instead of entering a password, they create a new Factor resource with Twilio Verify — initially marked as unverified — seeding it with a unique (auto-generated) identifier.
  2. Scan the QR code with an authenticator app: Using an authenticator app such as Twilio Authy, they scan the QR code then enter the code that the authenticator app provides. The code is validated using Twilio Verify. If the code is valid the Factor is marked verified. The authenticator app can now provide time-based codes to use in the future, when logging in, which Twilio Verify will verify.
  3. Code validation: When the user logs in, they enter their username and a TOTP code generated by their authenticator app. The application then uses Twilio Verify to validate the code along with their unique identifier.

This approach significantly enhances security by leveraging the inherent security features of the user's mobile device as a second factor in the authentication process. It also eliminates the need for them to memorize passwords, thereby simplifying the authentication process and enhancing user convenience.

Passwordless authentication has additional advantages, including the following:

  • Enhanced security: Reduces the risk of password-related breaches.
  • Convenience: Eliminates the need for users to remember and manage multiple passwords.
  • Reduced friction: Streamlines the login process, improving the user experience.
  • Scalability: Easily scalable with Twilio's infrastructure.

Build the app

Step 1: Set up the project

Set up a new project by running the following commands, wherever you store your Go projects:

mkdir -p \
     twilio-verify-totp-golang/assets/{css,qrcodes} \
     twilio-verify-totp-golang/templates
cd twilio-verify-totp-golang
go mod init

If you're using Microsoft Windows, use the commands below, instead.

mkdir twilio-verify-totp-golang/assets/css twilio-verify-totp-golang/assets/qrcodes twilio-verify-totp-golang/templates
cd twilio-verify-totp-golang
go mod init

Step 2: Install the required dependencies

Next, install the required dependencies, by running the following:

go get \
    github.com/alexedwards/scs/v2 \
    github.com/skip2/go-qrcode \
    github.com/gorilla/securecookie \
    github.com/gorilla/sessions \
    github.com/joho/godotenv \
    github.com/twilio/twilio-go
If you're using Microsoft Windows, replace the backslashes with carets (^).

In case you're not familiar with packages, here's a short description of them:

Now, open the project directory in your preferred text editor or IDE.

Step 3: Set the required environment variables

Dotenv files (commonly named .env) are used to store the configuration information that your app needs during development, separate from the application's code. For this tutorial, it will be your Twilio credentials (i.e., your Twilio Account SID and Auth Token) and a Verify Service SID.

In your project's top-level directory, open .env and add the following variable to the end of the file; the other two variables have already been set in the file.

TWILIO_VERIFY_SERVICE_SID=

You next need to retrieve the credentials to set as their values. To do that, sign into the Twilio Console. There, click the black and white up arrow at the bottom of the page, and you should see your Account SID and Auth Token in the Workbench; as shown in the image below.

Dark-themed dashboard showing API credentials and quick action buttons for various functionalities.

Copy these values and paste them into .env as the values for TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN.

Next, navigate to Products and Services > Verify > Services. On this page, click Create new. Then, fill out the initial form with the configuration values shown in the screenshot below, and click Continue.

Form to create a new Twilio project with options for verification via SMS, WhatsApp, Email, or Voice.

In the next step, leave Enable Fraud Guard set to "Yes" and click Continue to finish creating the service.

Popup window to enable Fraud Guard for SMS messages with options to select Yes or No.

After creating the service, copy the Service SID and paste it into .env as the value of TWILIO_VERIFY_SERVICE_SID.

Screenshot of Twilio service settings showing General options with fields for Friendly name and Service SID.

Step 4: Build the base Go application

The next thing to do is to build the base application. To do that, create a new file named main.go and paste the code below into it:

package main

import (
	"encoding/hex"
	"html/template"
	"log"
	"math/rand"
	"net/http"
	"os"
	"time"
	"github.com/alexedwards/scs/v2"
	"github.com/joho/godotenv"
	"github.com/twilio/twilio-go"
	verify "github.com/twilio/twilio-go/rest/verify/v2"
)

var sessionManager *scs.SessionManager

func main() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal(err)
	}

	sessionManager = scs.New()
	sessionManager.Lifetime = 24 * time.Hour

	app := Application{
		twilioRestClient: twilio.NewRestClientWithParams(twilio.ClientParams{
			Username: os.Getenv("TWILIO_ACCOUNT_SID"),
			Password: os.Getenv("TWILIO_AUTH_TOKEN"),
		}),
		verifyServiceSid: os.Getenv("TWILIO_VERIFY_SERVICE_SID"),
	}

	fileServer := http.FileServer(http.Dir("./assets/"))

	mux := http.NewServeMux()
	mux.Handle("GET /static/", http.StripPrefix("/static", fileServer))

	log.Println("Server starting on :8080")
	if err := http.ListenAndServe(":8080", sessionManager.LoadAndSave(mux)); err != nil {
		log.Printf("Failed to start server: %v\n", err)
	}
}

type Application struct {
	twilioRestClient *twilio.RestClient
	verifyServiceSid string
}

The code above creates a basic, Go-based, web application loads that:

  • Loads environment variables from the variables defined in .env, using GoDotEnv, ensuring that TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_VERIFY_SERVICE_SID have been set and are not empty.
  • Adds session support for persisting required information between requests.
  • Adds a Twilio Rest Client for interacting with Twilio's Verify API to set up and verify the TOTP Factor and validate TOTP codes.
  • Adds support for loading static files from the assets directory, required to load the application's stylesheet (styles.css).
  • Adds a custom struct, Application, that will make application state available to the methods which will handle requests to the application's various routes.

Step 5: Add the ability to create a new TOTP Factor

Now, you'll add the first feature of the application: the ability to create a new TOTP Factor. To do that, add the following method to the end of main.go.

func (app *Application) displayCreateTotpFactorForm(w http.ResponseWriter, r *http.Request) {
	signInTmpl, err := template.ParseFiles("./templates/enter-username.tmpl")
	if err != nil {
		log.Print(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	err = signInTmpl.Execute(w, nil)
	if err != nil {
		log.Print(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	}
}

The displayCreateTotpFactorForm() method uses Go templates to render templates/enter-username.tmpl (which you'll create shortly). The method will be called in response to GET requests to the application's default route "/", rendering a form for the user to enter their username as the first stage in the TOTP setup process.

Now, in the templates directory, create a new file named enter-username.tmpl, and in that file, paste the code below:

<!DOCTYPE html>
<html>
<head>
    <title>Register New TOTP Factor</title>
    <link href="https://assets.twilio.com/public_assets/paste-fonts/1.5.2/fonts.css" rel="stylesheet">
    <link href="/static/css/styles.css" rel="stylesheet">
</head>
<body class="container">
    <main>
        <h1>Register New TOTP Factor</h1>
        <p class="mt-60">
        This demo of the <a href="https://www.twilio.com/docs/verify/api">Twilio Verify API</a> shows how to set up two-factor authentication (2FA) with a third-party authenticator app like <a href="https://authy.com/download/">Authy</a> or Google Authenticator using the <a href="https://www.twilio.com/docs/glossary/totp">time-based one-time password (TOTP)</a> standard.
        </p>
        <hr class="mt-60">
        <form action="/" method="post" class="mt-60 mb-60">
            <label for="username">Enter your username:</label>
            <div class="mt-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
                <input
                    id="username"
                    inputmode="text"
                    name="username"
                    required
                    style="flex-shrink: 1;"
                    type="text"
                >
                <button
                    class="primary"
                    style="flex-grow: 1; white-space: nowrap"
                    type="submit"
                >Set up two-factor authentication</button>
            </div>
        </form>
        <div style="background-color: #d6ebf0; padding: 2em; border-radius: 0.25rem;" class="mb-60 mt-60">
            <h2 class="mb-60">Create a New Factor | What's happening here:</h2>
            <p style="line-height: 1.5;" class="mb-60">
                The API is <strong>creating a new TOTP <a href="https://www.twilio.com/docs/verify/api/factor#create-a-new-factor-resource">Factor</a></strong> when you click "Set up two-factor authentication".
                This is how the Verify API connects a user (<code>identity</code>), the <a href="https://www.twilio.com/docs/glossary/totp">TOTP channel</a>, and your app.
                Each Factor returns the secret seed and URI used to create a QR code.
            </p>
            <p>
                For this demo, we're asking for a username that will be displayed in the account name for the authenticator app.
            </p>
        </div>
        <a href="/">Reset</a>
    </main>
</body>
</html>

As you can see from the HTML above, it is a simplistic HTML page with a form, with a single field named "username". Note that the field's type and inputmode attributes are set to "text". This hints to browsers on mobile devices to render a virtual keyboard most appropriate for entering text input.

Then, add the following method and function to the end of main.go:

func (app *Application) processCreateTotpFactorForm(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		log.Fatal(err)
	}

	username := r.Form.Get("username")
	seed := generateSeed(55)
	sessionManager.Put(r.Context(), "seed", seed)

	params := &verify.CreateNewFactorParams{}
	params.SetFriendlyName(username)
	params.SetFactorType("totp")

	resp, err := app.twilioRestClient.VerifyV2.CreateNewFactor(
		app.verifyServiceSid,
		seed,
		params,
	)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	if resp.Binding == nil {
		log.Printf("Binding not available: %v\n", resp.Binding)
	}

	if binding, ok := (*resp.Binding).(map[string]any); ok {
		log.Printf("Binding: %v\n", binding)
		sessionManager.Put(r.Context(), "otp_uri", binding["uri"])
	}

	sessionManager.Put(r.Context(), "friendly_name", *resp.FriendlyName)
	sessionManager.Put(r.Context(), "sid", *resp.Sid)
	sessionManager.Put(r.Context(), "url", *resp.Url)

	http.Redirect(w, r, "/challenge", http.StatusSeeOther)
}

func generateSeed(n int) string {
	b := make([]byte, (n+1)/2)
	src := rand.New(rand.NewSource(time.Now().UnixNano()))

	if _, err := src.Read(b); err != nil {
		panic(err)
	}

	return hex.EncodeToString(b)[:n]
}

The processCreateTotpFactorForm() method creates a new TOTP Factor with Twilio Verify. The factor is seeded with a string returned from the generateSeed() function; which I borrowed from https://stackoverflow.com/a/46909816. The function creates a byte array of the length specified in n (55 chars). This is then filled with random values, encoded as a hex string, returned, and stored in the current session with the key "seed".

For generating cryptographic strings, crypto/rand should be used instead of math/rand.

Following this, the seed is used to create a new factor resource using the Twilio Rest Client. From the response to that request the following properties are stored in session for later use, the:

  • Friendly Name: The Factor's friendly name
  • SID: A 34 character string that uniquely identifies the Factor.
  • OTP URI: This stores the OTP configuration, including a shared secret and related parameters, for the OTP client (e.g., Authy) to use to generate the initial TOTP code.
  • URL:The URL of the Factor resource.

If the new Factor's status is set to "unverified", the user is redirected to the verify-totp route. Otherwise, they're redirected back to the create-totp route to try again.

Now, again in main.go, add the following code after the static route definition ("GET /static/") in the main() function.

mux.HandleFunc("GET /", app.displayCreateTotpFactorForm)
mux.HandleFunc("POST /", app.processCreateTotpFactorForm)

The code adds the GET and POST variants of the default route ("/") to the application's routing table; the GET version is handled by displayCreateTotpFactorForm(), and the POST version by processCreateTotpFactorForm().

Step 6: Add the ability to verify the TOTP Factor

You'll now add the functionality to render the form with the QR code for verifying the new Factor resource. Start by adding the code below to the end of main.go.

func (app *Application) displayVerifyUserForm(w http.ResponseWriter, r *http.Request) {
	verifyTmpl, err := template.ParseFiles("./templates/verify-user.tmpl")
	if err != nil {
		log.Print(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	uri := sessionManager.GetString(r.Context(), "otp_uri")
	if uri == "" {
		log.Println("An empty was retrieved from the session")
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	err = qrcode.WriteFile(uri, qrcode.Medium, 512, "assets/qrcodes/qr.png")
	if err != nil {
		log.Printf("Could not create QR code, because %s", err)
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	type VerifyOtpTemplateData struct {
		Seed string
	}

	err = verifyTmpl.Execute(w, VerifyOtpTemplateData{
		Seed: sessionManager.GetString(r.Context(), "seed"),
	})
	if err != nil {
		log.Print(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	}
}

The showVerifyOtpForm() method renders templates/verify.html.tmpl, providing the form for the user to enter to validate their new TOTP Factor. The template renders a template variable named Seed which is the unverified Factor's seed (or identity). It will also render a QR code which embeds the unverified Factor's OTP URI, and stored in assets/qrcodes/qr.png.

With that done, in the templates directory create a new file named verify-user.html.tmpl and paste the code below into the file.

<!DOCTYPE html>
<html>
<head lang="en-AU">
    <title>Register User</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link href="https://assets.twilio.com/public_assets/paste-fonts/1.5.2/fonts.css" rel="stylesheet">
    <link href="/static/css/styles.css" rel="stylesheet">
</head>
<body class="container">
    <main>
        <h1>Register User</h1>
        <p class="mt-60 mb-60">
        This demo of the <a href="https://www.twilio.com/docs/verify/api">Twilio Verify API</a> shows how to set up two-factor authentication (2FA) with a third-party authenticator app like <a href="https://authy.com/download/">Authy</a> or Google Authenticator using the <a href="https://www.twilio.com/docs/glossary/totp">time-based one-time password (TOTP)</a> standard.
        </p>
        <hr>
        <form action="/challenge" method="post" class="mt-60">
            <p>Please scan the QR code in an authenticator app like Authy.</p>
            <div class="mt-20 mb-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
                <img src="/static/qrcodes/qr.png" alt="QR Code" width="40%">
                <div class="mt-60">
                    <p>Or enter this code into your authentication app:</p>
                    <p><strong>{{ .Seed }}</strong></p>
                </div>
            </div>
            <label for="code">Enter the code generate by your authenticator app to verify this factor:</label>
            <div class="mt-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
                <input
                    id="code"
                    inputmode="numeric"
                    name="code"
                    required
                    pattern="\d{6}"
                    placeholder="123456"
                    type="text"
                >
                <button type="submit" class="primary" style="flex-grow: 1;">Verify</button>
            </div>
        </form>
        <div style="background-color: #d6ebf0; padding: 2em; border-radius: 0.25rem;" class="mb-60">
            <h2 class="mb-60">Create a New Factor | What's happening here:</h2>
            <p style="line-height: 1.5;" class="mb-60">
                The API is <strong>creating a new TOTP <a href="https://www.twilio.com/docs/verify/api/factor#create-a-new-factor-resource">Factor</a></strong> when you click "Set up two-factor authentication".
                This is how the Verify API connects a user (<code>identity</code>), the <a href="https://www.twilio.com/docs/glossary/totp">TOTP channel</a>, and your app.
                Each Factor returns the secret seed and URI used to create a QR code.
            </p>
            <p>
                For this demo, we're asking for a username that will be displayed in the account name for the authenticator app.
            </p>
        </div>
        <a href="/">Reset</a>
    </main>
</body>
</html>

The HTML renders another, simplistic, form with the QR code to scan, and a field for the TOTP code generated by the user's authenticator app. The field uses the pattern attribute to ensure that the only valid input is a 6-digit code. It also sets the inputmode attribute to "numeric" to have mobile browsers display a keyboard appropriate for entering digits, making it easier for the user to only enter numeric input.

Now, back in main.go, add the following method at the end of the file.

func (app *Application) processVerifyUserForm(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		log.Printf("Unable to retrieve form data, because %v\n", err)
		http.Error(w, "An OTP code was not supplied", http.StatusBadRequest)
		return
	}

	code := r.Form.Get("code")
	if code == "" {
		http.Error(w, "The code was not available", http.StatusBadRequest)
		return
	}

	params := &verify.UpdateFactorParams{}
	params.SetAuthPayload(code)

	resp, err := app.twilioRestClient.VerifyV2.UpdateFactor(
		app.verifyServiceSid,
		sessionManager.GetString(r.Context(), "seed"),
		sessionManager.GetString(r.Context(), "sid"),
		params,
	)

	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Could not update the Factor", http.StatusBadRequest)
		return
	} else {
		if resp.Status != nil {
			log.Println(resp.Status)
		}
	}

	if resp != nil && resp.Status != nil && *resp.Status == "verified" {
		log.Print("Factor setup complete.")
		sessionManager.Put(r.Context(), "flash", "Factor setup complete.")
		http.Redirect(w, r, "/token", http.StatusSeeOther)
	} else {
		log.Print("Factor setup incomplete.")
		http.Redirect(w, r, "/challenge", http.StatusSeeOther)
	}
}

The processVerifyUserForm() method retrieves the code that the user submitted from the request's POST data and attempts to verify the new Factor using it, along with the Factor's seed and SID retrieved from the current session.

If the Factor is successfully verified, a confirmation message is flashed into the current session, and the user is redirected to the route where they can enter QR codes generated by their application post-Factor creation, where the flashed message will be displayed, confirming that the Factor was successfully verified. Otherwise, they'll be redirected back to the "verify" TOTP Factor stage, to scan the QR code and try again.

Then, add the following to the import statement at the top of the file.

"github.com/skip2/go-qrcode"

Now, add the next two routes to the application's routing table, after the existing routes in main():

mux.HandleFunc("GET /challenge", app.displayVerifyUserForm)
mux.HandleFunc("POST /challenge", app.processVerifyUserForm)

These add the GET and POST forms of the "/challenge" route, handled by the displayVerifyUserForm() and processVerifyUserForm() methods, respectively.

Step 7: Add the ability to validate TOTP codes after Factor verification

Now, you'll add the third and final feature: the ability to validate TOTP codes, post-Factor verification. Start by adding the following two methods to the end of main.go.

func (app *Application) showQRCodeForm(w http.ResponseWriter, r *http.Request) {
	signInTmpl, err := template.ParseFiles("./templates/enter-code.tmpl")
	if err != nil {
		log.Print(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	type TemplateData struct {
		AlertType, FriendlyName, Identity, Message, Seed string
	}

	message := sessionManager.PopString(r.Context(), "flash")
	data := TemplateData{
		FriendlyName: sessionManager.GetString(r.Context(), "friendly_name"),
		Identity:     sessionManager.GetString(r.Context(), "sid"),
		Message:      message,
		Seed:         sessionManager.GetString(r.Context(), "seed"),
	}
	if message == "Verification success." || message == "Factor setup complete." {
		data.AlertType = "success"
	} else {
		data.AlertType = "error"
	}

	err = signInTmpl.Execute(w, data)
	if err != nil {
		log.Print(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
	}
}

func (app *Application) processQRCodeForm(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		log.Fatal(err)
	}

	code := r.Form.Get("code")

	params := &verify.CreateChallengeParams{}
	params.SetAuthPayload(code)
	params.SetFactorSid(sessionManager.GetString(r.Context(), "sid"))
	resp, err := app.twilioRestClient.VerifyV2.CreateChallenge(
		app.verifyServiceSid,
		sessionManager.GetString(r.Context(), "seed"),
		params,
	)
	if err != nil {
		log.Println(err.Error())
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	if resp.Status != nil {
		if *resp.Status == "approved" {
			sessionManager.Put(r.Context(), "flash", "Verification success.")
		} else {
			sessionManager.Put(r.Context(), "flash", "Verification failed.")
		}

		http.Redirect(w, r, "/token", http.StatusSeeOther)
	}
}

The first, showQRCodeForm(), renders an HTML form where the user can enter and submit a TOTP code generated by their authenticator app. The second, processQRCodeForm(), verifies the code with Twilio Verify.

If Twilio Verify marks the code as "approved", then a flash message confirming that is flashed. Otherwise, a message confirming that the code was invalid is flashed. The user is then redirected back to the TOTP code form, where the message will be displayed.

Now, in templates create a new file named enter-code.tmpl, and paste the code below into the file.

<!DOCTYPE html>
<html>
<head lang="en-AU">
    <title>Enter TOTP Code</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <link href="https://assets.twilio.com/public_assets/paste-fonts/1.5.2/fonts.css" rel="stylesheet">
    <link href="/static/css/styles.css" rel="stylesheet">
</head>
<body class="container">
    <main>
        <h1>Enter TOTP Code</h1>
        <p class="mt-60">
        This demo of the <a href="https://www.twilio.com/docs/verify/api">Twilio Verify API</a> shows how to set up two-factor authentication (2FA) with a third-party authenticator app like <a href="https://authy.com/download/">Authy</a> or Google Authenticator using the <a href="https://www.twilio.com/docs/glossary/totp">time-based one-time password (TOTP)</a> standard.
        </p>
        <hr class="mt-60">
        {{ if .Message }}
        <p class="mt-60 alert alert-{{ .AlertType }}">{{ .Message }}</p>
        {{ end }}
        <form action="/token" method="post" class="mt-60 mb-100">
            <label for="code">Enter the code generated by your authenticator app:</label>
            <div class="mt-20" style="display: flex; flex: auto; column-gap: 0.25rem; flex-shrink: 0;">
                <input
                    id="code"
                    inputmode="numeric"
                    name="code"
                    required
                    pattern="\d{6}"
                    placeholder="123456"
                    type="text"
                >
                <button type="submit" class="primary" style="flex-grow: 1;">Verify</button>
            </div>
        </form>
        <p>
            Demo is running for username '{{ .FriendlyName }}' with identity '{{ .Identity }}'.
            <a href="">Validate a code</a> or <a href="/">Reset</a>
        </p>
    </main>
</body>
</html>

Similar to the previous template, this one renders a form with a field where the user can input and submit the code which their authenticator app generates. It also provides a link to start over and create a new Factor.

With the template created, make the third and final update to the application's routing table, by adding the following code after the existing route definitions in the main() function.

mux.HandleFunc("GET /token", app.showQRCodeForm)
mux.HandleFunc("POST /token", app.processQRCodeForm)

Step 8: Download the application's CSS file

The last step in the process is to download the application's CSS file from the project's GitHub repository, to the project's public/css directory, naming it styles.css. Feel free to minimise the code using Minifier.org or a tool that you're more familiar with, if you prefer.

Test that the application works

Finally, it's time to test that the code works as expected. Start the application by running the following command.

go run main.go

Your server will start on port 8080, as shown by terminal output similar to the following.

2026/07/31 13:29:11 Server starting on :8080

You can now navigate to http://localhost:8080 to test the TOTP Factor creation process.

Webpage to set up two-factor authentication with Twilio Verify API using a username and TOTP factor.

Enter a username of your choice and click Set up two-factor authentication.

Screenshot of a web page showing QR code and instructions for setting up TOTP with Verify API.

You will be redirected to the Verify New TOTP Factor form. With your authentication app, scan the QR code, enter the 6-digit code that it generates into the form, and click Verify.

Webpage showing TOTP code entry for two-factor authentication with a verification button.

You'll then be redirected to the Enter TOTP Code form, where you'll see whether the Factor was set up successfully or not, as in the screenshot above.

Page showing TOTP code entry for Twilio Verify API with a success message and verification button.

If the Factor was set up successfully, enter the next 6-digit code into the Enter TOTP Code form and click Verify. Again, you should see if it was successfully verified or not, as in the screenshot above.

That's the essentials of implementing passwordless authentication in Go using Twilio Verify

TOTP-based passwordless authentication using Go and Twilio Verify presents a compelling alternative to traditional password-based systems.

It offers a blend of enhanced security and user convenience, making it an attractive option for modern applications. While passwordless auth introduces some extra complexity to your application, the benefits — especially regarding security and user experience — are worth the investment.

Matthew Setter is a PHP and Go Editor in the Twilio Voices team. He’s also the author of Mezzio Essentials and Deploy with Docker Compose . You can find him at msetter@twilio.com . He's also on LinkedIn and GitHub .

Otp icon created by Magnific on Flaticon.