GoLang JWT Authentication Using Golang Gin with MongoDB

Hello fellow developers❤!

In this blog, we're jumping headfirst into the creation of a JWT authentication application using the powerful Gin Web Framework. 

Grab your coffee, as we go on an goLang adventure filled with code, creativity, and countless "aha!" moments😆. 

But wait, there's more! Along the way, we'll learn of JWT - Authentication, learn when to wield its power and master the art of building APIs for a signup, login, user retrieval, and more.

And fear not, brave coder! I'll be your guide through every twist and turn, so you can learn with ease🌟

What should We Have Beforehand?

- Set up the directory structure for your project.

- Understand Mod Files, Packages, and Functions.

- Familiarize yourself with concepts like Struct, Slices, Routing, and Database Connection.

- For Database Connection, use MongoDB Atlas. Understand Clusters and how to fetch your Connection String from MongoDB Atlas and integrate it into your code.

JWT Authentication

Lets just go through the basics of JWT in no time😌!

  • JWT consists of three parts: header, payload, and signature, separated by dots.
  • The header contains token type (JWT) and signing algorithm (e.g., RSA, HMAC).
  • Payload holds claims (user information) and additional data (e.g., expiration time).
  • Signature, created with a secret key and secure algorithm (e.g., HMAC, SHA256), ensures authentication and data integrity.


Source

Folder Structure for Golang Gin Framework

This is the perfect folder structure for your Golang gin project.



Build JWT Authentication using Golang and MongoDB

Lets create a JWT authentication with Golang using Gin with MongoDB:

Fill Your main.go

As is already known, "main.go" will be the first file we create. I'll put a few packages, here.

In the `func main`, I set the `port` variable to 8000 and created a new router with `gin.New()`. Optionally, I added middleware for logging with `router.Use(gin.Logger())`. 

Two functions, `UserRoutes` and `AuthRoutes`, are then called from the `routes` package, passing the router variable to them. While this is a concise approach, it's good practice to consider using environment variables for configuration, like fetching the port number from a `.env` file. This approach requires importing the `os` and `github.com/joho/godotenv` packages.

package main
 
import (
    "github.com/gin-gonic/gin"
    routes "github.com/golangcompany/JWT-Authentication/routes"
)
 
func main() {
    port := "3000"
 
    router := gin.New()
 
    routes.UserRoutes(router)
    routes.AuthRoutes(router)
 
    router.GET("/get-api", func(c *gin.Context) {
        c.JSON(200, gin.H{"success": "Successfully fetched 1"})
    })
 
    router.GET("/get-api-2", func(c *gin.Context) {
        c.JSON(200, gin.H{"success": "Successfully fetched 12"})
    })
 
    router.Run(":" + port)
}

Create a Struct

In userModels.go, I defined a struct with fields including ID, First Name, Last Name, Password, E-Mail, Phone Number, Token ID, User Type, Refresh Token ID, Created at, Updated at, and User ID. 

I imported the "primitive" package for generating unique IDs. The validate function ensures client-entered data matches our format. For timestamps like Created at and Updated at, I used the time.Time data type and imported the time package.

package models
 
import (
    "go.mongodb.org/mongo-driver/bson/primitive"
    "time"
)
 
type Users struct {
    ID            primitive.ObjectID `bson:"_id"`
    FirstName    *string            `json:"firstName" validate:"required,min=2,max=100"`
    LastName     *string            `json:"lastName" validate:"required,min=2,max=100"`
    Password      *string            `json:"Password" validate:"required,min=6"`
    Email         *string            `json:"email" validate:"email,required"`
    Phone         *string            `json:"phone" validate:"required"`
    Token         *string            `json:"token"`
    UserType     *string            `json:"userType" validate:"required,eq=ADMIN|eq=USER"`
    RefreshToken *string            `json:"refresh_token"`
    CreatedAt    time.Time          `json:"createdAt"`
    UpdatedAt    time.Time          `json:"updatedAt"`
    UserId       string             `json:"userId"`
}

Now Comes Your Route

In the `routes.go` file, we define the `UserRoutes` function, which handles two POST methods for signup and login. 

The paths for these methods are "/users/signup" and "/users/login".

package routes
 
import (
    "github.com/gin-gonic/gin"
    controller "github.com/golangcompany/JWT-Authentication/controllers"
    "github.com/golangcompany/JWT-Authentication/middleware"
)
 
func UserRoutes(routes *gin.Engine) {
    routes.POST("users-signup", controller.AuthSignup())
    routes.POST("users-login", controller.AuthLogin())
}
func AuthRoutes(incomingRoutes *gin.Engine) {
    routes.Use(middleware.UserAuthenticate())
    routes.GET("/usersData", controller.GetUsers())
    routes.GET("/users/:user_id", controller.GetUser())
}

Each method calls a corresponding function in the controller package. Additionally, we define the `AuthRoutes` function, which uses the GET method and the `middleware.Authenticate` function to protect routes. This ensures that only authenticated users can access these routes.

package database
 
import (
    "context"
    "fmt"
    "log"
    "time"
 
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)
 
func DBSet() *mongo.Client {
    client, err := mongo.NewClient(options.Client().ApplyURI("mongodb+srv://codegirl_forya:@xxxxx.xxxxx.mongodb.net/?retryWrites=true&w=majority"))
    if err != nil {
        log.Fatal(err)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

Connect MongoDB with Golang Gin

I'll create a function called `DBSet` that connects to MongoDB and returns the client object if successful, otherwise `nil`. The URI is stored in the `client` variable. 

To connect to MongoDB, know the connection string. Then, I'll declare a variable called `Client` of type `*mongo.Client`, initialized with the return value of `DBSet()`. 

Finally, I'll define a function called `UserData` to access a collection. It takes a client pointer and collection name as parameters. After establishing the connection, I'll proceed with `userControllers.go`.

package database
 
import (
    "context"
    "fmt"
    "log"
    "time"
 
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)
 
func SetDB() *mongo.Client {
    client, err := mongo.NewClient(options.Client().ApplyURI("mongodb+srv://codegirl_forya:@xxxxx.xxxxx.mongodb.net/?retryWrites=true&w=majority"))
    if err != nil {
        log.Fatal(err)
    }
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    err = client.Connect(ctx)
    if err != nil {
        log.Fatal(err)
    }
    err = client.Ping(context.TODO(), nil)
    if err != nil {
        log.Println("Failed to Connect")
        return nil
    }
    fmt.Println("Successfully Connected to the MongoDB")
    return client
}
 
var Client *mongo.Client = SetDB()
 
func UserData(client *mongo.Client, CollectionName string) *mongo.Collection {
    var collection *mongo.Collection = client.Database("Cluster0").Collection(CollectionName)
    return collection
}

Control with Controllers

In this Go project, I started by importing essential packages like "context", "fmt", "log", "net/http", "strconv", and "time". I set up a connection to our MongoDB database and define a userCollection variable to interact with the "user" collection.

The controller functions we implement include HashPassword and VerifyPassword, which handle password hashing and verification using bcrypt. Signup creates a new user, validating the input and generating authentication tokens. Login checks user credentials and returns a token for authenticated users.

We also have GetUsers, which retrieves user data based on specific criteria, and GetUser, which fetches user information by ID. These functions use MongoDB's aggregation pipeline to efficiently query the database.
package controllers
 
import (
    "context"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "time"
 
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
    "github.com/golangcompany/JWT-Authentication/database"
    helper "github.com/golangcompany/JWT-Authentication/helpers"
    "github.com/golangcompany/JWT-Authentication/models"
    "golang.org/x/crypto/bcrypt"
 
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
)

This is how i set up the mongoDB connection

var usersCollection *mongo.Collection = database.UsersData(database.Client, "user")
var validate = validator.New()

The HashingPassword will make the password in hash form for security purpose.

func HashingPassword(password string) string {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        log.Panic(err)
    }
    return string(bytes)
}

The VerifyPassword will verify the password of an user.

func VerifyPassword(userPassword string, providedPassword string) (bool, string) {
    err := bcrypt.CompareHashAndPassword([]byte(providedPassword), []byte(userPassword))
    check := true
    msg := ""
 
    if err != nil {
        msg = fmt.Sprintf("E-Mail or Password is incorrect")
        check = false
    }
    return check, msg
}

The Signup func will create a new user with every necessary details.

func Signup() gin.HandlerFunc {
 
    return func(c *gin.Context) {
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
        var user models.User
 
        if err := c.BindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
 
        validationErr := validate.Struct(user)
        if validationErr != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": validationErr.Error()})
            return
        }
 
        count, err := userCollection.CountDocuments(ctx, bson.M{"email": user.Email})
        defer cancel()
        if err != nil {
            log.Panic(err)
            c.JSON(http.StatusInternalServerError, gin.H{"error": "error detected while fetching the email"})
        }
 
        password := HashPassword(*user.Password)
        user.Password = &password
 
        count, err = userCollection.CountDocuments(ctx, bson.M{"phone": user.Phone})
        defer cancel()
        if err != nil {
            log.Panic(err)
            c.JSON(http.StatusInternalServerError, gin.H{"Error": "Error occured while fetching the phone number"})
        }
 
        if count > 0 {
            c.JSON(http.StatusInternalServerError, gin.H{"Error": "The mentioned E-Mail or Phone Number already exists"})
        }
 
        user.Created_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.Updated_at, _ = time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
        user.ID = primitive.NewObjectID()
        user.User_id = user.ID.Hex()
        token, refreshToken, _ := helper.GenerateAllTokens(*user.Email, *user.First_name, *user.Last_name, *user.User_type, *&user.User_id)
        user.Token = &token
        user.Refresh_token = &refreshToken
 
        resultInsertionNumber, insertErr := userCollection.InsertOne(ctx, user)
        if insertErr != nil {
            msg := fmt.Sprintf("User Details were not Saved")
            c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
            return
        }
        defer cancel()
        c.JSON(http.StatusOK, resultInsertionNumber)
    }
 
}

The login function will verify the credentials, the user is providing. Based on that a jwt token and refresh token will generate.

func Login() gin.HandlerFunc {
    return func(c *gin.Context) {
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
        var user models.User
        var foundUser models.User
 
        if err := c.BindJSON(&user); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
 
        err := userCollection.FindOne(ctx, bson.M{"email": user.Email}).Decode(&foundUser)
        defer cancel()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "email or password is incorrect"})
            return
        }
 
        passwordIsValid, msg := VerifyPassword(*user.Password, *foundUser.Password)
        defer cancel()
        if passwordIsValid != true {
            c.JSON(http.StatusInternalServerError, gin.H{"error": msg})
            return
        }
 
        if foundUser.Email == nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "user not found"})
        }
        token, refreshToken, _ := helper.GenerateAllTokens(*foundUser.Email, *foundUser.First_name, *foundUser.Last_name, *foundUser.User_type, foundUser.User_id)
        helper.UpdateAllTokens(token, refreshToken, foundUser.User_id)
        err = userCollection.FindOne(ctx, bson.M{"user_id": foundUser.User_id}).Decode(&foundUser)
 
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
        c.JSON(http.StatusOK, foundUser)
    }
}

The GetUsers func will gather all the users who is of type "USER".

func GetUsers() gin.HandlerFunc {
    return func(c *gin.Context) {
        if err := helper.CheckUserType(c, "ADMIN"); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
 
        recordPerPage, err := strconv.Atoi(c.Query("recordPerPage"))
        if err != nil || recordPerPage < 1 {
            recordPerPage = 10
        }
        page, err1 := strconv.Atoi(c.Query("page"))
        if err1 != nil || page < 1 {
            page = 1
        }
 
        startIndex := (page - 1) * recordPerPage
        startIndex, err = strconv.Atoi(c.Query("startIndex"))
 
        matchStage := bson.D{{Key: "$match", Value: bson.D{{}}}}
        groupStage := bson.D{{Key: "$group", Value: bson.D{
            {Key: "_id", Value: bson.D{{Key: "_id", Value: "null"}}},
            {Key: "total_count", Value: bson.D{{Key: "$sum", Value: 1}}},
            {Key: "data", Value: bson.D{{Key: "$push", Value: "$$ROOT"}}}}}}
        projectStage := bson.D{
            {Key: "$project", Value: bson.D{
                {Key: "_id", Value: 0},
                {Key: "total_count", Value: 1},
                {Key: "user_items", Value: bson.D{{Key: "$slice", Value: []interface{}{"$data", startIndex, recordPerPage}}}}}}}
        result, err := userCollection.Aggregate(ctx, mongo.Pipeline{
            matchStage, groupStage, projectStage})
        defer cancel()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "error occured while listing user items"})
        }
        var allusers []bson.M
        if err = result.All(ctx, &allusers); err != nil {
            log.Fatal(err)
        }
        c.JSON(http.StatusOK, allusers[0])
    }
}

You can fetch users by specific user_id.

 
func GetUser() gin.HandlerFunc {
    return func(c *gin.Context) {
        userId := c.Param("user_id")
 
        if err := helper.MatchUserTypeToUid(c, userId); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }
        var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
 
        var user models.User
        err := userCollection.FindOne(ctx, bson.M{"user_id": userId}).Decode(&user)
        defer cancel()
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }
        c.JSON(http.StatusOK, user)
    }
}

Is Middleware Necessary?

The authentication function verifies if the client provides a token in the request header. If not, it returns an error and stops the request. If a valid token is present, it sets values in the context and proceeds to the next handler using 'c.Next()'. If the token is invalid, it also returns an error and halts the request. 'c.Set()' assigns key-value pairs, and 'c.Next()' ensures the request continues to subsequent handlers. 
package middleware
 
import (
    "fmt"
    "github.com/gin-gonic/gin"
    helper "github.com/golangcompany/JWT-Authentication/helpers"
    "net/http"
)
 
func Authenticate() gin.HandlerFunc {
    return func(c *gin.Context) {
        clientToken := c.Request.Header.Get("token")
        if clientToken == "" {
            c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("No Authorization Header Provided")})
            c.Abort()
            return
        }
 
        claims, err := helper.ValidateToken(clientToken)
        if err != "" {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err})
            c.Abort()
            return
        }
        c.Set("email", claims.Email)
        c.Set("first_name", claims.First_name)
        c.Set("last_name", claims.Last_name)
        c.Set("uid", claims.Uid)
        c.Set("user_type", claims.User_type)
        c.Next()
    }
}

Helpers will Help You

package helper
 
import (
    "errors"
    "github.com/gin-gonic/gin"
)
 
func CheckUserType(c *gin.Context, role string) (err error) {
    userType := c.GetString("user_type")
    err = nil
    if userType != role {
        err = errors.New("You are not authorised to access this resource")
        return err
    }
    return err
}
 
func MatchUserTypeToUid(c *gin.Context, userId string) (err error) {
    userType := c.GetString("user_type")
    uid := c.GetString("uid")
    err = nil
 
    if userType == "USER" && uid != userId {
        err = errors.New("You are not authorised to access this resource")
        return err
    }
    err = CheckUserType(c, userType)
    return err
}

authHelper.go manages user authentication with functions for checking user roles and matching user types to IDs. tokenHelper.go handles JWT token generation, validation, and database updates, including functions for generating, validating, and updating tokens.

package helper
 
import (
    "context"
    "fmt"
    jwt "github.com/dgrijalva/jwt-go"
    "github.com/golangcompany/JWT-Authentication/database"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "log"
    "os"
    "time"
)
 
type SignedDetails struct {
    Email      string
    First_name string
    Last_name  string
    Uid        string
    User_type  string
    jwt.StandardClaims
}
 
var userCollection *mongo.Collection = database.UserData(database.Client, "user")
 
var SECRET_KEY string = os.Getenv("SECRET_KEY")
 
func GenerateAllTokens(email string, firstName string, lastName string, userType string, uid string) (signedToken string, signedRefreshToken string, err error) {
    claims := &SignedDetails{
        Email:      email,
        First_name: firstName,
        Last_name:  lastName,
        Uid:        uid,
        User_type:  userType,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(24)).Unix(),
        },
    }
 
    refreshClaims := &SignedDetails{
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(168)).Unix(),
        },
    }
 
    token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(SECRET_KEY))
    refreshToken, err := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString([]byte(SECRET_KEY))
 
    if err != nil {
        log.Panic(err)
        return
    }
 
    return token, refreshToken, err
}
 
func ValidateToken(signedToken string) (claims *SignedDetails, msg string) {
    token, err := jwt.ParseWithClaims(
        signedToken,
        &SignedDetails{},
        func(token *jwt.Token) (interface{}, error) {
            return []byte(SECRET_KEY), nil
        },
    )
 
    if err != nil {
        msg = err.Error()
        return
    }
 
    claims, ok := token.Claims.(*SignedDetails)
    if !ok {
        msg = fmt.Sprintf("the token is invalid")
        msg = err.Error()
        return
    }
 
    if claims.ExpiresAt < time.Now().Local().Unix() {
        msg = fmt.Sprintf("token is expired")
        msg = err.Error()
        return
    }
    return claims, msg
}
 
func UpdateAllTokens(signedToken string, signedRefreshToken string, userId string) {
    var ctx, cancel = context.WithTimeout(context.Background(), 100*time.Second)
 
    var updateObj primitive.D
 
    updateObj = append(updateObj, bson.E{Key: "token", Value: signedToken})
    updateObj = append(updateObj, bson.E{Key: "refresh_token", Value: signedRefreshToken})
 
    Updated_at, _ := time.Parse(time.RFC3339, time.Now().Format(time.RFC3339))
    updateObj = append(updateObj, bson.E{Key: "updated_at", Value: Updated_at})
 
    upsert := true
    filter := bson.M{"user_id": userId}
    opt := options.UpdateOptions{
        Upsert: &upsert,
    }
 
    _, err := userCollection.UpdateOne(
        ctx,
        filter,
        bson.D{
            {Key: "$set", Value: updateObj},
        },
        &opt,
    )
 
    defer cancel()
 
    if err != nil {
        log.Panic(err)
        return
    }
    return
}

Now check your APIs on  Postman and get your desired output👀

Happy coding✌

Frequently Asked Questions

1. Is Golang Gin fast?

Gin framework is widely recognized for its superior functionality. Among Go web frameworks, it has one of the quickest request processing speeds. Because of this, it may be used to create online apps and APIs with excellent performance.

2. Why should we use gin in Golang?

Gin is a Golang (Go) web framework designed for excellent speed using HTTP. Gin promises to be up to 40 times faster. Gin lets you create Go microservices and web apps. 

3. Is Golang faster than NodeJS?

In terms of calculation and raw performance, Go is faster than NodeJS. Up to 1000 concurrent requests can be handled by it in a second. It is lightweight and quick because to its C and C++ features. NodeJS is slower than other languages since it is statically typed and derived from JavaScript.

4. What is the gin context in Golang?

The pointer is *gin. Context offers you access to both the HTTP request and Gin's framework functions, such as String, which allows you to build a string-based HTTP response. 

codegirl

Hello, I’m Sangita, person behind "codegirl", a dedicated web developer. Crafting digital experiences is not just my job; it’s my passion. Let’s build something exceptional together!

Post a Comment

Previous Post Next Post