This article will teach you how to secure a Golang API by implementing two-factor authentication (2FA) using TOTP codes generated by an authenticator app like Google Authenticator or Authy. There are different methods like SMS, Voice call, etc that we can use to deliver the one-time passcode (OTP) to the user but we will use an authenticator application to reduce the complexity of the project.

Cyber attacks on APIs have increased almost fourfold between January 2021 and October 2022 and it makes a lot of sense to add an extra layer of protection like Facial recognition, Biometrics, social login with Google, GitHub, or Facebook, Two-factor/multifactor Authentication, and many more to our APIs.

Let’s get to the main focus of this tutorial. We will be using the https://github.com/pquerna/otp/ package to implement the Time-based OTP (TOTP) in Golang. The API will run on a Gin Gonic framework and use GORM to store data in an SQLite database.

Related article:

More practice:

How to Implement Two-factor Authentication (2FA) in Golang

What is Two-Factor Authentication (2FA)

Two-Factor Authentication (2FA) is an electronic authentication method that requires two or more pieces of evidence before access to a website or application will be granted. The pieces of evidence include the traditional method of authentication like username/email and password – plus a one-time passcode sent to a mobile phone via SMS.

Many organizations use two-factor authentication (2FA) because it protects against cyber attacks that target user accounts and passwords. These security threats include phishing, brute-force attacks, credential exploitation, malware attack, etc.

The two-factor authentication (2FA) methods that businesses use include:

  • Voice-based authentication
  • SMS verification
  • Hardware tokens
  • Push notifications

Prerequisites

Before you start this tutorial, these are the prerequisites required to get the most out of this article.

  • Have the latest version of Golang installed on your system
  • Have basic knowledge of Golang
  • Have a basic understanding of API designs and CRUD patterns.
  • Familiarity with Gin Gonic and GORM will be beneficial.

Run the Golang 2FA App Locally

  1. If you don’t have Golang installed, visit https://go.dev/doc/install to download the right binary for your machine.
  2. Download or clone the Golang two-factor authentication source code from https://github.com/wpcodevo/two_factor_golang
  3. Run go mod tidy to install all the necessary packages
  4. Start the Gin Gonic HTTP server by running go run main.go
  5. Test the 2FA verification system with any API testing software.

Run the 2FA Frontend Built with React.js

  • Clone or download the React.js 2FA source code from https://github.com/wpcodevo/two_factor_reactjs.
  • Run yarn or yarn install to install all the required dependencies.
  • Start the Vite server by running yarn dev .
  • Navigate to http://localhost:3000 in a new tab to test the two-factor authentication system against the API.

Two-factor Authentication in Golang Overview

The Golang API will have the following endpoints:

METHODENDPOINTDESCRIPTION
POST/api/auth/registerCreates new user
POST/api/auth/loginLogin the user
POST/api/auth/otp/generateGenerate the TOTP Secret
POST/api/auth/otp/verifyVerify the TOTP token
POST/api/auth/otp/validateValidate the TOTP Token
POST/api/auth/otp/disableDisable the 2FA Feature

Despite having a frontend app to interact with the API, you can also use API testing software like Insomnia, Postman, VS Code Thunder Client extension, etc to test the endpoints defined on the Golang API.

Setup the 2FA feature

The React app has login and signup pages but I will skip the user registration and sign-in process to focus on the two-factor authentication. After signing into the app, you will be redirected to the profile page.

When the Setup 2FA button on the profile page is clicked, React will fire an Axios POST request to the /api/auth/otp/generate endpoint. The Golang API will then generate the one-time passcode (OTP) secrets, store the secrets in the database and return the OTP URL in addition to the base32 string to the frontend app or client.

reactjs setup 2fa with totp

Generate the QR Code

Upon receiving the OTP URL, React will generate the QR Code from the OTP URL and render a modal component to display it.

reactjs setup 2fa scan the qrcode

Enable the 2FA feature

To read the content of the QR Code, you can utilize the following mediums:

To demonstrate how the QR Code can be scanned, I will use the Authenticator extension in Chrome. If you have the Google Authenticator app, feel free to scan the QR Code with it.

Scan the QR Code with the authenticator extension in Chrome by clicking the scan icon adjacent to the edit icon and dragging the scanner over the QR Code.

The authenticator popup will close after you scan the QR Code. To view the TOTP token, open the authenticator extension again.

reactjs 2fa display the totp token with an authenticator app

Copy the OTP token and paste it into the input component on the popup. Click on the Verify & Activate button to submit the token to the Golang API. The Golang API will then verify the token, enable the 2FA feature, and return a response to the client or app.

Validate the TOTP token

Once the 2FA feature has been enabled, refresh the browser to log out of the app. When you sign into the app, you will be redirected to the two-factor authentication page where you will provide the OTP token generated by the authenticator app.

reactjs 2fa verify the totp token

When you click on the Authenticate button, a POST request will be fired to the /api/auth/otp/validate endpoint with the OTP token included in the request body.

The Golang API will verify the OTP token against the OTP secrets stored in the database and return a response to the app.

Disable the 2FA Feature

To disable the 2FA feature, click on the Disable 2FA button on the profile page. Clicking the Disable 2FA button will fire an Axios POST request to the /api/auth/otp/disable endpoint.

When you try logging into the app after the 2FA feature is disabled, React will take you to the profile page instead of going through the OTP verification page.

reactjs 2fa disable the feature

Step 1 – Setup the Golang Project

To begin, navigate to the location you would like to set up the project and create a directory with the name two_factor_golang. After the folder has been created, open it with your preferred IDE or text editor.


mkdir two_factor_golang && cd two_factor_golang && code .

Next, create the project module with this command:


go mod init github.com/:github_username/name_of_project

Now that you’ve initialized the Golang project, run this command to install the Gin Gonic package. Gin Gonic is an HTTP framework for building APIs in Golang.


go get -u github.com/gin-gonic/gin

With that out of the way, create a main.go file and add the following code snippets.

main.go


package main

import (
	"log"
	"net/http"

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

var (
	server *gin.Engine
)

func init() {
	server = gin.Default()
}

func main() {
	router := server.Group("/api")
	router.GET("/healthchecker", func(ctx *gin.Context) {
		message := "Welcome to Two-Factor Authentication with Golang"
		ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": message})
	})

	log.Fatal(server.Run(":8000"))
}


In the above code, we created a Gin Gonic server and added a health checker route to test the API. Start the Gin Gonic server by running go run main.go from the terminal in the root project directory.

Open a new tab in your browser and navigate to http://localhost:8000/api/healthchecker. You should see the JSON response sent by the Golang API.

golang test the API in the browser

Step 2 – Create the Database Model with GORM

This project will use GORM to store data in an SQLite database. What is GORM? GORM is a fantastic ORM library for Golang. It supports databases servers like:

  • PostgreSQL
  • MySQL
  • SQLite
  • SQL Server

That means you can easily adjust the code in this project to work with any GORM-supported database. I decided to use SQLite because it requires a few steps to set up.

To begin, install the following packages:


go get -u gorm.io/gorm gorm.io/driver/sqlite github.com/satori/go.uuid

  • gorm – ORM for Golang
  • sqlite – SQLite driver based on GGO
  • go.uuid – For generating UUIDs

With the packages installed, it’s now time to create the database model with GORM. To do that, create a models/user.model.go file and add the following code:

models/user.model.go


package models

import (
	uuid "github.com/satori/go.uuid"
	"gorm.io/gorm"
)

type User struct {
	ID       uuid.UUID `gorm:"type:uuid;primary_key;"`
	Name     string    `gorm:"type:varchar(255);not null"`
	Email    string    `gorm:"uniqueIndex;not null"`
	Password string    `gorm:"not null"`

	Otp_enabled  bool `gorm:"default:false;"`
	Otp_verified bool `gorm:"default:false;"`

	Otp_secret   string
	Otp_auth_url string
}

func (user *User) BeforeCreate(*gorm.DB) error {
	user.ID = uuid.NewV4()

	return nil
}

type RegisterUserInput struct {
	Name     string `json:"name" binding:"required"`
	Email    string `json:"email" bindinig:"required"`
	Password string `json:"password" binding:"required"`
}

type LoginUserInput struct {
	Email    string `json:"email" bindinig:"required"`
	Password string `json:"password" binding:"required"`
}

type OTPInput struct {
	UserId string `json:"user_id"`
	Token  string `json:"token"`
}


Quite a lot happening in the above, let’s break it down:

  • We created a User struct that will be used by GORM to generate the underlying SQL table.
  • Next, we used GORM’s BeforeCreate hook to assign a UUID to the ID column before the data gets stored in the database.
  • Lastly, we created a couple of structs that Gin Gonic will use to validate the request payloads.

With the above explanation, replace the content of the main.go file with the following code:

main.go


package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/two_factor_golang/models"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

var (
	DB     *gorm.DB
	server *gin.Engine
)

func init() {
	var err error
	DB, err = gorm.Open(sqlite.Open("golang.db"), &gorm.Config{})
	DB.AutoMigrate(&models.User{})

	if err != nil {
		log.Fatal("Failed to connect to the Database")
	}
	fmt.Println("? Connected Successfully to the Database")

	server = gin.Default()
}

func main() {
	router := server.Group("/api")
	router.GET("/healthchecker", func(ctx *gin.Context) {
		message := "Welcome to Two-Factor Authentication with Golang"
		ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": message})
	})

	log.Fatal(server.Run(":8000"))
}


In the above, we created an init function to automatically create the SQLite database and migrate the model we defined above.

Now execute the main.go by running go run main.go . The init function will be executed first to create the SQLite database and migrate the model to the database. You should see a golang.db SQLite file created in the root project directory.

After the init function has finished executing, the main function will be evoked to start the Gin Gonic HTTP server.

Step 3 – Create the Golang Route Handlers

In this section, you will create six Gin Gonic Route handlers to handle the 2FA authentication. To begin, install the TOTP library for Go. The https://github.com/pquerna/otp package will enable us to easily add TOTP authentication to the Golang API.


go get github.com/pquerna/otp

After that, create a controllers/auth.controller.go file and add the following code.

controllers/auth.controller.go


package controllers

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/two_factor_golang/models"
	"gorm.io/gorm"

	"github.com/pquerna/otp/totp"
)

type AuthController struct {
	DB *gorm.DB
}

func NewAuthController(DB *gorm.DB) AuthController {
	return AuthController{DB}
}

Register User Handler

The first step in the two-factor verification system is user registration. We will create a route handler that Gin Gonic will evoke to register the user.

controllers/auth.controller.go


// [...] Register User
func (ac *AuthController) SignUpUser(ctx *gin.Context) {
	var payload *models.RegisterUserInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	newUser := models.User{
		Name:     payload.Name,
		Email:    strings.ToLower(payload.Email),
		Password: payload.Password,
	}

	result := ac.DB.Create(&newUser)

	if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {
		ctx.JSON(http.StatusConflict, gin.H{"status": "fail", "message": "Email already exist, please use another email address"})
		return
	} else if result.Error != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": result.Error.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"status": "success", "message": "Registered successfully, please login"})
}

Let’s evaluate the above code:

  • First, we passed the RegisterUserInput struct to the .ShouldBindJSON() method provided by Gin Gonic to validate the request body during the account registration process.
  • Next, we initialized the models.User{} struct and called GORM’s .Create() method to add the new user to the database. Since we added a unique constraint on the email column, we used an if...else statement to handle any possible errors returned by the database.
  • Lastly, we returned a JSON response to the client.

To register a user, you need to make a POST request with the necessary credentials to the /api/auth/register endpoint. When the request hits the Golang API, the SignUpUser route handler will be evoked to register the user.

golang 2fa register user

Login User Handler

Here, you will create a route handler that Gin Gonic will call to log the user into the API. To reduce the steps involved in the authentication flow, we will ignore other authentication methods and return the user’s data to the client.

The API will assume the user is authenticated when the user_id is provided in the request body.

controllers/auth.controller.go


// [...] Register User

// [...] Login User
func (ac *AuthController) LoginUser(ctx *gin.Context) {
	var payload *models.LoginUserInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	var user models.User
	result := ac.DB.First(&user, "email = ?", strings.ToLower(payload.Email))
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or Password"})
		return
	}

	userResponse := gin.H{
		"id":          user.ID.String(),
		"name":        user.Name,
		"email":       user.Email,
		"otp_enabled": user.Otp_enabled,
	}
	ctx.JSON(http.StatusOK, gin.H{"status": "success", "user": userResponse})
}

In the above code, we validated the request payload against the LoginUserInput struct and called GORM’s .First() method to check the database if a user with that email exists.

After that, we returned the found record to the client in JSON format. To log into the API, make a POST request with the required credentials to the /api/auth/login endpoint.

golang 2fa login user

Generate the TOTP token

It’s now time to create the handler that will be evoked to generate the OTP secret key. The secret key will link the server and the application that will generate the OTP tokens.

controllers/auth.controller.go


// [...] Register User

// [...] Login User

// [...] Generate TOTP
func (ac *AuthController) GenerateOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      "codevoweb.com",
		AccountName: "admin@admin.com",
		SecretSize:  15,
	})

	if err != nil {
		panic(err)
	}

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or Password"})
		return
	}

	dataToUpdate := models.User{
		Otp_secret:   key.Secret(),
		Otp_auth_url: key.URL(),
	}

	ac.DB.Model(&user).Updates(dataToUpdate)

	otpResponse := gin.H{
		"base32":      key.Secret(),
		"otpauth_url": key.URL(),
	}
	ctx.JSON(http.StatusOK, otpResponse)
}

To generate the secret key, we will use the totp.Generate() method provided by the https://github.com/pquerna/otp package. This method will return an object that has the secret key in base32, and URL format. The URL-encoded secret key will be sent to the frontend app to generate the QR Code.

After the OTP secret key has been generated, we will call GORM’s .Updates() method to add the secret key to the user’s data in the database.

Now generate the OTP secret key by making a POST request to the /api/auth/otp/generate endpoint with the user_id provided in the request body.

golang 2fa generate the otp token

Open your authenticator app and enter the base32 encoded key to view the OTP token.

display the totp token by using the base32 string

Verify the TOTP token

You should see the OTP token after adding the secret key to your authenticator app.

display the totp token with chrome authenticator

Now that we’ve been able to display the TOTP tokens, let’s create a route handler to verify them. This handler will verify the TOTP token and update the otp_enabled column in the “users” table to true.

controllers/auth.controller.go


// [...] Register User

// [...] Login User

// [...] Generate TOTP

// [...] Verify the OTP
func (ac *AuthController) VerifyOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	message := "Token is invalid or user doesn't exist"

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	valid := totp.Validate(payload.Token, user.Otp_secret)
	if !valid {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	dataToUpdate := models.User{
		Otp_enabled:  true,
		Otp_verified: true,
	}

	ac.DB.Model(&user).Updates(dataToUpdate)

	userResponse := gin.H{
		"id":          user.ID.String(),
		"name":        user.Name,
		"email":       user.Email,
		"otp_enabled": user.Otp_enabled,
	}
	ctx.JSON(http.StatusOK, gin.H{"otp_verified": true, "user": userResponse})
}

We extracted the token from the request payload and called the totp.Validate() method to verify the token against the base32-encoded secret key stored in the database.

Once the OTP token is valid, we’ll enable the 2FA feature and update the Otp_verified column to true. To verify the OTP token generated by the authenticator app, make a POST request with the token and user_id included in the request body to the /api/auth/otp/verify .

golang 2fa verify the totp token

Validate the TOTP token

Similar to the OTP verification handler, this handle will also utilize the totp.Validate() method but it won’t update the user’s information in the database.

This handler will only verify the OTP token and return a validation response to the client.

controllers/auth.controller.go


// [...] Register User

// [...] Login User

// [...] Generate TOTP

// [...] Verify the OTP

// [...] Validate the TOTP
func (ac *AuthController) ValidateOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	message := "Token is invalid or user doesn't exist"

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	valid := totp.Validate(payload.Token, user.Otp_secret)
	if !valid {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"otp_valid": true})
}

To validate the OTP token, make a POST request with the user_id and token to the /api/auth/otp/validate endpoint.

golang api 2fa validate totp token

Disable the TOTP Feature

The final step is to create a route handler that will be called to disable the 2FA feature. This handler will update the otp_enabled column in the “users” table to false.

controllers/auth.controller.go


// [...] Register User

// [...] Login User

// [...] Generate TOTP

// [...] Verify the OTP

// [...] Validate the TOTP

// [...] Disable the TOTP
func (ac *AuthController) DisableOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "User doesn't exist"})
		return
	}

	user.Otp_enabled = false
	ac.DB.Save(&user)

	userResponse := gin.H{
		"id":          user.ID.String(),
		"name":        user.Name,
		"email":       user.Email,
		"otp_enabled": user.Otp_enabled,
	}
	ctx.JSON(http.StatusOK, gin.H{"otp_disabled": true, "user": userResponse})
}


You can disable the 2FA feature by making a POST request with the user_id included in the request body to the /api/auth/otp/disable endpoint.

golang api 2fa disable the totp feature

Final Code of the Route Controllers

controllers/auth.controller.go


package controllers

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/two_factor_golang/models"
	"gorm.io/gorm"

	"github.com/pquerna/otp/totp"
)

type AuthController struct {
	DB *gorm.DB
}

func NewAuthController(DB *gorm.DB) AuthController {
	return AuthController{DB}
}

func (ac *AuthController) SignUpUser(ctx *gin.Context) {
	var payload *models.RegisterUserInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	newUser := models.User{
		Name:     payload.Name,
		Email:    strings.ToLower(payload.Email),
		Password: payload.Password,
	}

	result := ac.DB.Create(&newUser)

	if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {
		ctx.JSON(http.StatusConflict, gin.H{"status": "fail", "message": "Email already exist, please use another email address"})
		return
	} else if result.Error != nil {
		ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": result.Error.Error()})
		return
	}

	ctx.JSON(http.StatusCreated, gin.H{"status": "success", "message": "Registered successfully, please login"})
}

func (ac *AuthController) LoginUser(ctx *gin.Context) {
	var payload *models.LoginUserInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	var user models.User
	result := ac.DB.First(&user, "email = ?", strings.ToLower(payload.Email))
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or Password"})
		return
	}

	userResponse := gin.H{
		"id":          user.ID.String(),
		"name":        user.Name,
		"email":       user.Email,
		"otp_enabled": user.Otp_enabled,
	}
	ctx.JSON(http.StatusOK, gin.H{"status": "success", "user": userResponse})
}

func (ac *AuthController) GenerateOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      "codevoweb.com",
		AccountName: "admin@admin.com",
		SecretSize:  15,
	})

	if err != nil {
		panic(err)
	}

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Invalid email or Password"})
		return
	}

	dataToUpdate := models.User{
		Otp_secret:   key.Secret(),
		Otp_auth_url: key.URL(),
	}

	ac.DB.Model(&user).Updates(dataToUpdate)

	otpResponse := gin.H{
		"base32":      key.Secret(),
		"otpauth_url": key.URL(),
	}
	ctx.JSON(http.StatusOK, otpResponse)
}

func (ac *AuthController) VerifyOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	message := "Token is invalid or user doesn't exist"

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	valid := totp.Validate(payload.Token, user.Otp_secret)
	if !valid {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	dataToUpdate := models.User{
		Otp_enabled:  true,
		Otp_verified: true,
	}

	ac.DB.Model(&user).Updates(dataToUpdate)

	userResponse := gin.H{
		"id":          user.ID.String(),
		"name":        user.Name,
		"email":       user.Email,
		"otp_enabled": user.Otp_enabled,
	}
	ctx.JSON(http.StatusOK, gin.H{"otp_verified": true, "user": userResponse})
}

func (ac *AuthController) ValidateOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	message := "Token is invalid or user doesn't exist"

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	valid := totp.Validate(payload.Token, user.Otp_secret)
	if !valid {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": message})
		return
	}

	ctx.JSON(http.StatusOK, gin.H{"otp_valid": true})
}

func (ac *AuthController) DisableOTP(ctx *gin.Context) {
	var payload *models.OTPInput

	if err := ctx.ShouldBindJSON(&payload); err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})
		return
	}

	var user models.User
	result := ac.DB.First(&user, "id = ?", payload.UserId)
	if result.Error != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "User doesn't exist"})
		return
	}

	user.Otp_enabled = false
	ac.DB.Save(&user)

	userResponse := gin.H{
		"id":          user.ID.String(),
		"name":        user.Name,
		"email":       user.Email,
		"otp_enabled": user.Otp_enabled,
	}
	ctx.JSON(http.StatusOK, gin.H{"otp_disabled": true, "user": userResponse})
}


Step 4 – Create the Gin Gonic API Routes

We are now ready to create the Gin Gonic routes to evoke the route handlers. Create a routes/auth.routes.go file and add the following Golang code to define the API routes.

routes/auth.routes.go


package routes

import (
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/two_factor_golang/controllers"
)

type AuthRouteController struct {
	authController controllers.AuthController
}

func NewAuthRouteController(authController controllers.AuthController) AuthRouteController {
	return AuthRouteController{authController}
}

func (rc *AuthRouteController) AuthRoute(rg *gin.RouterGroup) {
	router := rg.Group("auth")

	router.POST("/register", rc.authController.SignUpUser)
	router.POST("/login", rc.authController.LoginUser)
	router.POST("/otp/generate", rc.authController.GenerateOTP)
	router.POST("/otp/verify", rc.authController.VerifyOTP)
	router.POST("/otp/validate", rc.authController.ValidateOTP)
	router.POST("/otp/disable", rc.authController.DisableOTP)
}


Step 5 – Add Routes to Gin Gonic Middleware

Now that we have the routes defined, let’s add the router to the Gin Gonic middleware pipeline. Before that, install the CORS package that we’ll use to configure the Gin Gonic server to accept cross-origin requests.


go get github.com/gin-contrib/cors

Next, replace the content of the main.go file with the following code:

main.go


package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"github.com/wpcodevo/two_factor_golang/controllers"
	"github.com/wpcodevo/two_factor_golang/models"
	"github.com/wpcodevo/two_factor_golang/routes"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

var (
	DB     *gorm.DB
	server *gin.Engine

	AuthController      controllers.AuthController
	AuthRouteController routes.AuthRouteController
)

func init() {
	var err error
	DB, err = gorm.Open(sqlite.Open("golang.db"), &gorm.Config{})
	DB.AutoMigrate(&models.User{})

	if err != nil {
		log.Fatal("Failed to connect to the Database")
	}
	fmt.Println("? Connected Successfully to the Database")

	AuthController = controllers.NewAuthController(DB)
	AuthRouteController = routes.NewAuthRouteController(AuthController)

	server = gin.Default()
}

func main() {
	corsConfig := cors.DefaultConfig()
	corsConfig.AllowOrigins = []string{"http://localhost:3000"}
	corsConfig.AllowCredentials = true

	server.Use(cors.New(corsConfig))

	router := server.Group("/api")
	router.GET("/healthchecker", func(ctx *gin.Context) {
		message := "Welcome to Two-Factor Authentication with Golang"
		ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": message})
	})

	AuthRouteController.AuthRoute(router)
	log.Fatal(server.Run(":8000"))
}


Start the Gin HTTP server by running go run main.go .

Conclusion

In this article, you learned how to implement two-factor authentication in Golang using the https://github.com/pquerna/otp package. You also learned how to generate the OTP tokens with Chrome’s Authenticator extension.

You can find the React.js and Golang source code on this GitHub repository: