Mobile OTP based authentication in golang
Tags: go, mongodb, gofiber, twilio
Published: 2023-08-05 20:47:36

In this post, we will develop a mobile OTP-based authentication API in Golang.

### Tech stack


**Golang**

Go (or Golang) is an open-source programming language developed by Google. It emphasizes simplicity, efficiency, and concurrency support. Go is widely used for web development, system programming, and cloud-native applications.

https://go.dev/


**Gofiber**

GoFiber is a lightweight and fast web framework written in Go (Golang). It is built on top of Fasthttp, making it an efficient choice for building high-performance APIs and web applications. GoFiber offers features like middleware support, routing, and context handling.

https://gofiber.io/


**MongoDB**

MongoDB is a NoSQL, document-oriented database, known for its flexibility and scalability. It stores data in JSON-like BSON format, enabling easy manipulation and indexing. MongoDB is widely used for handling large-scale, unstructured, and real-time data in modern applications.

https://www.mongodb.com/


**Twilio**

Twilio is a cloud communications platform that enables developers to integrate messaging, voice, and video functionalities into their applications using APIs. It simplifies the process of building communication features such as SMS, MMS, phone calls, and more, allowing businesses to engage with their customers programmatically.

https://www.twilio.com/


Create a new directory `auth` and init go modules inside the project.

```sh
go mod init auth
```

Install required packages
```sh
go get github.com/gofiber/fiber/v2
go get github.com/golang-jwt/jwt/v5
go get github.com/joho/godotenv
go get github.com/twilio/twilio-go
go get go.mongodb.org/mongo-driver/mongo
```

Create folder structure for our project
```
Project
├── README.md
├── config
│   └── config.go
├── database
│   ├── connection.go
│   └── database.go
├── go.mod
├── go.sum
├── handler
│   └── auth.go
├── main.go
├── middleware
│   └── auth.go
├── model
│   └── user.go
├── router
│   └── router.go
├── schema
│   ├── auth.go
│   └── response.go
└── util
    ├── twilio.go
    └── user.go
```


- `config` folder contains configurations i.e. env variable configuration.

- `database` folder contains database connections and types related to db i.e. mongodb driver setup.

- `handler` folder contains route handler i.e. register handler, login handler etc...

- `middleware` folder contains route middleware that runs before or after any handler executes i.e. auth jwt validation middleware.

- `model` contains MongoDB schema i.e. User schema.

- `router` contains route initialization and maps API routes to specific route handlers i.e. auth routes.

- `schema` contains application schema i.e. incoming request body schema.

- `util` contains reusable database query and application helper function i.e. Find user by phone, send sms etc...

- `.env` contains environment variables i.e. Twilio api-key, mongodb-uri etc...

- copy all `.env.example` and paste it inside `.env`.

- `main.go` is the entry point of the Go application.


Next, I will show you the required routes for this project.
```
- /api/auth
    - /register
    - /login
    - /verify_otp
    - /resend_otp
    - /me
```

`main.go`
```go
package main

import (
	"auth/database"
	"auth/router"
	"log"
)

func main() {
	app := router.New()
	err := database.Connect()
	if err != nil {
		panic(err)
	}
	log.Fatal(app.Listen(":3000"))
}

```

If you are familiar with `Express` then you will find `gofiber` syntax is similar to `Express` (Node web framework).

Here we have done 3 things:-

- Initialized our router.

- Connected with MongoDB  database.

- And started our server to listen on port 3000 


`database/database.go`
```go
package database

import "go.mongodb.org/mongo-driver/mongo"

const Users = "users"

type MongoInstance struct {
	Client *mongo.Client
	Db     *mongo.Database
}
```

Users const store collection name so that we can reuse this variable where we required users collection for query users data.

We have also created one struct to store MongoDB client and database pointers.



`database/connection.go`

```go
package database

import (
	"auth/config"
	"context"
	"log"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

var Mg MongoInstance

func Connect() error {
	dbName := config.Config("DATABASE_NAME")
	uri := config.Config("DATABASE_URI") + dbName
	client, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
	if err != nil {
		return err
	}
	log.Printf("Connected with databse %s", dbName)
	db := client.Database(dbName)
	Mg = MongoInstance{
		Client: client,
		Db:     db,
	}

	return nil
}


```
We have defined package level variable Mg and store database info here so that we can use this variable to query our database.

`DATABASE_NAME` and `DATABASE_URI` is an environment variable whose value is loaded from the `.env` file.

The context package is commonly used in Go to pass context information between different function calls in a chain or across concurrent goroutines. This package provides the ability to propagate cancellation signals and deadlines through the call stack.

The `context.TODO()` function in Go's context package is used to create a new, empty context when you don't have a more specific context available or when you are not sure which context to use.


`config/config.go`

```go
package config

import (
	"fmt"
	"os"

	"github.com/joho/godotenv"
)

// Config func to get env value
func Config(key string) string {
	// load .env file
	err := godotenv.Load(".env")
	if err != nil {
		fmt.Print("Error loading .env file")
	}
	return os.Getenv(key)
}
```
Here we have used the github.com/joho/godotenv package to load the env variables from the `.env file`. 

`router/router.go`
```go
package router

import (
	"auth/handler"
	"auth/middleware"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/compress"
	"github.com/gofiber/fiber/v2/middleware/cors"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/gofiber/fiber/v2/middleware/monitor"
)

func New() *fiber.App {
	app := fiber.New()
	api := app.Group("/api")
	auth := api.Group("/auth")

	auth.Post("/register", handler.Register)
	auth.Post("/login", handler.Login)
	auth.Post("/verify_otp", handler.VerifyOTP)
	auth.Post("/resend_otp", handler.ResendOTP)
	auth.Get("/me", middleware.Protected(), handler.GetCurrentUser)

	return app
}
```

We have initialized fiber and added Groups for `api` and `auth` so that we do need to type each path again and again. It also provides a better way to apply middleware to a group of routes instead of applying the same middleware to each route one by one.

`model/user.go`

```go

package model

import "go.mongodb.org/mongo-driver/bson/primitive"

type User struct {
	ID    primitive.ObjectID `json:"id" bson:"_id"`
	Name  string             `json:"name"`
	Phone string             `json:"phone"`
	Otp   string             `json:"otp,omitempty"`
}
```

We have created a User model to store user data consistently in MongoDB database.

`schema/response.go`

```go
package schema

type ResponseHTTP struct {
	Success bool   `json:"success"`
	Data    any    `json:"data"`
	Message string `json:"message"`
}

```

We will use this schema to send HTTP responses in a consistent way.


`schema/auth.go`

```go
package schema

type RegisterBody struct {
	Name  string `json:"name"`
	Phone string `json:"phone"`
}

type LoginSchema struct {
	Phone string `json:"phone"`
}

type VerifyOTPSchema struct {
	Phone string `json:"phone"`
	Otp   string `json:"otp"`
}

```

We will use this schema to parse the incoming request body  data.

`handler/auth.go`

```go
package handler

import (
	"auth/model"
	"auth/schema"
	"auth/util"

	"github.com/gofiber/fiber/v2"
)
```

We have added the required package imports later we will add many utility functions in `util`.


```go
// ...
// ...
func Register(c *fiber.Ctx) error {
	// request body data
	body := new(schema.RegisterBody)
	if err := c.BodyParser(body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	// validate duplicate mobile number

	user, err := util.FindUserByPhone(body.Phone)

	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	if user != nil {
		return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: "Phone number already in use",
		})
	}

	// create new user

	id, err := util.InsertUser(body)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	return c.Status(fiber.StatusCreated).JSON(schema.ResponseHTTP{
		Success: true,
		Data: fiber.Map{
			"id": id,
		},
		Message: "Account registered successfully",
	})
}
```


Here we have added a login handler it will send OTP to a mobile number if  the specific user is already registered.

- First, it will parse request body data and store it in the body.

- Next, it will verify the user from the database using the mobile number.

- Generate OTP and update otp field  of users collection in  the database.

- And send generated OTP to the user's mobile number.

```go
func VerifyOTP(c *fiber.Ctx) error {
	// request body data
	body := new(schema.VerifyOTPSchema)
	if err := c.BodyParser(body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	// find phone in database
	user, err := util.FindUserByPhone(body.Phone)

	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	if user == nil {
		return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: "Phone number not exists",
		})
	}

	if user.Otp != body.Otp {
		return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: "Incorrect Otp",
		})
	}

	// generate jwt token
	token, err := util.GenerateJWT(user.ID.Hex())
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	// remove old otp from db
	util.UpdateUser(user.ID, map[string]any{
		"otp": "",
	})

	return c.Status(fiber.StatusCreated).JSON(schema.ResponseHTTP{
		Success: true,
		Data: fiber.Map{
			"token": "Bearer " + token,
		},
		Message: "Account login successfully",
	})
}
```

This handler will verify the user's OTP and return  the JWT Bearer token in response if OTP will be correct.

- First, it will parse request body data and store it in the body.

- Verify provided phone number.

- Verify provided OTP.

- Create a JWT token with userId as a payload.

- Remove old OTP from the user collection.


```go
func ResendOTP(c *fiber.Ctx) error {
	// request body data
	body := new(schema.VerifyOTPSchema)
	if err := c.Status(fiber.StatusBadRequest).BodyParser(body); err != nil {
		return c.JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	// find phone in database
	user, err := util.FindUserByPhone(body.Phone)

	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	if user == nil {
		return c.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: "Phone number not exists",
		})
	}

	otp := util.GenerateRandomNumber()

	// save otp in database
	util.UpdateUser(user.ID, map[string]any{
		"otp": otp,
	})
	// send otp to user phone

	err = util.SendOTP(user.Phone, otp)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{
			Success: false,
			Data:    nil,
			Message: err.Error(),
		})
	}

	return c.Status(fiber.StatusCreated).JSON(schema.ResponseHTTP{
		Success: true,
		Data:    nil,
		Message: "Sent otp to registered mobile number",
	})
}

```


This handler will handle resend of OTP to the specific mobile number.


```go

func GetCurrentUser(c *fiber.Ctx) error {
	user := c.Locals("user").(*model.User)
	user.Otp = ""
	return c.Status(fiber.StatusOK).JSON(schema.ResponseHTTP{
		Success: true,
		Data:    user,
		Message: "Get current user",
	})
}
```

This handler will return the currently logged-in user. We have added user  value in locals from auth middleware. We removed the OTP value so that the user can't get otp in the response.

`util/user.go`

```go
package util

import (
	"auth/config"
	"auth/database"
	"auth/model"
	"auth/schema"
	"context"
	"math/rand"
	"strconv"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
)

func FindUserByPhone(phone string) (*model.User, error) {
	// Create a context and a collection instance
	ctx := context.TODO()
	collection := database.Mg.Db.Collection(database.Users)

	// Create a filter to find the user by phone number
	filter := bson.M{"phone": phone}

	// Create a variable to store the result
	var result model.User

	// Find the user with the given phone number
	err := collection.FindOne(ctx, filter).Decode(&result)

	if err != nil {
		if err == mongo.ErrNoDocuments {
			// If the error is ErrNoDocuments, it means no user was found
			return nil, nil
		}
		// Handle other potential errors
		return nil, err
	}

	return &result, nil
}

func InsertUser(user *schema.RegisterBody) (any, error) {
	// Create a context and a collection instance
	ctx := context.TODO()
	collection := database.Mg.Db.Collection(database.Users)

	// Insert the user into the collection
	result, err := collection.InsertOne(ctx, user)
	return result.InsertedID, err
}

func UpdateUser(userID primitive.ObjectID, updatedFields map[string]any) error {
	// Create a context and a collection instance
	ctx := context.TODO()
	collection := database.Mg.Db.Collection(database.Users)

	// Create a filter to find the user by ID
	filter := bson.M{"_id": userID}

	// Create an update with the provided fields
	update := bson.M{"$set": updatedFields}

	// Update the user document in the collection
	_, err := collection.UpdateOne(ctx, filter, update)
	return err

}

func FindUserById(userId string) (*model.User, error) {
	// Create a context and a collection instance
	id, err := primitive.ObjectIDFromHex(userId)
	if err != nil {
		return nil, err
	}
	ctx := context.TODO()
	collection := database.Mg.Db.Collection(database.Users)

	// Create a filter to find the user by phone number
	filter := bson.M{"_id": id}

	// Create a variable to store the result
	var result model.User

	// Find the user with the given phone number
	err = collection.FindOne(ctx, filter).Decode(&result)

	if err != nil {
		if err == mongo.ErrNoDocuments {
			// If the error is ErrNoDocuments, it means no user was found
			return nil, nil
		}
		// Handle other potential errors
		return nil, err
	}

	return &result, nil
}

func GenerateRandomNumber() string {
	// Generate a random number between 1000 and 9999 (inclusive)
	num := rand.Intn(9000) + 1000
	return strconv.Itoa(num)
}

func GenerateJWT(id string) (string, error) {
	token := jwt.New(jwt.SigningMethodHS256)
	claims := token.Claims.(jwt.MapClaims)
	claims["userId"] = id
	claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
	return token.SignedString([]byte(config.Config("SECRET")))

}

```
Here we have added all utility helper function to query our database and auth-related helper functions.


`util/twilio.go`

```go

package util

import (
	"auth/config"
	"fmt"
	"log"

	"github.com/twilio/twilio-go"

	openapi "github.com/twilio/twilio-go/rest/api/v2010"
)

func SendOTP(to string, otp string) error {
	accountSid := config.Config("TWILIO_ACCOUNT_SID")
	authToken := config.Config("TWILIO_AUTH_TOKEN")

	client := twilio.NewRestClientWithParams(twilio.ClientParams{
		Username: accountSid,
		Password: authToken,
	})

	params := &openapi.CreateMessageParams{}

	params.SetTo(to)
	params.SetFrom(config.Config("TWILIO_PHONE_NUMBER"))

	msg := fmt.Sprintf("Your OTP is %s", otp)
	params.SetBody(msg)

	_, err := client.Api.CreateMessage(params)
	if err != nil {
		log.Println(err.Error())
		return err
	}
	log.Println("SMS sent successfully!")

	return nil
}
```
Here we have used the Twilio sms service to send OTP to mobile numbers. You need to create an account in Twilio to use their services.

You can generate your virtual mobile number from the Twilio dashboard and also copy the account sid and auth token from the dashboard.

`middleware/auth.go`

```go
package middleware

import (
	"auth/config"
	"auth/schema"
	"auth/util"

	jwtware "github.com/gofiber/contrib/jwt"
	"github.com/gofiber/fiber/v2"
	"github.com/golang-jwt/jwt/v5"
)

// Protected protect routes
func Protected() fiber.Handler {
	return jwtware.New(jwtware.Config{
		SigningKey:     jwtware.SigningKey{Key: []byte(config.Config("SECRET"))},
		ErrorHandler:   jwtError,
		SuccessHandler: jwtSuccess,
		ContextKey:     "payload",
	})
}

func jwtSuccess(c *fiber.Ctx) error {
	payload := c.Locals("payload").(*jwt.Token)
	claims := payload.Claims.(jwt.MapClaims)
	userId := claims["userId"].(string)
	user, err := util.FindUserById(userId)
	if err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(schema.ResponseHTTP{
			Success: false,
			Message: "User not exists",
			Data:    nil,
		})
	}
	c.Locals("user", user)
	return c.Next()
}
func jwtError(c *fiber.Ctx, err error) error {
	if err.Error() == "Missing or malformed JWT" {
		return c.Status(fiber.StatusBadRequest).
			JSON(schema.ResponseHTTP{Success: false, Message: "Missing or malformed JWT", Data: nil})
	}
	return c.Status(fiber.StatusUnauthorized).
		JSON(schema.ResponseHTTP{Success: false, Message: "Invalid or expired JWT", Data: nil})
}
```
Protected middleware will do the following thing:-

- Verify and parse the JWT token and get a payload from it.

- Handle error using jwtError handler function in case of wrong or expired JWT token.

- If there is no error then it will execute jwtSuccess.

- We find the user by userId.

- Add user info in locals to access from any hander.

- c.Next() will execute the next handler function.

By default `ContextKey` was `user` but we have changed it to `payload` .




You can explore complete codebase in github

{% embed https://github.com/harshmangalam/golang-mobile-otp-auth %}