Securing the Admin Layer in Go with JWT: A Step-by-Step Integration Guide 🔐
In modern web applications, securing administrative endpoints is essential. In this article, we’ll walk through how to integrate JWT (JSON Web Token) authentication into the admin layer of a Go project using the Gin framework. We’ll use the go-simple project as our working example.
Go-simple
go-simple is a clean, modular Go project designed to handle HTTP and gRPC requests with clarity and maintainability. It’s ideal for developers who want to explore idiomatic Go patterns, experiment with scalable architecture, and integrate modern tooling like Uber FX, Buf, and gRPC-Gateway.
🎯 Project Goals
- Simplicity: Minimal boilerplate, clear separation of concerns, and intuitive structure.
- Modularity: Organized by domain, not just layers — making it easy to scale and refactor.
- Extensibility: Built to support JWT authentication, Swagger documentation, and lifecycle hooks.
- Real-world readiness: Includes patterns for configuration management, dependency injection, and testing.
🧱 Project Structure Overview
The go-simple project follows a modular design with clear separation of concerns. The admin product controller is defined in:
internal/product/controller/admin.goConfiguration is loaded via Viper in:
internal/config/config.go🔐 Step 1: Extend Configuration for JWT
First, we add JWT-related fields to the Config struct:
type Config struct {
// existing fields...
JWTSecret string
JWTExpiryHours int
}Then, in NewConfig():
v.SetDefault("JWT_SECRET", "your-secret-key")v.SetDefault("JWT_EXPIRY_HOURS", 72)cfg := &Config{
// existing fields... JWTSecret: v.GetString("JWT_SECRET"), JWTExpiryHours:
v.GetInt("JWT_EXPIRY_HOURS"),}🧪 Step 2: Token Generation
In internal/auth/token.go, we update the GenerateToken function to use the config:
func GenerateToken(cfg *config.Config, adminID string) (string, error) {
claims := jwt.MapClaims{
"admin_id": adminID, "exp": time.Now().Add(time.Hour *
time.Duration(cfg.JWTExpiryHours)).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(cfg.JWTSecret))
}🧱 Step 3: JWT Middleware for Gin
In internal/middleware/jwt.go, we define a Gin-compatible middleware:
func JWTAuth(cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization") if authHeader == "" ||
!strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Missing or invalid token"
}) return
}
tokenStr := strings.TrimPrefix(authHeader, "Bearer ") token, err := jwt.Parse(tokenStr,
func(token *jwt.Token) (interface{
}, error) {
return []byte(cfg.JWTSecret), nil
}) if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token"
}) return
}
c.Next()
}
}🔗 Step 4: Protect Admin Routes
In admin.go, we inject the middleware into the route group:
func (c *AdminProduct) RegisterRoutes(rg *gin.RouterGroup, cfg *config.Config) {
auth := middleware.JWTAuth(cfg)
rg.POST("/", auth, c.CreateProduct)
rg.PUT("/:id", auth, c.UpdateProduct)
rg.DELETE("/:id", auth, c.DeleteProduct)
rg.GET("/:id", auth, c.GetProductByID)
rg.GET("/", auth, c.ListProducts)
}🧪 Step 5: Authenticated Integration Tests
We updated all test helpers to include the JWT token in the Authorization header:
✅ Create Product
req.Header.Set("Authorization", "Bearer "+token)✅ List Products
req, _ := http.NewRequest(http.MethodGet, addr, nil)req.Header.Set("Authorization", "Bearer "+token)✅ Get Product by ID
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%d", addr, product.ID),
nil)req.Header.Set("Authorization", "Bearer "+token)✅ Update Product
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/%d", addr, product.ID),
body)req.Header.Set("Authorization", "Bearer "+token)✅ Delete Product
req, _ := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/%d", addr, product.ID),
nil)req.Header.Set("Authorization", "Bearer "+token)✅ Verify Deletion
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%d", addr, product.ID),
nil)req.Header.Set("Authorization", "Bearer "+token)🧩 Final Thoughts
This integration ensures that all admin endpoints are protected by JWT, with configuration-driven secrets and expiration. The test suite now mirrors real-world usage by authenticating each request, improving both security and test fidelity.
If you’re building a scalable Go service, this pattern — modular, idiomatic, and secure — lays a strong foundation for future enhancements like role-based access, refresh tokens, and OAuth integration.
Git commit
Add Swagger
🔐 How to Add JWT Authentication to Swagger in Go
If you’re building a secure API in Go and using Swagger for documentation, adding JWT (JSON Web Token) support to Swagger UI is a must. It allows developers to test protected endpoints directly from the browser by authorizing with a token.
This guide walks you through integrating JWT Bearer authentication into Swagger using swaggo/swag and gin-swagger.
✍️ Step 1: Define JWT Security Scheme
In your main Swagger annotations file (usually main.go), add the following block:
// @securityDefinitions.apikey BearerAuth// @in header// @name Authorization// @description Enter
your JWT token in the format: Bearer <token>🔍 What This Does
- Declares a security scheme named
BearerAuth - Tells Swagger to send the token in the
Authorizationheader - Adds a helpful description to guide users on formatting
🔐 Step 2: Annotate Protected Endpoints
For each route that requires JWT authentication, add:
// @Security BearerAuthExample:
// @Summary Create a new product// @Description Admin-only endpoint to create a product// @Tags
Admin// @Accept json// @Produce json// @Param product body dto.AdminCreateProductRequest true
"Product info"// @Success 201 {object} dto.ProductResponse// @Security BearerAuth// @Router
/admin/products [post]Repeat this for all admin routes: POST, PUT, DELETE, GET /:id, and GET /.
🚀 Step 3: Serve Swagger UI
Make sure Swagger UI is wired into your Gin router:
import ginSwagger "github.com/swaggo/gin-swagger"
import swaggerFiles "github.com/swaggo/files"r.GET("/swagger/*any",
ginSwagger.WrapHandler(swaggerFiles.Handler))🧪 Step 4: Test It
- Run your app and visit
/swagger/index.html - Click the 🔒 “Authorize” button
- Paste your token like this:
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Swagger will now include this token in all requests to protected endpoints
✅ Result
You now have a fully interactive Swagger UI that supports JWT authentication. This improves developer experience, makes testing easier, and ensures your API documentation reflects real-world usage.
