Implementing JWT Authentication with Go Fiber

Introduction

In this post, we’ll explore how to implement JSON Web Token (JWT) authentication in a Go Fiber application. We’ll use a real-world example from a project that combines Firebase, Firestore, and a custom memory store for efficient token management.

Prerequisites

  • Basic knowledge of Go programming
  • Familiarity with the Fiber web framework
  • Understanding of JWT concepts

Setting Up the Project

First, let’s look at the necessary imports and global variables:

import (
    // ... other imports ...
    "github.com/gofiber/fiber/v2"
    "github.com/golang-jwt/jwt/v4"
    // ... custom packages ...
)

var jwtSecret []byte
var config *utils.Config

Ensure you have a configuration file or environment variables to store your JWT secret.

Implementing the Verification Middleware

The heart of our JWT authentication is the VerifyJWT middleware. Here’s a breakdown of its functionality:

  1. Extract the token from the Authorization header
  2. Check the token in a memory store for quick validation
  3. Parse and validate the JWT
  4. Extract user information from claims
  5. Store the token in memory for future quick access
  6. Set user information in the Fiber context

Let’s examine the key parts of this middleware:

func VerifyJWT(c *fiber.Ctx) error {
    // Extract token from header
    authHeader := c.Get("Authorization")
    tokenStr := strings.Split(authHeader, " ")[1]

    // Check memory store first
    if cachedUserID, found, _ := ms.Get(tokenStr); found {
        c.Locals("userID", cachedUserID)
        return c.Next()
    }

    // Parse and validate token
    token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
        return jwtSecret, nil
    })

    // Extract claims and user ID
    claims, _ := token.Claims.(jwt.MapClaims)
    userID, _ := claims["jti"].(string)

    // Store in memory and set context
    ms.Set(tokenStr, userID, 60*time.Minute)
    c.Locals("userID", userID)

    return c.Next()
}

Using the Middleware

To protect routes with JWT authentication, apply the middleware like this:

router.Use(VerifyJWT)

// Protected routes
router.Get("/user/:userID", func(c *fiber.Ctx) error {
    return routes.GetUserByID(c, client)
})

Error Handling and Security Considerations

  • Always validate the token’s signing method
  • Handle errors gracefully and return appropriate status codes
  • Use HTTPS in production to protect tokens in transit
  • Implement token expiration and refresh mechanisms

Conclusion

Implementing JWT authentication in Go Fiber provides a robust and scalable solution for securing your web applications. By combining JWT with a memory store, we achieve both security and performance.

Remember to keep your JWT secret secure and consider implementing additional security measures like rate limiting and token revocation for a production-ready system.

Canonical Example: main.go

Here’s a complete, simplified example of a main.go file that demonstrates JWT authentication in a Go Fiber application:

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/gofiber/fiber/v2"
    "github.com/golang-jwt/jwt/v4"
)

var jwtSecret = []byte("your-secret-key")

type User struct {
    ID       string `json:"id"`
    Username string `json:"username"`
    Password string `json:"password"`
}

func main() {
    app := fiber.New()

    // Unprotected routes
    app.Post("/login", login)

    // JWT Middleware
    app.Use(verifyJWT)

    // Protected routes
    app.Get("/protected", protected)

    log.Fatal(app.Listen(":3000"))
}

func login(c *fiber.Ctx) error {
    user := new(User)
    if err := c.BodyParser(user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
    }

    // In a real application, you would verify the username and password against a database
    if user.Username == "admin" && user.Password == "password" {
        token := jwt.New(jwt.SigningMethodHS256)

        claims := token.Claims.(jwt.MapClaims)
        claims["username"] = user.Username
        claims["exp"] = time.Now().Add(time.Hour * 72).Unix()

        t, err := token.SignedString(jwtSecret)
        if err != nil {
            return c.SendStatus(fiber.StatusInternalServerError)
        }

        return c.JSON(fiber.Map{"token": t})
    }

    return c.SendStatus(fiber.StatusUnauthorized)
}

func verifyJWT(c *fiber.Ctx) error {
    authHeader := c.Get("Authorization")
    if authHeader == "" {
        return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing auth token"})
    }

    tokenString := authHeader[7:] // Remove "Bearer " prefix

    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return jwtSecret, nil
    })

    if err != nil {
        return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired token"})
    }

    if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
        c.Locals("username", claims["username"])
        return c.Next()
    }

    return c.SendStatus(fiber.StatusUnauthorized)
}

func protected(c *fiber.Ctx) error {
    username := c.Locals("username")
    return c.SendString(fmt.Sprintf("Welcome %s to the protected route!", username))
}

This example includes:

  1. A simple user structure
  2. A login route that generates a JWT
  3. A middleware function to verify the JWT
  4. A protected route that can only be accessed with a valid JWT

To use this, you would:

  1. Start the server
  2. POST to /login with a JSON body containing username and password
  3. Use the returned token in the Authorization header (prepended with “Bearer “) for subsequent requests to /protected

Remember to replace “your-secret-key” with a strong, unique secret in a real application, and never expose it in your code. Instead, use environment variables or a secure configuration management system.