Browse by type

📚 Documentation • 🚀 Getting Started • ✨ What's New in v3 • 💬 Feedback
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"),
)
kid matchingFramework-agnostic validation logic that can be reused across HTTP, gRPC, and other transports:
HTTP Middleware → Core Engine → Validator
gRPC Interceptor → Core Engine → Validator
Native gRPC integration with interceptors for unary and streaming methods:
import (
jwtgrpc "github.com/auth0/go-jwt-middleware/v3/integrations/grpc"
"github.com/auth0/go-jwt-middleware/v3/validator"
"google.golang.org/grpc"
)
// Create validator (same as HTTP)
jwtValidator, _ := validator.New(
validator.WithKeyFunc(keyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer("https://issuer.example.com/"),
validator.WithAudience("my-api"),
)
// Create gRPC interceptor
interceptor, _ := jwtgrpc.New(
jwtgrpc.WithValidator(jwtValidator),
jwtgrpc.WithExcludedMethods("/health.Check"),
)
// Register with gRPC server
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(interceptor.UnaryServerInterceptor()),
grpc.StreamInterceptor(interceptor.StreamServerInterceptor()),
)
The gRPC interceptor also supports Multiple Custom Domains (MCD) with WithIssuers or WithIssuersResolver, and automatic JWKS routing via MultiIssuerProvider. See the gRPC integration docs for details.
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()),
)
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 get github.com/auth0/go-jwt-middleware/v3
package 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:3000
This 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:3000
Response:
{
"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:3000
Response:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token is malformed"
{
"error": "invalid_token",
"error_description": "The access token is malformed",
"error_code": "token_malformed"
}
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 *jwtmiddleware.ValidationError
if errors.As(err, &validationErr) {
switch validationErr.Code {
case jwtmiddleware.ErrorCodeTokenExpired:
http.Error(w, "Token expired", http.StatusUnauthorized)
case jwtmiddleware.ErrorCodeInvalidIssuer:
http.Error(w, "Untrusted issuer", http.StatusUnauthorized)
case jwtmiddleware.ErrorCodeInvalidAudience:
http.Error(w, "Audience mismatch", http.StatusUnauthorized)
case jwtmiddleware.ErrorCodeInvalidSignature:
http.Error(w, "Invalid signature", 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),
)
The default error handler returns specific HTTP status codes and structured JSON responses for each type of JWT validation failure:
| Failure | HTTP Status | error |
error_code |
|---|---|---|---|
| Mal |
$ claude mcp add go-jwt-middleware \
-- python -m otcore.mcp_server <graph>