JWT Authentication With Fiber Middleware In Go

by Jhon Lennon 47 views

Securing your APIs is super important, and JSON Web Tokens (JWT) are a fantastic way to do just that. If you're building web applications with Go and using Fiber, you're in luck! Fiber is a fast and lightweight web framework that plays nicely with middleware. In this article, we'll explore how to implement JWT authentication using Fiber middleware. Let's dive in, guys!

What is JWT and Why Use It?

JWT (JSON Web Token) is a standard for securely transmitting information between parties as a JSON object. Think of it like a digital passport. This passport contains claims about the user, which can be verified to ensure the user is who they say they are. JWTs are compact, self-contained, and can be signed using a secret key or a public/private key pair.

So, why should you use JWT? There are several compelling reasons:

  1. Stateless Authentication: JWTs are stateless, meaning the server doesn't need to store session information. The token itself contains all the necessary information to authenticate the user. This makes your application more scalable.
  2. Scalability: Because JWTs are stateless, you can easily scale your application across multiple servers without worrying about session management.
  3. Security: JWTs can be signed to ensure that they haven't been tampered with. This provides a high level of security for your authentication system.
  4. Cross-Domain Authentication: JWTs can be used for authentication across different domains, making them ideal for single sign-on (SSO) implementations.
  5. Simplicity: Implementing JWT authentication is relatively straightforward, especially with libraries that handle the heavy lifting.

In essence, JWTs provide a secure, scalable, and flexible way to handle authentication in modern web applications. This is particularly useful in microservices architectures where services need to verify the identity of users or other services without relying on a central session store.

Setting Up a Fiber Project

Before we get into the JWT middleware, let's set up a basic Fiber project. If you haven't already, make sure you have Go installed. Then, create a new project directory and initialize a Go module:

mkdir go-fiber-jwt
cd go-fiber-jwt
go mod init go-fiber-jwt

Next, install Fiber and the go-jwt/jwt/v4 package, which we'll use for JWT handling:

go get github.com/gofiber/fiber/v2
go get github.com/golang-jwt/jwt/v4
go get github.com/joho/godotenv

Create a main.go file and a .env file. The .env file will store our secret key. Add the following line to your .env file:

JWT_SECRET=your-super-secret-key

Important: Replace your-super-secret-key with a strong, randomly generated secret key. Keep this key safe and never expose it in your code or commit it to your repository. Remember to add .env to your .gitignore file to prevent accidental commits.

Here’s a basic Fiber app setup in main.go:

package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/joho/godotenv"
)

func main() {

	app := fiber.New()

	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Hello, World!")
	})

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

Also, add this function to load the .env file. Make sure to call it in the main function before you define the Fiber app.

import "os"

func goDotEnvVariable(key string) string {

	// load .env file
	err := godotenv.Load()

	// handle errors

	if err != nil {
		log.Fatalf("Error loading .env file")
	}

	return os.Getenv(key)
}

This sets up a simple Fiber app that listens on port 3000 and returns "Hello, World!" when you visit the root path. Now, let's integrate JWT authentication.

Implementing JWT Middleware

First, we need functions to generate and verify JWTs. Let's create a jwt_utils.go file to handle these tasks.

package main

import (
	"fmt"
	"time"

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

// Generate new token
func GenerateNewAccessToken(userID string) (string, error) {

	// Set claims
	claims := jwt.MapClaims{
		"id":  userID,
		"exp": time.Now().Add(time.Minute * 15).Unix(),
	}

	// Create token

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// Generate encoded token and send it as response.

t, err := token.SignedString([]byte(goDotEnvVariable("JWT_SECRET")))

	if err != nil {
		return "", err
	}

	return t, nil
}

func JWTProtected() fiber.Handler {

	return func(c *fiber.Ctx) error {
		tokenString := c.Get("Authorization")

		if tokenString == "" {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
				"status":  "error",
				"message": "Missing Authorization header",
				"data":    nil,
			})
		}

		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			// Make sure token's signature hasn't been changed
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("Unexpected signing method: %s", token.Header["alg"])
			}

			// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
			return []byte(goDotEnvVariable("JWT_SECRET")), nil
		})

		if err != nil {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
				"status":  "error",
				"message": "Invalid token",
				"data":    nil,
			})
		}

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

		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"status":  "error",
			"message": "Invalid token",
			"data":    nil,
		})
	}
}

Generating JWTs

The GenerateNewAccessToken function takes a user ID and generates a new JWT. It sets the expiration time to 15 minutes. You can customize the claims and expiration time as needed. This function signs the token with your secret key. Remember that security depends critically on the secrecy of your key; never expose your key.

JWT Protected Middleware

The JWTProtected function is the Fiber middleware that protects your routes. It retrieves the token from the Authorization header, verifies the token, and sets the claims in the context's locals if the token is valid. If the token is missing or invalid, it returns an error.

Integrating JWT Middleware into Fiber

Now that we have our JWT utility functions, let's integrate the middleware into our Fiber application. Modify your main.go file to include the JWT middleware.

package main

import (
	"log"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/joho/godotenv"
)

func main() {

	godotenv.Load()

	app := fiber.New()

	// Middleware
	app.Use(logger.New())

	app.Get("/", func(c *fiber.Ctx) error {
		return c.SendString("Hello, World!")
	})

	app.Get("/protected", JWTProtected(), func(c *fiber.Ctx) error {
		claims := c.Locals("claims").(jwt.MapClaims)
		return c.JSON(claims)
	})

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

In this example, the /protected route is protected by the JWTProtected middleware. Only requests with a valid JWT in the Authorization header will be able to access this route. The handler for the /protected route retrieves the claims from the context and returns them as JSON. This is just an example; you'd typically use the claims to authorize the user's actions.

Testing the Authentication

To test the authentication, you'll need to generate a JWT first. You can create a new endpoint in your Fiber app to handle user login and generate the JWT. For simplicity, let's create a mock login endpoint.

Add the following code to main.go:

app.Post("/login", func(c *fiber.Ctx) error {
	userID := "123" // Replace with actual user authentication logic

	token, err := GenerateNewAccessToken(userID)
	 if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"status":  "error",
			"message": "Could not generate token",
			"data":    err,
		})
	}

	return c.JSON(fiber.Map{
		"status":  "success",
		"message": "Success!",
		"data": fiber.Map{
			"token": token,
		},
	})
})

Now, you can test the authentication by following these steps:

  1. Start the Fiber application: go run main.go
  2. Send a POST request to /login: This will return a JWT.
  3. Send a GET request to /protected with the Authorization header: Set the Authorization header to Bearer <your-jwt>. Replace <your-jwt> with the JWT you received from the /login endpoint.

If the JWT is valid, you'll receive the claims as JSON. If the JWT is missing or invalid, you'll receive an error.

Enhancements and Best Practices

Here are some enhancements and best practices to consider when implementing JWT authentication:

  • Refresh Tokens: Implement refresh tokens to allow users to refresh their JWTs without re-authenticating.
  • Token Revocation: Implement a mechanism to revoke tokens, for example, when a user logs out.
  • Secure Storage: Store your secret key securely. Consider using environment variables or a dedicated secret management system.
  • HTTPS: Always use HTTPS to protect your JWTs from being intercepted.
  • Input Validation: Validate user input to prevent vulnerabilities like SQL injection.
  • Rate Limiting: Implement rate limiting to prevent brute-force attacks.

By following these best practices, you can ensure that your JWT authentication system is secure and reliable.

Conclusion

Implementing JWT authentication with Fiber middleware in Go is a straightforward way to secure your APIs. By generating and verifying JWTs, you can ensure that only authenticated users have access to your protected routes. Remember to store your secret key securely and follow best practices to prevent vulnerabilities. Now go forth and secure your applications, guys!