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:
- Extract the token from the Authorization header
- Check the token in a memory store for quick validation
- Parse and validate the JWT
- Extract user information from claims
- Store the token in memory for future quick access
- 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:
- A simple user structure
- A login route that generates a JWT
- A middleware function to verify the JWT
- A protected route that can only be accessed with a valid JWT
To use this, you would:
- Start the server
- POST to
/loginwith a JSON body containing username and password - 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.