Building A Web Application With Go, Gin, And JWT Authentication – Part 2

Tutorial: Building on Go Gin JWT Authentication – Part 2

Welcome to the second part of our Go Gin JWT Authentication tutorial series! If you haven’t checked out the first part, make sure to do so here. In this tutorial, we’ll expand our authentication system to include user login, registration, and a user profile page with updates. Additionally, we’ll introduce a flash messaging system to enhance user experience.

Prerequisites

Before you proceed, make sure you have completed the first part of the tutorial. If you haven’t, please refer to Part 1 for the initial setup and JWT authentication.

  • Go installed on your machine
  • MySQL database installed and running
  • Basic understanding of Go Gin and JWT authentication

New Files

We’ve added several new files to our project to support user registration and profile updates:

mywebapp/
├── static/
│   ├── css/
│   └── js/
└── controllers/
│   └── AuthController.go
│   └── ProfileController.go
└── config/
│   └── mysql.go
└── middlewares/
│   └── AuthMiddleware.go
│   └── FlashMessageMiddleware.go
└── models/
└── views/
│   └── login.tmpl
│   └── profile.tmpl
│   └── register.tmpl
└── utils/
    └── RenderTemplate.go

Step 1: Create register.tmpl

This is a simple registration page with username, and password required. Additionally, there’s a section to display flash messages.

<!-- views/register.tmpl -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Register</title>
</head>
<body>
    <h2>Register</h2>
    <!-- Flash messages -->
    {{ with .Flash }}
      {{ range . }}
        <div class="flash-message">{{ . }}</div>
      {{ end }}
    {{ end }}

    <form action="/api/register" method="POST">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name" required><br>

        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required><br>

        <label for="email">Email:</label>
        <input type="email" id="email" name="email" required><br>

        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required><br>

        <button type="submit">Register</button>
    </form>

    <!-- Login link -->
    <p>Already have an account? <a href="/login">Login here</a></p>
</body>
</html>

Step 2: Create login.tmpl

Login page with a form where users can enter their username and password. Similar to the registration page, the login.tmpl HTML template displays any flash messages on the page.

<!-- views/login.tmpl -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>
<body>
    <h2>Login</h2>
    <!-- Flash messages -->
    {{ with .Flash }}
        {{ range . }}
            <div class="flash-message">{{ . }}</div>
        {{ end }}
    {{ end }}

    <form action="/api/login" method="POST">
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required><br>

        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required><br>

        <button type="submit">Login</button>
    </form>

    <!-- Registration link -->
    <p>Don't have an account? <a href="/register">Register here</a></p>
</body>
</html>

Step 3: Create profile.tmpl

The profile page features a form where users can update their name and email, and it will display any flash messages.

<!-- views/profile.tmpl -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Profile</title>
</head>
<body>
    <h2>Profile</h2>
    <!-- Flash messages -->
    {{ with .Flash }}
        {{ range . }}
            <div class="flash-message">{{ . }}</div>
        {{ end }}
    {{ end }}

    <form action="/web/user" method="POST" enctype="application/x-www-form-urlencoded">
        <!-- Your form fields here -->
        <label for="name">Name:</label>
        <input type="text" id="name" name="name" value="{{.Name}}" required><br>

        <label for="email">Email:</label>
        <input type="email" id="email" name="email" value="{{.Email}}" required><br>

        <button type="submit">Update Profile</button>
    </form>
</body>
</html>

Step 4: Create AuthMiddleware.go

The AuthMiddleware.go file is a middleware in Go Gin designed to handle session-based authentication. This middleware ensures that certain routes or actions are only accessible by authenticated users. It redirects unauthenticated users to the login page.

// middlewares/AuthMiddleware.go
package middlewares

import (
	"net/http"

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

// AuthMiddleware is a middleware to check session for authentication
func AuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		session := sessions.Default(c)

		// Check if the "user" key is present in the session
		user := session.Get("user")
		if user == nil {
			// Redirect to the login page if not authenticated
			c.Redirect(http.StatusSeeOther, "/login")
			c.Abort() // Stop the execution of subsequent middleware and the handler
			return
		}

		// Continue with the request if authenticated
		c.Next()
	}
}

Step 5: Create FlashMessageMiddleware.go

The FlashMessageMiddleware.go file is another middleware in Go Gin, and it specifically adds flash message functionality to the Gin context. This middleware provides functions to set and retrieve flash messages, enhancing the user experience by allowing the display of success or error messages.

// middlewares/FlashMessageMiddleware.go
package middlewares

import (
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
)

// FlashMessageMiddleware adds flash message functionality to the Gin context
func FlashMessageMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// Function to set flash messages
		c.Set("setFlashMessage", func(message string) {
			session := sessions.Default(c)
			session.AddFlash(message)
			session.Save()
		})

		// Function to get flash messages
		c.Set("getFlashMessages", func() []string {
			session := sessions.Default(c)
			flashes := session.Flashes()
			messages := make([]string, len(flashes))
			for i, flash := range flashes {
				messages[i] = flash.(string)
			}
			session.Save()
			return messages
		})

		c.Next()
	}
}

Moreover, these middleware components play a crucial role in enhancing the security and user interaction aspects of your Go Gin application. The AuthMiddleware ensures that only authenticated users can access certain routes, and FlashMessageMiddleware facilitates the display of informative messages across different requests.

Step 6: Create RenderTemplate.go

The RenderTemplate.go file is a utility in your Go Gin application that has been updated to handle rendering templates along with flash messages. It facilitates the process of rendering HTML templates while incorporating flash messages for improved user interaction.

// utils/RenderTemplate.go
package utils

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// RenderTemplate is a custom function to render a template with flash messages
func RenderTemplate(c *gin.Context, templateName string, data gin.H) {
	// Fetch flash messages from the template context
	flashMessages := c.MustGet("getFlashMessages").(func() []string)()

	// Add flash messages to the data to be passed to the template
	data["Flash"] = flashMessages

	// Render the template
	c.HTML(http.StatusOK, templateName, data)
}

This utility becomes handy when you want to render HTM templates and include flash messages, improving the user experience with informative messages.

Building A Web Application With Go, Gin, And JWT Authentication – Part 2

An additional function ExtractToken has been added to token.go. This function extracts the token from the Gin context, considering both query parameters and the “Authorization” header. It’s a utility function that simplifies the process of retrieving tokens from various sources within your application.

// token.go
func ExtractToken(c *gin.Context) string {
	token := c.Query("token")
	if token != "" {
		return token
	}
	bearerToken := c.Request.Header.Get("Authorization")
	if len(strings.Split(bearerToken, " ")) == 2 {
		return strings.Split(bearerToken, " ")[1]
	}
	return ""
}

The function makes it easier to obtain tokens from different locations within the HTTP request.

Step 8: Update AuthController.go

The AuthController.go file has been extended to include registration handling. This controller manages the authentication-related functionalities in your Go Gin application.

// controllers/AuthController.go (continued)
package controllers

import (
	"github.com/gin-gonic/gin"
	"mywebapp/models"
	"mywebapp/utils"
	"net/http"
	"github.com/gin-contrib/sessions"
)

// RegisterIndex displays the registration form
func RegisterIndex(c *gin.Context) {
	utils.RenderTemplate(c, "register.tmpl", gin.H{
		"title": "Register",
	})
}

// Register handles user registration
func Register(c *gin.Context) {
	var input models.RegisterInput

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

	// Check if passwords match
	if input.Password != input.ConfirmPassword {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Passwords do not match"})
		return
	}

	// Create a new user
	user, err := models.CreateUser(input.Username, input.Password)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Registration successful", "user": user})
}

// RegisterForm handle user registration via form
func RegisterForm(c *gin.Context) {
	username := c.PostForm("username")
	password := c.PostForm("password")
	name := c.PostForm("name")
	email := c.PostForm("email")

	// Create a new user
	user := models.User{
		Username: username,
		Password: password,
		Name:     name,
		Email:    email,
	}

	// Save the user to the database
	_, err := user.SaveUser()

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

	flashMessage := c.MustGet("setFlashMessage").(func(string))
	flashMessage("Register successfully")

	redirectURL := "/login"
    c.Redirect(http.StatusSeeOther, redirectURL)
}

// Login Index Page
func LoginIndex (c *gin.Context) {
	utils.RenderTemplate(c, "login.tmpl", gin.H{
		"title": "Login",
	})
}

// LoginWithSession handles user login and sets a session
func LoginWithSession(c *gin.Context) {
	username := c.PostForm("username")
	password := c.PostForm("password")

	// Authenticate user
	token, err := models.LoginCheck(username, password)

	if err != nil {
		// Authentication failed, return an error response
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
		return
	}

	// Set a session variable
	session := sessions.Default(c)
	session.Set("token", token)
	session.Set("user", username)
	session.Save()

	redirectURL := "/dashboard/profile"
    c.Redirect(http.StatusSeeOther, redirectURL)
}

This includes register, and login rendering function, as well as registration, login with session setting functionalities.

Step 9: Create ProfileController.go

A new controller, ProfileController.go, has been added to manage user profiles and updates.

package controllers

import (
	"net/http"
	"strings"
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
	"ginjwtauth/models"
	"ginjwtauth/utils"
)

// ProfileInput represents the input for the profile update endpoint
type ProfileInput struct {
    Name  string `form:"name" binding:"required"`
    Email string `form:"email" binding:"required"`
}

// Profile displays the user's profile
func ProfileIndex(c *gin.Context) {
	session := sessions.Default(c)
	username := session.Get("user").(string)

	user, err := models.GetUserByUsername(username)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "User not found"})
		return
	}

	utils.RenderTemplate(c, "profile.tmpl", gin.H{
		"Name":   user.Name,
		"Email":  user.Email,
	})
}

// UpdateProfile updates the user profile based on the provided input
func UpdateProfile(c *gin.Context) {
	var (
		userID uint
		err    error
	)

	session := sessions.Default(c)
	username := session.Get("user")

	// Retrieve user
	if username != nil {
		user, err := models.GetUserByUsername(username.(string))
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "User not found"})
			return
		}

		userID = user.ID
	} else {
		userID, err = utils.ExtractTokenID(utils.ExtractToken(c))

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

	var input ProfileInput

	// Use ShouldBind with Form instead of ShouldBindQuery
	if err := c.ShouldBind(&input); err != nil {
	    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	    return
	}

	u, err := models.GetUserByID(userID)

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

	u.Name = input.Name
	u.Email = input.Email

	_, saveErr := u.UpdateUser()

	if saveErr != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error4": saveErr.Error()})
		return
	}

	// Check if the request is from the web (HTML form)
    if strings.Contains(c.Request.URL.Path, "/web/") {
        // Redirect to a different page
        flashMessage := c.MustGet("setFlashMessage").(func(string))
		flashMessage("Profile updated successfully")

        redirectURL := "/dashboard/profile"
        c.Redirect(http.StatusSeeOther, redirectURL)
    } else {
        // Respond with JSON for API requests
        c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"})
    }
}

This controller manages the user’s profile display and the update process based on the provided input. It includes functions for rendering the profile page and handling profile updates.

Step 10: Update main.go

Install Session Package

go get github.com/gin-contrib/session

Import Session Packages. The sessions package provides general session support, while cookie is a specific session store implementation using cookies.

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"

Set up Session Middleware. This middleware will handle session-related tasks, such as creating and managing user sessions.

// Set up session middleware
store := cookie.NewStore([]byte("secret"))
r.Use(sessions.Sessions("mysession", store))

Use FlashMessageMiddleware. This line adds the FlashMessageMiddleware to the Gin engine.

// Set up FlashMessageMiddleware
r.Use(middlewares.FlashMessageMiddleware())

Load HTML Templates. This line tells Gin to load HTML templates from the views directory. It’s crucial for rendering HTML pages with dynamic content.

// Load templates
r.LoadHTMLGlob("views/*")

Profile Routes with JWT Authentication Middleware. These routes are related to user profiles and are protected by JWT authentication middleware (JwtAuthMiddleware). The middleware ensures that requests to these routes include a valid JWT token.

api.GET("/profile", middlewares.JwtAuthMiddleware(), controllers.Profile)
api.GET("/user", middlewares.JwtAuthMiddleware(), controllers.Profile)
api.POST("/user", middlewares.JwtAuthMiddleware(), controllers.UpdateProfile)

Furthermore, define web routes for the web interface. The /login and /register routes serve the login and registration pages, while the “/dashboard/profile” route is protected by AuthMiddleware, displaying the user profile page. Additionally, the route /web/user serves for updating user profiles.

r.GET("/login", controllers.LoginIndex)
r.GET("/register", controllers.RegisterIndex)

dashboard := r.Group("/dashboard")
dashboard.Use(middlewares.AuthMiddleware())
dashboard.GET("/profile", controllers.ProfileIndex)

web := r.Group("/web")
{
    web.Use(middlewares.AuthMiddleware())
    web.POST("/user", controllers.UpdateProfile)
}

New login route with session, and register route to handle form submission. Login route sets a session variable upon successful login.

// New login route that sets a session
r.POST("/api/login", controllers.LoginWithSession)
// New register route for form
r.POST("/api/register", controllers.RegisterForm)

Full Final main.go

// main.go (continued)
package main

import (
    "github.com/gin-gonic/gin"
    "mywebapp/controllers"
    "mywebapp/middlewares"
    "mywebapp/utils"
)

func main() {
    // Initialize the Gin router
    r := gin.Default()

    // Connect to the database
    models.ConnectDataBase()

    // Set up FlashMessageMiddleware
    r.Use(middlewares.FlashMessageMiddleware())

    // Load templates
    r.LoadHTMLGlob("views/*")

    // Set up session middleware
    store := cookie.NewStore([]byte("secret"))
    r.Use(sessions.Sessions("mysession", store))

    // Group Api routes
    api := r.Group("/api")
    {
        auth := api.Group("/auth")
        {
            // Register and login routes
            auth.POST("/register", controllers.Register)
            auth.POST("/login", controllers.Login)
        }

        // Profile route with JWT authentication middleware
        api.GET("/profile", middlewares.JwtAuthMiddleware(), controllers.Profile)
        api.GET("/user", middlewares.JwtAuthMiddleware(), controllers.Profile)
        api.POST("/user", middlewares.JwtAuthMiddleware(), controllers.UpdateProfile)
    }

    r.GET("/login", controllers.LoginIndex)
    r.GET("/register", controllers.RegisterIndex)

    dashboard := r.Group("/dashboard")
    dashboard.Use(middlewares.AuthMiddleware())
    dashboard.GET("/profile", controllers.ProfileIndex)

    web := r.Group("/web")
    {
        web.Use(middlewares.AuthMiddleware())
        web.POST("/user", controllers.UpdateProfile)
    }

    // New login route that sets a session
    r.POST("/api/login", controllers.LoginWithSession)
    // New register route for form
    r.POST("/api/register", controllers.RegisterForm)

    // Run the application on port 8082
    r.Run(":8080")
}

This is the final version of the main.go file, combining all the changes and additions made during the steps. It includes routes for user authentication, profile management, and web interface functionality.

This completes the second part of our tutorial. Users can now register, log in, and update their profiles with ease. Please feel free to visit my GitHub to get the full source code.

Thank you for exploring FastDT. Explore our range of services to enhance your business.

Learn more about our services.