📚 Documentation • 🚀 Getting Started • ✨ What's New in v3 • 💬 Feedback
- Godoc - explore the go-jwt-middleware documentation.
- Docs site — explore our docs site and learn more about Auth0.
- Quickstart - our guide for adding go-jwt-middleware to your app.
- Migration Guide - upgrading from v2 to v3.
v3 introduces significant improvements while maintaining the simplicity and flexibility you expect:
All configuration through functional options for better IDE support and compile-time validation:
// v3: Clean, self-documenting API
validator.New(
validator.WithKeyFunc(keyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer("https://issuer.example.com/"),
validator.WithAudience("my-api"),
)- Better performance and security
- Support for 14 signature algorithms (including EdDSA, ES256K)
- Improved JWKS handling with automatic
kidmatching - Active maintenance and modern Go support
Framework-agnostic validation logic that can be reused across HTTP, gRPC, and other transports:
HTTP Middleware → Core Engine → Validator
Use Go 1.24+ generics for compile-time type safety:
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())Optional structured logging compatible with log/slog:
jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithLogger(slog.Default()),
)- RFC 6750 compliant error responses
- Secure defaults (credentials required, clock skew = 0)
- DPoP support (RFC 9449) for proof-of-possession tokens
Prevent token theft with proof-of-possession:
jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
)This library follows the same support policy as Go. The last two major Go releases are actively supported and compatibility issues will be fixed.
- Go 1.24+
go get github.com/auth0/go-jwt-middleware/v3package main
import (
"context"
"encoding/json"
"log"
"net/http"
"github.com/auth0/go-jwt-middleware/v3"
"github.com/auth0/go-jwt-middleware/v3/validator"
)
var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Type-safe claims retrieval with generics
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
if err != nil {
http.Error(w, "failed to get claims", http.StatusInternalServerError)
return
}
payload, err := json.Marshal(claims)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(payload)
})
func main() {
keyFunc := func(ctx context.Context) (any, error) {
// Our token must be signed using this secret
return []byte("secret"), nil
}
// Create validator with options pattern
jwtValidator, err := validator.New(
validator.WithKeyFunc(keyFunc),
validator.WithAlgorithm(validator.HS256),
validator.WithIssuer("go-jwt-middleware-example"),
validator.WithAudience("audience-example"),
)
if err != nil {
log.Fatalf("failed to set up the validator: %v", err)
}
// Create middleware with options pattern
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
)
if err != nil {
log.Fatalf("failed to set up the middleware: %v", err)
}
http.ListenAndServe("0.0.0.0:3000", middleware.CheckJWT(handler))
}Try it out:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnby1qd3QtbWlkZGxld2FyZS1leGFtcGxlIiwiYXVkIjoiYXVkaWVuY2UtZXhhbXBsZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW1lIjoidXNlcjEyMyJ9.XFhrzWzntyINkgoRt2mb8dES84dJcuOoORdzKfwUX70" \
http://localhost:3000This JWT is signed with secret and contains:
{
"iss": "go-jwt-middleware-example",
"aud": "audience-example",
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"username": "user123"
}package main
import (
"context"
"log"
"net/http"
"net/url"
"os"
"github.com/auth0/go-jwt-middleware/v3"
"github.com/auth0/go-jwt-middleware/v3/jwks"
"github.com/auth0/go-jwt-middleware/v3/validator"
)
func main() {
issuerURL, err := url.Parse("https://" + os.Getenv("AUTH0_DOMAIN") + "/")
if err != nil {
log.Fatalf("failed to parse issuer URL: %v", err)
}
// Create JWKS provider with caching
provider, err := jwks.NewCachingProvider(
jwks.WithIssuerURL(issuerURL),
)
if err != nil {
log.Fatalf("failed to create JWKS provider: %v", err)
}
// Create validator
jwtValidator, err := validator.New(
validator.WithKeyFunc(provider.KeyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer(issuerURL.String()),
validator.WithAudience(os.Getenv("AUTH0_AUDIENCE")),
)
if err != nil {
log.Fatalf("failed to set up the validator: %v", err)
}
// Create middleware
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
)
if err != nil {
log.Fatalf("failed to set up the middleware: %v", err)
}
// Protected route
http.Handle("/api/private", middleware.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, _ := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
w.Write([]byte("Hello, " + claims.RegisteredClaims.Subject))
})))
// Public route
http.HandleFunc("/api/public", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, anonymous user"))
})
log.Println("Server listening on :3000")
http.ListenAndServe(":3000", nil)
}After running the server (go run main.go), test with curl:
Valid Token:
$ curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnby1qd3QtbWlkZGxld2FyZS1leGFtcGxlIiwiYXVkIjoiYXVkaWVuY2UtZXhhbXBsZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW1lIjoidXNlcjEyMyJ9.XFhrzWzntyINkgoRt2mb8dES84dJcuOoORdzKfwUX70" localhost:3000Response:
{
"CustomClaims": null,
"RegisteredClaims": {
"iss": "go-jwt-middleware-example",
"aud": ["audience-example"],
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
}Invalid Token:
$ curl -v -H "Authorization: Bearer invalid.token.here" localhost:3000Response:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token is invalid"
{
"error": "invalid_token",
"error_description": "The access token is invalid"
}
Define and validate custom claims:
type CustomClaims struct {
Scope string `json:"scope"`
Permissions []string `json:"permissions"`
}
func (c *CustomClaims) Validate(ctx context.Context) error {
if c.Scope == "" {
return errors.New("scope is required")
}
return nil
}
// Use with validator
jwtValidator, err := validator.New(
validator.WithKeyFunc(keyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer("https://issuer.example.com/"),
validator.WithAudience("my-api"),
validator.WithCustomClaims(func() *CustomClaims {
return &CustomClaims{}
}),
)
// Access in handler
func handler(w http.ResponseWriter, r *http.Request) {
claims, _ := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
customClaims := claims.CustomClaims.(*CustomClaims)
if contains(customClaims.Permissions, "read:data") {
// User has permission
}
}Allow both authenticated and public access:
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithCredentialsOptional(true),
)
func handler(w http.ResponseWriter, r *http.Request) {
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
if err != nil {
// No JWT - serve public content
w.Write([]byte("Public content"))
return
}
// JWT present - serve authenticated content
w.Write([]byte("Hello, " + claims.RegisteredClaims.Subject))
}Extract tokens from cookies or query parameters:
// From cookie
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithTokenExtractor(jwtmiddleware.CookieTokenExtractor("jwt")),
)
// From query parameter
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithTokenExtractor(jwtmiddleware.ParameterTokenExtractor("token")),
)
// Try multiple sources
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithTokenExtractor(jwtmiddleware.MultiTokenExtractor(
jwtmiddleware.AuthHeaderTokenExtractor,
jwtmiddleware.CookieTokenExtractor("jwt"),
)),
)Skip JWT validation for specific URLs:
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithExclusionUrls([]string{
"/health",
"/metrics",
"/public",
}),
)Enable logging with log/slog or compatible loggers:
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithLogger(logger),
)Implement custom error responses:
func customErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("JWT error: %v", err)
if errors.Is(err, jwtmiddleware.ErrJWTMissing) {
http.Error(w, "No token provided", http.StatusUnauthorized)
return
}
var validationErr *core.ValidationError
if errors.As(err, &validationErr) {
switch validationErr.Code {
case core.ErrorCodeTokenExpired:
http.Error(w, "Token expired", http.StatusUnauthorized)
default:
http.Error(w, "Invalid token", http.StatusUnauthorized)
}
return
}
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithErrorHandler(customErrorHandler),
)Allow for time drift between servers:
jwtValidator, err := validator.New(
validator.WithKeyFunc(keyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer("https://issuer.example.com/"),
validator.WithAudience("my-api"),
validator.WithAllowedClockSkew(30*time.Second),
)v3 adds support for DPoP (RFC 9449), which provides proof-of-possession for access tokens. This prevents token theft and replay attacks.
| Mode | Description | Use Case |
|---|---|---|
| DPoPAllowed (default) | Accepts both Bearer and DPoP tokens | Migration period, backward compatibility |
| DPoPRequired | Only accepts DPoP tokens | Maximum security |
| DPoPDisabled | Ignores DPoP proofs, rejects DPoP scheme | Legacy systems |
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPAllowed), // Default
)middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
)When running behind a reverse proxy, configure trusted proxy headers:
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
jwtmiddleware.WithStandardProxy(), // Trust X-Forwarded-* headers
)See the DPoP examples for complete working code.
For complete working examples, check the examples directory:
- http-example - Basic HTTP server with HMAC
- http-jwks-example - Production setup with JWKS and Auth0
- http-dpop-example - DPoP support (allowed mode)
- http-dpop-required - DPoP required mode
- http-dpop-disabled - DPoP disabled mode
- http-dpop-trusted-proxy - DPoP behind reverse proxy
- gin-example - Integration with Gin framework
- echo-example - Integration with Echo framework
- iris-example - Integration with Iris framework
v3 supports 14 signature algorithms:
| Type | Algorithms |
|---|---|
| HMAC | HS256, HS384, HS512 |
| RSA | RS256, RS384, RS512 |
| RSA-PSS | PS256, PS384, PS512 |
| ECDSA | ES256, ES384, ES512, ES256K |
| EdDSA | EdDSA (Ed25519) |
See MIGRATION_GUIDE.md for a complete guide on upgrading from v2 to v3.
Key changes:
- Pure options pattern for all components
- Type-safe claims with generics
- New JWT library (lestrrat-go/jwx v3)
- Core-Adapter architecture
We appreciate feedback and contribution to this repo! Before you get started, please see the following:
To provide feedback or report a bug, please raise an issue on our issue tracker.
Please do not report security vulnerabilities on the public Github issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.

Auth0 is an easy to implement, adaptable authentication and authorization platform.
To learn more checkout Why Auth0?
This project is licensed under the MIT license. See the LICENSE file for more info.
