# How to Implement JWT Authentication Middleware in Gin: A Complete Guide

> Learn how to implement JWT authentication middleware in Gin. Secure your API by creating custom handler functions for token validation and claim storage.

- Repository: [Gin-Gonic/gin](https://github.com/gin-gonic/gin)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/gin-gonic/gin/blob/main/gin.go) and the `Context` type in [`context.go`](https://github.com/gin-gonic/gin/blob/main/context.go).

### The HandlerFunc Pattern

Gin middleware uses the `HandlerFunc` type, defined as `func(*gin.Context)`. In [`gin.go`](https://github.com/gin-gonic/gin/blob/main/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 chain
- **`c.Abort()`** or **`c.AbortWithStatus()`** – Stops the chain immediately and writes the response

This pattern mirrors the implementation in [`auth.go`](https://github.com/gin-gonic/gin/blob/main/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`](https://github.com/gin-gonic/gin/blob/main/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`](https://github.com/gin-gonic/gin/blob/main/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`](https://github.com/gin-gonic/gin/blob/main/auth.go) and leverages `golang-jwt/jwt/v5`:

```go
// 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`](https://github.com/gin-gonic/gin/blob/main/gin.go), or apply it to specific route groups via `RouterGroup.Use()` from [`routergroup.go`](https://github.com/gin-gonic/gin/blob/main/routergroup.go):

```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`](https://github.com/gin-gonic/gin/blob/main/middleware_test.go) file in the repository demonstrates how to test middleware ordering and abort behavior:

```go
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 `HandlerFunc` types that manipulate the `gin.Context` through methods defined in [`context.go`](https://github.com/gin-gonic/gin/blob/main/context.go) and orchestrated by `Engine.handleHTTPRequest` in [`gin.go`](https://github.com/gin-gonic/gin/blob/main/gin.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 with `c.AbortWithStatusJSON` on failure.
- Store validated claims using `c.Set()` to make authentication data available to downstream handlers via `c.Get()`, avoiding redundant token parsing.
- Reference the built-in BasicAuth implementation in [`auth.go`](https://github.com/gin-gonic/gin/blob/main/auth.go) for the canonical pattern of credential validation, context storage, and request abortion used throughout the `gin-gonic/gin` codebase.

## 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`](https://github.com/gin-gonic/gin/blob/main/gin.go) and flow control methods in [`context.go`](https://github.com/gin-gonic/gin/blob/main/context.go), but you must implement the JWT logic yourself following the patterns demonstrated in [`auth.go`](https://github.com/gin-gonic/gin/blob/main/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.