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.
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.
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.
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.
Create a new directory auth and init go modules inside the project.
go mod init auth
Install required packages
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
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
packagedatabaseimport("auth/config""context""log""go.mongodb.org/mongo-driver/mongo""go.mongodb.org/mongo-driver/mongo/options")varMgMongoInstancefuncConnect()error{dbName:=config.Config("DATABASE_NAME")uri:=config.Config("DATABASE_URI")+dbNameclient,err:=mongo.Connect(context.TODO(),options.Client().ApplyURI(uri))iferr!=nil{returnerr}log.Printf("Connected with databse %s",dbName)db:=client.Database(dbName)Mg=MongoInstance{Client:client,Db:db,}returnnil}
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
packageconfigimport("fmt""os""github.com/joho/godotenv")// Config func to get env valuefuncConfig(keystring)string{// load .env fileerr:=godotenv.Load(".env")iferr!=nil{fmt.Print("Error loading .env file")}returnos.Getenv(key)}
Here we have used the github.com/joho/godotenv package to load the env variables from the .env file.
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.
We have added the required package imports later we will add many utility functions in util.
// ...// ...funcRegister(c*fiber.Ctx)error{// request body databody:=new(schema.RegisterBody)iferr:=c.BodyParser(body);err!=nil{returnc.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}// validate duplicate mobile numberuser,err:=util.FindUserByPhone(body.Phone)iferr!=nil{returnc.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}ifuser!=nil{returnc.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:"Phone number already in use",})}// create new userid,err:=util.InsertUser(body)iferr!=nil{returnc.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}returnc.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.
funcVerifyOTP(c*fiber.Ctx)error{// request body databody:=new(schema.VerifyOTPSchema)iferr:=c.BodyParser(body);err!=nil{returnc.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}// find phone in databaseuser,err:=util.FindUserByPhone(body.Phone)iferr!=nil{returnc.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}ifuser==nil{returnc.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:"Phone number not exists",})}ifuser.Otp!=body.Otp{returnc.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:"Incorrect Otp",})}// generate jwt tokentoken,err:=util.GenerateJWT(user.ID.Hex())iferr!=nil{returnc.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}// remove old otp from dbutil.UpdateUser(user.ID,map[string]any{"otp":"",})returnc.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.
funcResendOTP(c*fiber.Ctx)error{// request body databody:=new(schema.VerifyOTPSchema)iferr:=c.Status(fiber.StatusBadRequest).BodyParser(body);err!=nil{returnc.JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}// find phone in databaseuser,err:=util.FindUserByPhone(body.Phone)iferr!=nil{returnc.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}ifuser==nil{returnc.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:"Phone number not exists",})}otp:=util.GenerateRandomNumber()// save otp in databaseutil.UpdateUser(user.ID,map[string]any{"otp":otp,})// send otp to user phoneerr=util.SendOTP(user.Phone,otp)iferr!=nil{returnc.Status(fiber.StatusInternalServerError).JSON(schema.ResponseHTTP{Success:false,Data:nil,Message:err.Error(),})}returnc.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.
funcGetCurrentUser(c*fiber.Ctx)error{user:=c.Locals("user").(*model.User)user.Otp=""returnc.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
packageutilimport("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")funcFindUserByPhone(phonestring)(*model.User,error){// Create a context and a collection instancectx:=context.TODO()collection:=database.Mg.Db.Collection(database.Users)// Create a filter to find the user by phone numberfilter:=bson.M{"phone":phone}// Create a variable to store the resultvarresultmodel.User// Find the user with the given phone numbererr:=collection.FindOne(ctx,filter).Decode(&result)iferr!=nil{iferr==mongo.ErrNoDocuments{// If the error is ErrNoDocuments, it means no user was foundreturnnil,nil}// Handle other potential errorsreturnnil,err}return&result,nil}funcInsertUser(user*schema.RegisterBody)(any,error){// Create a context and a collection instancectx:=context.TODO()collection:=database.Mg.Db.Collection(database.Users)// Insert the user into the collectionresult,err:=collection.InsertOne(ctx,user)returnresult.InsertedID,err}funcUpdateUser(userIDprimitive.ObjectID,updatedFieldsmap[string]any)error{// Create a context and a collection instancectx:=context.TODO()collection:=database.Mg.Db.Collection(database.Users)// Create a filter to find the user by IDfilter:=bson.M{"_id":userID}// Create an update with the provided fieldsupdate:=bson.M{"$set":updatedFields}// Update the user document in the collection_,err:=collection.UpdateOne(ctx,filter,update)returnerr}funcFindUserById(userIdstring)(*model.User,error){// Create a context and a collection instanceid,err:=primitive.ObjectIDFromHex(userId)iferr!=nil{returnnil,err}ctx:=context.TODO()collection:=database.Mg.Db.Collection(database.Users)// Create a filter to find the user by phone numberfilter:=bson.M{"_id":id}// Create a variable to store the resultvarresultmodel.User// Find the user with the given phone numbererr=collection.FindOne(ctx,filter).Decode(&result)iferr!=nil{iferr==mongo.ErrNoDocuments{// If the error is ErrNoDocuments, it means no user was foundreturnnil,nil}// Handle other potential errorsreturnnil,err}return&result,nil}funcGenerateRandomNumber()string{// Generate a random number between 1000 and 9999 (inclusive)num:=rand.Intn(9000)+1000returnstrconv.Itoa(num)}funcGenerateJWT(idstring)(string,error){token:=jwt.New(jwt.SigningMethodHS256)claims:=token.Claims.(jwt.MapClaims)claims["userId"]=idclaims["exp"]=time.Now().Add(time.Hour*72).Unix()returntoken.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
packageutilimport("auth/config""fmt""log""github.com/twilio/twilio-go"openapi"github.com/twilio/twilio-go/rest/api/v2010")funcSendOTP(tostring,otpstring)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)iferr!=nil{log.Println(err.Error())returnerr}log.Println("SMS sent successfully!")returnnil}
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
packagemiddlewareimport("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 routesfuncProtected()fiber.Handler{returnjwtware.New(jwtware.Config{SigningKey:jwtware.SigningKey{Key:[]byte(config.Config("SECRET"))},ErrorHandler:jwtError,SuccessHandler:jwtSuccess,ContextKey:"payload",})}funcjwtSuccess(c*fiber.Ctx)error{payload:=c.Locals("payload").(*jwt.Token)claims:=payload.Claims.(jwt.MapClaims)userId:=claims["userId"].(string)user,err:=util.FindUserById(userId)iferr!=nil{returnc.Status(fiber.StatusUnauthorized).JSON(schema.ResponseHTTP{Success:false,Message:"User not exists",Data:nil,})}c.Locals("user",user)returnc.Next()}funcjwtError(c*fiber.Ctx,errerror)error{iferr.Error()=="Missing or malformed JWT"{returnc.Status(fiber.StatusBadRequest).JSON(schema.ResponseHTTP{Success:false,Message:"Missing or malformed JWT",Data:nil})}returnc.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 .