How to Implement JWT Authentication Middleware in Gin: A Complete Guide
Gin does not ship with built-in JWT authentication, but you can implement secure token validation by creating a custom HandlerFunc that extracts tokens from request headers, validates signatures using a library like golang-jwt/jwt, and stores parsed claims in the gin.Context for downstream handlers.
While the gin-gonic/gin framework provides a high-performance HTTP router and middleware stack, it leaves authentication implementation to the developer. Implementing JWT authentication middleware in Gin requires leveraging the framework's HandlerFunc pattern and understanding the request lifecycle defined in the core source files. This guide demonstrates how to build production-ready JWT middleware using the same patterns found in Gin's built-in BasicAuth implementation.
Understanding Gin's Middleware Architecture
Before writing authentication logic, you need to understand how Gin processes requests through handler chains. The framework's middleware system is orchestrated by the Engine type in gin.go and the Context type in context.go.
The HandlerFunc Pattern
Gin middleware uses the HandlerFunc type, defined as func(*gin.Context). In gin.go, the Engine.Use method appends these functions to the HandlersChain. When a request arrives, Engine.handleHTTPRequest creates a fresh Context and executes the chain sequentially, passing the same context pointer to each handler.
Flow Control with Abort and Next
The gin.Context provides two critical methods for middleware control flow:
c.Next()– Hands control to the next handler in the chainc.Abort()orc.AbortWithStatus()– Stops the chain immediately and writes the response
This pattern mirrors the implementation in auth.go, where the built-in BasicAuth middleware calls c.Set(AuthUserKey, user) on success or c.AbortWithStatus(401) on failure. Your JWT middleware should follow this exact flow.
Building the JWT Authentication Middleware
A robust JWT middleware performs three distinct operations for every request: extraction, validation, and context storage. Each step maps to specific methods in the Gin source code.
Step 1: Extracting the JWT Token
Tokens typically arrive in the Authorization header as Bearer <token>, but may also appear in query parameters or cookies. Your middleware should support configurable extraction locations using c.GetHeader(), c.Query(), and c.Cookie() from context.go.
Step 2: Validating Token Signatures and Claims
After extraction, validate the token's cryptographic signature and standard claims (expiration, issuer, audience). Use the golang-jwt/jwt/v5 library to parse the token with your secret key. If validation fails, immediately call c.AbortWithStatusJSON(http.StatusUnauthorized, ...) to reject the request before it reaches route handlers.
Step 3: Storing Claims in the Gin Context
On successful validation, store the parsed claims in the context using c.Set(ClaimsKey, claims) as implemented in context.go lines 75-79. This allows route handlers to retrieve the authenticated user's identity with c.Get(ClaimsKey) without re-parsing the token or making database calls.
Complete JWT Middleware Implementation
Here is a production-ready implementation that follows the patterns from auth.go and leverages golang-jwt/jwt/v5:
// file: middleware/jwt.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// key under which the token claims are stored in the gin.Context.
const ClaimsKey = "jwtClaims"
// JWTAuth returns a gin.HandlerFunc that validates a JWT.
// secret – the HMAC secret (or RSA/ECDSA public key for asymmetric signing)
// tokenLookup – a comma‑separated list of places to look for the token.
// Supported prefixes: "header:", "query:", "cookie:".
// Example: "header:Authorization,query:token,cookie:jwt"
func JWTAuth(secret []byte, tokenLookup string) gin.HandlerFunc {
// Parse tokenLookup once during middleware creation.
lookups := parseTokenLookup(tokenLookup)
return func(c *gin.Context) {
// 1️⃣ Extract token.
rawToken := extractToken(c, lookups)
if rawToken == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized,
gin.H{"error": "missing or malformed JWT"})
return
}
// 2️⃣ Validate/Parse token.
claims := jwt.MapClaims{}
_, err := jwt.ParseWithClaims(rawToken, claims,
func(t *jwt.Token) (interface{}, error) {
// Ensure the signing method is HMAC (adjust if you use RSA/ECDSA).
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return secret, nil
})
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized,
gin.H{"error": "invalid JWT: " + err.Error()})
return
}
// 3️⃣ Store claims for downstream handlers.
c.Set(ClaimsKey, claims)
// Continue to the next handler.
c.Next()
}
}
// -------------------------------------------------------------------
// Helper functions – not part of the public middleware API.
// -------------------------------------------------------------------
// tokenLocation describes where to fetch the token from.
type tokenLocation struct {
kind string // "header", "query", "cookie"
name string // header name, query param, or cookie name
prefix string // optional prefix to strip (e.g., "Bearer ")
}
// parseTokenLookup creates a slice of tokenLocation from the comma‑separated string.
func parseTokenLookup(lookup string) []tokenLocation {
parts := strings.Split(lookup, ",")
locs := make([]tokenLocation, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
switch {
case strings.HasPrefix(p, "header:"):
h := strings.TrimPrefix(p, "header:")
// Most APIs use "Bearer " prefix.
locs = append(locs, tokenLocation{kind: "header", name: h, prefix: "Bearer "})
case strings.HasPrefix(p, "query:"):
q := strings.TrimPrefix(p, "query:")
locs = append(locs, tokenLocation{kind: "query", name: q})
case strings.HasPrefix(p, "cookie:"):
c := strings.TrimPrefix(p, "cookie:")
locs = append(locs, tokenLocation{kind: "cookie", name: c})
}
}
return locs
}
// extractToken walks through the configured locations and returns the first non‑empty token.
func extractToken(c *gin.Context, locs []tokenLocation) string {
for _, l := range locs {
switch l.kind {
case "header":
val := c.GetHeader(l.name)
if strings.HasPrefix(val, l.prefix) {
return strings.TrimPrefix(val, l.prefix)
}
if val != "" && l.prefix == "" {
return val
}
case "query":
if v := c.Query(l.name); v != "" {
return v
}
case "cookie":
if cookie, err := c.Cookie(l.name); err == nil {
return cookie
}
}
}
return ""
}
Integrating the Middleware into Your Application
Register the middleware using Engine.Use() as defined in gin.go, or apply it to specific route groups via RouterGroup.Use() from routergroup.go:
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"your-module/middleware" // adjust import path
)
func main() {
r := gin.Default()
// Example HMAC secret – in production load from env or a secret manager.
secret := []byte("my‑super‑secret‑key")
// Register JWT middleware globally or per‑group.
r.Use(middleware.JWTAuth(secret, "header:Authorization,query:token,cookie:jwt"))
// Protected endpoint – claims are available via c.Get(middleware.ClaimsKey).
r.GET("/profile", func(c *gin.Context) {
claims, exists := c.Get(middleware.ClaimsKey)
if !exists {
// Should never happen because the middleware aborts on missing/invalid token.
c.AbortWithStatus(http.StatusInternalServerError)
return
}
// Example: expose the "sub" (subject) claim.
sub := claims.(jwt.MapClaims)["sub"]
c.JSON(http.StatusOK, gin.H{"user": sub})
})
// Public endpoint – no JWT required.
r.GET("/public", func(c *gin.Context) {
c.String(http.StatusOK, "no auth needed")
})
if err := r.Run(":8080"); err != nil {
log.Fatalf("server error: %v", err)
}
}
Testing Your JWT Middleware
Verify your implementation using Gin's test utilities. The middleware_test.go file in the repository demonstrates how to test middleware ordering and abort behavior:
func TestJWTAuth(t *testing.T) {
r := gin.New()
secret := []byte("test-secret")
r.Use(middleware.JWTAuth(secret, "header:Authorization"))
r.GET("/protected", func(c *gin.Context) {
claims, _ := c.Get(middleware.ClaimsKey)
c.JSON(http.StatusOK, gin.H{"claims": claims})
})
// 1️⃣ Request without token → 401
w := PerformRequest(r, http.MethodGet, "/protected")
assert.Equal(t, http.StatusUnauthorized, w.Code)
// 2️⃣ Request with a valid token
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
jwt.MapClaims{"sub": "12345", "exp": time.Now().Add(time.Hour).Unix()})
signed, _ := token.SignedString(secret)
w = PerformRequest(r, http.MethodGet, "/protected",
// helper to set Authorization header
SetHeader("Authorization", "Bearer "+signed))
assert.Equal(t, http.StatusOK, w.Code)
// assert that the JSON body contains the "sub" claim, etc.
}
Summary
- Gin's middleware architecture relies on
HandlerFunctypes that manipulate thegin.Contextthrough methods defined incontext.goand orchestrated byEngine.handleHTTPRequestingin.go. - Implement JWT authentication middleware by extracting tokens from configurable locations (headers, query strings, or cookies), validating them with
jwt.ParseWithClaims, and aborting the chain withc.AbortWithStatusJSONon failure. - Store validated claims using
c.Set()to make authentication data available to downstream handlers viac.Get(), avoiding redundant token parsing. - Reference the built-in BasicAuth implementation in
auth.gofor the canonical pattern of credential validation, context storage, and request abortion used throughout thegin-gonic/gincodebase.
Frequently Asked Questions
Does Gin have built-in JWT authentication?
No, the gin-gonic/gin repository does not include JWT middleware in its standard library. The framework provides the HandlerFunc infrastructure in gin.go and flow control methods in context.go, but you must implement the JWT logic yourself following the patterns demonstrated in auth.go.
Where should I store the JWT secret key in a Gin application?
Never hardcode secrets in your source code. Load the HMAC secret or RSA public key from environment variables, a secrets manager, or a secure configuration file at runtime. Pass the secret as a parameter to your middleware factory function, as shown in the JWTAuth(secret []byte, ...) signature.
How do I handle token expiration in Gin JWT middleware?
The jwt.ParseWithClaims function automatically validates the exp (expiration) claim against the current time. If the token is expired, it returns an error that your middleware should catch, triggering c.AbortWithStatusJSON(http.StatusUnauthorized, ...) to reject the request with a 401 status code before it reaches your route handlers.
Can I use RSA or ECDSA instead of HMAC for JWT signing in Gin?
Yes. Modify the key function passed to jwt.ParseWithClaims to return an *rsa.PublicKey or *ecdsa.PublicKey instead of the HMAC secret byte slice. Update the signing method assertion from jwt.SigningMethodHMAC to jwt.SigningMethodRSA or jwt.SigningMethodECDSA. The Gin middleware structure and context handling remain identical regardless of the JWT signing algorithm.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →