How to implement JWT Authetication in Go II

This blog post is the second part of a series that guides readers through the process of implementing JSON Web Token (JWT) authentication in a Go application using the Gin web framework. In this blog, we discuss storing JWT tokens in cookies for authentication purposes and defining the necessary authentication routes.

GraphQL has a role beyond API Query Language- being the backbone of application Integration
background Coditation

How to implement JWT Authetication in Go II

In the previous blog, we learned how to set up the project, including connecting to the database and migrating the user model. We also covered the creation and validation of JWT tokens, as well as the implementation of JWT middleware. In this second part of the series, we will explore how to store JWT in cookies using Go, and we will also discuss the authentication routes.

Storing JWT in cookies

In the previous section, we learned how to use JWT authentication for our Go application. In this section, we will learn how to store JWT tokens in cookies for authentication.

Understanding HTTP cookies

An HTTP cookie is a small piece of data stored on the client’s computer by the web browser. The data is sent back to the server on every request to the server that the browser makes. Cookies are used to store user information or preferences for future use, and they are an essential part of web development.

Setting and retrieving cookies in Go

To set cookies in Go, we can use the SetCookie function provided by the net/http package. We can use this function to set the cookie's name, value, expiration time, and other attributes.

To retrieve cookies in Go, we can use the c.Cookie function provided by the gin context. This function returns a pointer to an http.Cookie object, which contains the cookie's value and attributes.

We will introduce some helper functions that will help us to set and clear the cookies. Here’s what the cookieHelper.go file looks like:


cookieHelper.go file looks like:
func SetCookie(c *gin.Context, name string, value string, expiration time.Time) {
   cookie := buildCookie(name, value, expiration.Second())
   http.SetCookie(c.Writer, cookie)
}

func ClearCookie(c *gin.Context, name string) {
   cookie := buildCookie(name, "", -1)

   http.SetCookie(c.Writer, cookie)
}

func buildCookie(name string, value string, expires int) *http.Cookie {
   cookie := &http.Cookie{
       Name:     name,
       Value:    value,
       Path:     "/",
       HttpOnly: true,
       MaxAge:   expires,
   }

   return cookie
}

In this example, we have buildCookie function that takes in the cookie's name, its value and its expiry time and returns an HTTP.Cookie. The SetCookie function is a helper function that sets an HTTP cookie for a given gin.Context. The function takes three arguments:

  • c: A gin.Context instance.
  • name: A string representing the name of the cookie to be set.
  • value: A string representing the value of the cookie to be set.
  • expiration: A time.Time instance representing the expiration time of the cookie.

The function first calls the buildCookie function, passing in the name, value, and expiration arguments, and gets back a pointer to an http.Cookie instance. It then calls the http.SetCookie function to set the cookie on the response writer of the gin.Context.

The ClearCookie function is used to clear the value of an existing cookie by setting its value to an empty string and its MaxAge attribute to -1, indicating that the cookie has expired.

The function takes in two parameters:

  • c: a pointer to a gin.Context object, which contains information about the current HTTP request and response.
  • name: a string representing the name of the cookie that needs to be cleared.

The function calls the buildCookie helper function to create a new cookie object with the given name and an empty value and sets its MaxAge attribute to -1. The cookie is then written to the response header using the http.SetCookie function, which takes the c.Writer field of the gin.Context object as its first argument. This causes the client's browser to delete the specified cookie.

Routes

In this section, we will learn about the routes that we came across in the first part of the series. Let us have a look at the AuthRoutes route:


// authRouter.go
func AuthRoutes(incomingRoutes *gin.Engine) {
   incomingRoutes.POST("/users/signup", controllers.Signup())
   incomingRoutes.POST("/users/login", controllers.Login())
   incomingRoutes.GET("/users/refresh-token", controllers.RefreshToken())
   incomingRoutes.POST("/users/logout", controllers.Logout())
}

This code block defines the routes for the authentication functionality of the application. It utilizes the Gin web framework to create and manage the routes. The AuthRoutes function accepts an incomingRoutes parameter of the type *gin.Engine which is an instance of the Gin engine.

The POST method is used for the /users/signup and /users/login routes, which are responsible for creating a new user and logging in an existing user respectively. These routes are handled by the Signup and Login functions in the controllers package.

The /users/refresh-token route is a GET route that is responsible for generating a new access token and refresh token for an authenticated user. This route is handled by the RefreshToken function in the controllers package.

The /users/logout route is a POST route that is responsible for logging out an authenticated user by invalidating their refresh token. This route is handled by the Logout function in the controllers package.

By defining the routes in this way, the application can easily handle requests for user authentication and respond with the appropriate actions, such as creating a new user, logging in an existing user, refreshing access and refresh tokens, and logging out a user.

Now let’s have a look at the authController.go file. This file is a set of controller functions for handling user authentication and authorization in our web application.

The file starts by importing necessary packages and dependencies, including the Gin web framework, the Go validator package for data validation, and the bcrypt library for hashing passwords.


func UserResponse(user models.User) serializers.UserSerializer {
   return serializers.UserSerializer{
       ID:        user.ID,
       FirstName: user.FirstName,
       LastName:  user.LastName,
       Email:     user.Email,
       CreatedAt: user.CreatedAt,
       UpdatedAt: user.UpdatedAt,
   }
}

The UserResponse function returns a serialized user object with selected properties from the models.User struct.

Next, the Signup function handles user signup by first binding the request data to a models.User struct, validating the input using the validator package, and checking if a user already exists with the same email address. If the email is not already registered, the function hashes the password and creates a new user record in the database. It then generates an access token and a refresh token using the helpers.GenerateToken function and sets cookies with the tokens using the helpers.SetCookie function. Finally, the function returns a serialized user object in JSON format.


func Signup() gin.HandlerFunc {
   return func(c *gin.Context) {
       var existing_user models.User
       var user models.User

       if err := c.Bind(&user); err != nil {
           c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
           return
       }

       if err := validate.Struct(user); err != nil {
           c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
           return
       }

       database.Database.Db.Find(&existing_user, "email = ?", user.Email)

       if existing_user.ID != 0 {
           c.JSON(http.StatusBadRequest, gin.H{"error": "User already exists with this email address"})
           return
       }

       password, _ := hashPassword(string(user.Password))

       user.Password = password

       database.Database.Db.Create(&user)

       accessToken, err := helpers.GenerateToken(&user, "access")

       if err != nil {
           c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
           return
       }

       refreshToken, err := helpers.GenerateToken(&user, "refresh")

       if err != nil {
           c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
       }

       responseUser := UserResponse(user)

       helpers.SetCookie(c, "jwt", accessToken, time.Now().Add(time.Hour*1))
       helpers.SetCookie(c, "refresh_token", refreshToken, time.Now().Add(time.Hour*24*7))

       c.JSON(http.StatusCreated, responseUser)
   }
}

The Login function handles user login by first binding the request data to a map of strings and then checking if a user with the provided email exists in the database. If a user exists, the function uses the checkPasswordHash function to compare the provided password with the stored hashed password. If the passwords match, the function generates new access and refresh tokens, sets cookies with the tokens, and returns a serialized user object in JSON format.


func Login() gin.HandlerFunc {
   return func(c *gin.Context) {
       var data map[string]string

       if err := c.Bind(&data); err != nil {
           c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
           return
       }

       if data["email"] == "" || data["password"] == "" {
           c.JSON(http.StatusBadRequest, gin.H{"error": "Please provide email and password"})
           return
       }

       var user models.User

       database.Database.Db.Where("email = ?", data["email"]).First(&user)

       if user.ID == 0 {
           c.JSON(http.StatusNotFound, gin.H{"error": "No user found with this email"})
           return
       }

       if match := checkPasswordHash(data["password"], string(user.Password)); !match {
           c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Credentials"})
           return
       }

       accessToken, err := helpers.GenerateToken(&user, "access")

       if err != nil {
           c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
       }

       refreshToken, err := helpers.GenerateToken(&user, "refresh")
if err != nil {
           c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
       }

       responseUser := UserResponse(user)

       helpers.SetCookie(c, "jwt", accessToken, time.Now().Add(time.Hour*1))
       helpers.SetCookie(c, "refresh_token", refreshToken, time.Now().Add(time.Hour*24*7))

       c.JSON(http.StatusOK, responseUser)
   }
}

The RefreshToken function handles refreshing the access token using the refresh token. The function first retrieves the refresh token from the cookie, validates the token using the helpers.ValidateToken function, retrieves the user ID from the token claims, and retrieves the user record from the database. It then generates a new access token and sets a new cookie with the token.


func RefreshToken() gin.HandlerFunc {
   return func(c *gin.Context) {
       refreshToken, err := c.Cookie("refresh_token")
       if err != nil {
           c.JSON(http.StatusBadRequest, gin.H{"error": "missing refresh token"})
           return
       }

       claims, err := helpers.ValidateToken(refreshToken, "refresh")
       if err != nil {
           c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid refresh token"})
           return
       }

       var user models.User

       database.Database.Db.Where("ID = ?", claims.UserID).First(&user)

       accessToken, err := helpers.GenerateToken(&user, "access")
       if err != nil {
           c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate access token"})
           return
       }
       helpers.SetCookie(c, "jwt", accessToken, time.Now().Add(time.Hour*1))

       c.JSON(http.StatusOK, gin.H{"message": "access token refreshed successfully"})

   }
}

The Logout function handles user logout by clearing the cookies containing the access and refresh tokens.


tokens.
func Logout() gin.HandlerFunc {
   return func(c *gin.Context) {
       helpers.ClearCookie(c, "jwt")
       helpers.ClearCookie(c, "refresh_token")

       c.JSON(http.StatusOK, gin.H{
           "message": "User logged out successfully",
       })
   }
}

Lastly, the hashPassword and checkPasswordHash functions are used to hash and compare passwords using the bcrypt library.


func hashPassword(password string) (string, error) {
   bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
   return string(bytes), err
}

func checkPasswordHash(password, hash string) bool {
   err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
   return err == nil
}

We now got to know how to authenticate a user with login and signup method. We will now learn about the protected routes UserRoutes. We authorize a user with the help of AuthMiddleware what we learned in the first part of this series.


func UserRoutes(incomingRoutes *gin.Engine) {
   incomingRoutes.GET("/users", controllers.GetUsers())
   incomingRoutes.GET("/users/:userId", controllers.GetUser())
}

This code defines two routes for handling HTTP GET requests to retrieve user data:

  1. incomingRoutes.GET("/users", controllers.GetUsers()) sets up a route for the URL /users and maps it to the GetUsers function defined in the controllers package. When the server receives a GET request to /users, it will call the GetUsers function and return the response to the client.
  2. incomingRoutes.GET("/users/:userId", controllers.GetUser()) sets up a route for the URL /users/:userId, where :userId is a dynamic parameter that can be any string value. It maps this route to the GetUser function defined in the controllers package. When the server receives a GET request to /users/123, for example, it will call the GetUser function and pass in the value 123 as a parameter. The function can then use this value to retrieve the corresponding user data and return it to the client.

Overall, this code is responsible for handling user-related GET requests and directing them to the appropriate functions in the controllers package to generate the response data.

Now let us have a look at the userController.go file to have a better understanding of the routes.

The GetUsers function retrieves all users from the database using the Find method from the database package. It then maps each user to a UserSerializer struct defined in the serializers package using the UserResponse function, which formats the user data for JSON serialization. Finally, the list of serialized users is returned as a JSON response using the JSON method from the gin.Context object.


func GetUsers() gin.HandlerFunc {
   return func(c *gin.Context) {
       var users []models.User

       if err := database.Database.Db.Find(&users); err.Error != nil {
           c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Error while querying database"})
           return
       }

       var responseUsers []serializers.UserSerializer

       for _, user := range users {
           responseUsers = append(responseUsers, UserResponse(user))
       }

       c.JSON(http.StatusOK, responseUsers)
   }
}

The GetUser function retrieves a single user from the database by ID using the Where and First methods from the database package. If no user is found, a 404 error is returned as a JSON response. Otherwise, the retrieved user is mapped to a UserSerializer struct using the UserResponse function, and returned as a JSON response using the JSON method from the gin.Context object.


func GetUser() gin.HandlerFunc {
   return func(c *gin.Context) {
       userId := c.Param("userId")
       var user models.User

       database.Database.Db.Where("ID = ?", userId).First(&user)

       if user.ID == 0 {
           c.JSON(http.StatusNotFound, gin.H{"error": "No user found with the given ID"})
           return
       }

       responseUser := UserResponse(user)

       c.JSON(http.StatusOK, responseUser)
   }
}

Conclusion

To conclude, the series explained how to implement JWT authentication in a Go application using the Gin framework. It started by explaining the basics of JWT and its benefits. Then it moved on to explaining how to set up a basic Gin application and install the necessary packages.

After setting up the application, the blog explained how to create a user model, set up the database, and implement user authentication using JWT. It also explained how to create a middleware function to authenticate the JWT token on protected routes.

Finally, the blog demonstrated how to test the authentication functionality using Postman. By following the steps outlined in the blog, readers should have a solid understanding of how to implement JWT authentication in a Go application using the Gin framework.

Benefits of using JWT authentication in Go:

Using JWT authentication in Go offers several benefits, including:

  1. Stateless Authentication: JWT authentication enables stateless authentication, meaning that the server doesn’t need to store any session data for authenticated users.
  2. Improved Security: JWT authentication provides an additional layer of security to web applications by encrypting the user’s information.
  3. Scalability: JWT authentication is scalable because it can be used with multiple servers.
  4. Flexibility: JWT authentication is flexible and can be used across multiple platforms and languages.

Future improvements and considerations:

While JWT authentication provides a secure and efficient way to authenticate users, there are some considerations to keep in mind for future improvements. Some of these include:

  1. Token Revocation: If a user’s token is compromised or stolen, there needs to be a way to revoke the token and prevent unauthorized access.
  2. Token Expiration: JWT tokens have an expiration date, and if the token is not renewed, the user will be required to log in again.
  3. Token Size: If the token contains a lot of information, it can become quite large, which can cause performance issues.
  4. Key Management: The secret key used to sign the token should be kept secure and updated regularly to prevent any security breaches.

Overall, JWT authentication is a great way to secure your web applications, and with the right considerations and best practices, it can be a reliable and efficient method of user authentication in Go.

Hello, I'm Rekib Ahmed. I am passionate about all things tech, with an interest in cooking, reading, and the always-changing world of innovation.

Want to receive update about our upcoming podcast?

Thanks for joining our newsletter.
Oops! Something went wrong.

Latest Articles

Implementing Custom Instrumentation for Application Performance Monitoring (APM) Using OpenTelemetry

Application Performance Monitoring (APM) has become crucial for businesses to ensure optimal software performance and user experience. As applications grow more complex and distributed, the need for comprehensive monitoring solutions has never been greater. OpenTelemetry has emerged as a powerful, vendor-neutral framework for instrumenting, generating, collecting, and exporting telemetry data. This article explores how to implement custom instrumentation using OpenTelemetry for effective APM.

Mobile Engineering
time
5
 min read

Implementing Custom Evaluation Metrics in LangChain for Measuring AI Agent Performance

As AI and language models continue to advance at breakneck speed, the need to accurately gauge AI agent performance has never been more critical. LangChain, a go-to framework for building language model applications, comes equipped with its own set of evaluation tools. However, these off-the-shelf solutions often fall short when dealing with the intricacies of specialized AI applications. This article dives into the world of custom evaluation metrics in LangChain, showing you how to craft bespoke measures that truly capture the essence of your AI agent's performance.

AI/ML
time
5
 min read

Enhancing Quality Control with AI: Smarter Defect Detection in Manufacturing

In today's competitive manufacturing landscape, quality control is paramount. Traditional methods often struggle to maintain optimal standards. However, the integration of Artificial Intelligence (AI) is revolutionizing this domain. This article delves into the transformative impact of AI on quality control in manufacturing, highlighting specific use cases and their underlying architectures.

AI/ML
time
5
 min read