Secure Your Go REST API with JWT Auth Made Easy

Implementing a secure REST API is crucial for protecting sensitive data and ensuring only authorized users access your application. Fortunately, JSON Web Tokens (JWT) offer a straightforward way to add authentication to your Go REST API. In this guide, we’ll walk you through securing your Go REST API with JWT auth, making the process easy even for beginners. Whether you’re building a small project or a large-scale application, this step-by-step tutorial will help you understand and implement JWT authentication effectively.

go rest api jwt auth easy

Why Use JWT for Your Go REST API?

JWT, or JSON Web Token, is a popular method for securing APIs. It’s a compact, self-contained token that securely transmits information between parties. For instance, when a user logs in, the server generates a JWT, which the client includes in subsequent requests to prove their identity. As a result, JWT is ideal for stateless authentication in REST APIs.

Moreover, JWT is widely used because it’s:

  • Secure: It uses cryptographic signatures to verify authenticity.
  • Scalable: It works well in distributed systems, as tokens are self-contained.
  • Simple: It integrates easily with Go and other languages.

By securing your Go REST API with JWT, you ensure only authorized users access protected routes, keeping your application safe.

Prerequisites for This Tutorial

Before diving into the code, let’s ensure you have everything you need. This guide targets beginner to intermediate developers, so we’ll keep things simple yet comprehensive. You’ll need:

  • Basic knowledge of Go programming.
  • Go installed on your machine (version 1.18 or later recommended).
  • A code editor like VS Code.
  • Familiarity with REST APIs and HTTP requests.
  • A tool like Postman for testing API endpoints.

If you’re new to Go or REST APIs, don’t worry. We’ll explain each step clearly, including technical terms, to make JWT auth easy to understand.

Setting Up Your Go Project

To get started, let’s set up a basic Go project for your REST API. First, create a new directory for your project and initialize it as a Go module.

mkdir go-jwt-api
cd go-jwt-api
go mod init go-jwt-api

Next, install the necessary packages. For this tutorial, we’ll use:

  • gorilla/mux: A powerful router for handling HTTP requests.
  • dgrijalva/jwt-go: A popular library for JWT in Go.

Run these commands to install them:

go get github.com/gorilla/mux
go get github.com/dgrijalva/jwt-go

Now, you’re ready to build a simple Go REST API and secure it with JWT auth.

Also read about Spring Boot Audit Logs

Creating a Basic Go REST API

Let’s create a minimal REST API with a public and a protected endpoint. The public endpoint will handle user login, while the protected endpoint will require JWT authentication.

Here’s a basic Go REST API setup:

package main

import (
	"fmt"
	"log"
	"net/http"
	"github.com/gorilla/mux"
)

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/login", loginHandler).Methods("POST")
	r.HandleFunc("/protected", protectedHandler).Methods("GET")

	log.Fatal(http.ListenAndServe(":8080", r))
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Login endpoint")
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Protected endpoint")
}

This code sets up a server with two endpoints:

  • /login: A public endpoint for user login.
  • /protected: A restricted endpoint that will require JWT authentication.

However, the /protected endpoint isn’t secure yet. Let’s implement JWT to protect it.

Understanding JWT Structure

Before coding the authentication logic, let’s break down what a JWT is. A JWT consists of three parts:

  1. Header: Contains metadata, like the token type and signing algorithm.
  2. Payload: Holds claims, such as user ID and expiration time.
  3. Signature: Verifies the token’s authenticity using a secret key.

These parts are encoded in Base64 and separated by dots (.). For example: header.payload.signature. When a client sends a JWT, the server verifies the signature to ensure the token hasn’t been tampered with.

Implementing JWT Authentication

Now, let’s secure your Go REST API with JWT auth. We’ll create a login system that generates a JWT and a middleware to protect the /protected endpoint.

Step 1: Generate a JWT on Login

First, modify the loginHandler to generate a JWT when a user logs in. For simplicity, we’ll use hardcoded credentials, but in a real application, you’d verify credentials against a database.

Here’s the updated code:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"
	"github.com/dgrijalva/jwt-go"
	"github.com/gorilla/mux"
)

var secretKey = []byte("my-secret-key") // Replace with a secure key in production

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

func loginHandler(w http.ResponseWriter, r *http.Request) {
	var user User
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		http.Error(w, "Invalid request payload", http.StatusBadRequest)
		return
	}

	// Hardcoded credentials for demo purposes
	if user.Username != "admin" || user.Password != "password" {
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		return
	}

	// Create JWT token
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"username": user.Username,
		"exp":      time.Now().Add(time.Hour * 24).Unix(), // Token expires in 24 hours
	})

	tokenString, err := token.SignedString(secretKey)
	if err != nil {
		http.Error(w, "Error generating token", http.StatusInternalServerError)
		return
	}

	json.NewEncoder(w).Encode(map[string]string{"token": tokenString})
}

This code:

  • Accepts a JSON payload with username and password.
  • Verifies the credentials (hardcoded for simplicity).
  • Generates a JWT with a username claim and a 24-hour expiration.
  • Returns the JWT to the client.

Step 2: Create JWT Middleware

Next, we need middleware to verify the JWT for protected routes. The middleware checks the Authorization header for a valid token.

Here’s the middleware code:

func jwtMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tokenString := r.Header.Get("Authorization")
		if len(tokenString) == 0 {
			http.Error(w, "Missing Authorization Header", http.StatusUnauthorized)
			return
		}

		// Remove "Bearer " prefix if present
		tokenString = strings.TrimPrefix(tokenString, "Bearer ")

		// Parse and validate token
		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 secretKey, nil
		})

		if err != nil || !token.Valid {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}

		next.ServeHTTP(w, r)
	})
}

This middleware:

  • Checks for the Authorization header.
  • Extracts and validates the JWT.
  • Calls the next handler if the token is valid, or returns an error if it’s not.

Step 3: Secure the Protected Endpoint

Apply the middleware to the /protected endpoint. Update the main function:

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/login", loginHandler).Methods("POST")
	r.Handle("/protected", jwtMiddleware(http.HandlerFunc(protectedHandler))).Methods("GET")

	log.Fatal(http.ListenAndServe(":8080", r))
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Welcome to the protected endpoint!")
}

Now, the /protected endpoint is only accessible with a valid JWT.

Testing Your Secure Go REST API

To test your API, follow these steps:

  1. Start the server: go run main.go.
  2. Use Postman or curl to send a POST request to /login with this JSON body:{ "username": "admin", "password": "password" } You’ll receive a JWT in the response.
  3. Copy the JWT and send a GET request to /protected with the header:Authorization: Bearer <your-jwt-token> If the token is valid, you’ll see “Welcome to the protected endpoint!” Otherwise, you’ll get an error.

For example, using curl:

curl -X POST http://localhost:8080/login -d '{"username":"admin","password":"password"}'
curl -H "Authorization: Bearer <your-jwt-token>" http://localhost:8080/protected

Best Practices for JWT Authentication

To make your Go REST API with JWT auth more secure, consider these best practices:

  • Use a Strong Secret Key: Store your secret key in an environment variable, not in the code.
  • Set Short Token Expirations: Use short-lived tokens (e.g., 1 hour) and implement refresh tokens.
  • Use HTTPS: Always use HTTPS to encrypt data in transit.
  • Validate Claims: Check additional claims, like user roles, in your middleware.
  • Handle Errors Gracefully: Provide clear error messages without exposing sensitive details.

For more details on JWT best practices, refer to the official JWT documentation.

Common Issues and Troubleshooting

Here are some common problems and solutions:

  • Invalid Token Error: Ensure the Authorization header includes “Bearer” followed by the token.
  • Token Expired: Check the exp claim and generate a new token if needed.
  • Signature Mismatch: Verify that the secret key matches on both token creation and validation.

By addressing these issues, you can ensure your JWT auth works smoothly.

Full Code Example

Below is the complete Go code for your secure REST API with JWT authentication:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"
	"github.com/dgrijalva/jwt-go"
	"github.com/gorilla/mux"
)

var secretKey = []byte("my-secret-key") // Replace with a secure key in production

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

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/login", loginHandler).Methods("POST")
	r.Handle("/protected", jwtMiddleware(http.HandlerFunc(protectedHandler))).Methods("GET")

	log.Fatal(http.ListenAndServe(":8080", r))
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	var user User
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		http.Error(w, "Invalid request payload", http.StatusBadRequest)
		return
	}

	if user.Username != "admin" || user.Password != "password" {
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		return
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"username": user.Username,
		"exp":      time.Now().Add(time.Hour * 24).Unix(),
	})

	tokenString, err := token.SignedString(secretKey)
	if err != nil {
		http.Error(w, "Error generating token", http.StatusInternalServerError)
		return
	}

	json.NewEncoder(w).Encode(map[string]string{"token": tokenString})
}

func jwtMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		tokenString := r.Header.Get("Authorization")
		if len(tokenString) == 0 {
			http.Error(w, "Missing Authorization Header", http.StatusUnauthorized)
			return
		}

		tokenString = strings.TrimPrefix(tokenString, "Bearer ")

		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 secretKey, nil
		})

		if err != nil || !token.Valid {
			http.Error(w, "Invalid token", http.StatusUnauthorized)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Welcome to the protected endpoint!")
}

Conclusion

Securing your Go REST API with JWT auth is easier than it seems. By following this guide, you’ve learned how to generate JWTs, create middleware to protect endpoints, and test your API. Moreover, you’ve explored best practices to keep your API secure and scalable. With these tools, you can confidently build secure Go REST APIs for your projects.

For further learning, check out the official Go documentation or explore advanced JWT features like refresh tokens. Start implementing JWT auth today and make your Go REST API secure and reliable!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top