Skip to content

auth0/go-jwt-middleware

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

336 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Go JWT Middleware

GoDoc Go Report Card License Release Codecov Tests Ask DeepWiki

πŸ“š Documentation β€’ πŸš€ Getting Started β€’ ✨ What's New in v3 β€’ πŸ’¬ Feedback

Documentation

  • 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.

What's New in v3

v3 introduces significant improvements while maintaining the simplicity and flexibility you expect:

🎯 Pure Options Pattern

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"),
)

πŸ” Enhanced JWT Library (lestrrat-go/jwx v3)

  • Better performance and security
  • Support for 14 signature algorithms (including EdDSA, ES256K)
  • Improved JWKS handling with automatic kid matching
  • Active maintenance and modern Go support

πŸ—οΈ Core-Adapter Architecture

Framework-agnostic validation logic that can be reused across HTTP, gRPC, and other transports:

HTTP Middleware β†’ Core Engine β†’ Validator

🎁 Type-Safe Claims with Generics

Use Go 1.24+ generics for compile-time type safety:

claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())

πŸ“Š Built-in Logging Support

Optional structured logging compatible with log/slog:

jwtmiddleware.New(
    jwtmiddleware.WithValidator(jwtValidator),
    jwtmiddleware.WithLogger(slog.Default()),
)

πŸ›‘οΈ Enhanced Security

  • RFC 6750 compliant error responses
  • Secure defaults (credentials required, clock skew = 0)
  • DPoP support (RFC 9449) for proof-of-possession tokens

πŸ”‘ DPoP (Demonstrating Proof-of-Possession)

Prevent token theft with proof-of-possession:

jwtmiddleware.New(
    jwtmiddleware.WithValidator(jwtValidator),
    jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
)

Getting Started

Requirements

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+

Installation

go get github.com/auth0/go-jwt-middleware/v3

Basic Usage

Simple Example with HMAC

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"
}

Production Example with JWKS and Auth0

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)
}

Testing the Server

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"
}

Advanced Usage

Custom Claims

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
	}
}

Optional Credentials

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))
}

Custom Token Extraction

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"),
	)),
)

URL Exclusions

Skip JWT validation for specific URLs:

middleware, err := jwtmiddleware.New(
	jwtmiddleware.WithValidator(jwtValidator),
	jwtmiddleware.WithExclusionUrls([]string{
		"/health",
		"/metrics",
		"/public",
	}),
)

Structured Logging

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),
)

Custom Error Handling

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),
)

Error Responses

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
Malformed token 401 invalid_token token_malformed
Invalid algorithm 401 invalid_token invalid_algorithm
Invalid signature 401 invalid_token invalid_signature
Expired token 401 invalid_token token_expired
Not yet valid (nbf/iat) 401 invalid_token token_not_yet_valid
Invalid issuer 401 invalid_token invalid_issuer
Invalid audience 401 invalid_token invalid_audience
Invalid claims 401 invalid_token invalid_claims
JWKS fetch failed 401 invalid_token jwks_fetch_failed
JWKS key not found 401 invalid_token jwks_key_not_found
Missing token 401 invalid_token β€”

Example response for an expired token:

{
  "error": "invalid_token",
  "error_description": "The access token expired",
  "error_code": "token_expired"
}

All error responses include RFC 6750 compliant WWW-Authenticate headers.

Available error code constants (on the jwtmiddleware package):

ErrorCodeTokenMalformed, ErrorCodeTokenExpired, ErrorCodeTokenNotYetValid, ErrorCodeInvalidSignature, ErrorCodeInvalidAlgorithm, ErrorCodeInvalidIssuer, ErrorCodeInvalidAudience, ErrorCodeInvalidClaims, ErrorCodeJWKSFetchFailed, ErrorCodeJWKSKeyNotFound

Clock Skew Tolerance

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),
)

DPoP (Demonstrating Proof-of-Possession)

v3 adds support for DPoP (RFC 9449), which provides proof-of-possession for access tokens. This prevents token theft and replay attacks.

DPoP Modes

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

Basic DPoP Setup

middleware, err := jwtmiddleware.New(
	jwtmiddleware.WithValidator(jwtValidator),
	jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPAllowed), // Default
)

Require DPoP for Maximum Security

middleware, err := jwtmiddleware.New(
	jwtmiddleware.WithValidator(jwtValidator),
	jwtmiddleware.WithDPoPMode(jwtmiddleware.DPoPRequired),
)

Behind a Proxy

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.

Multiple Issuers (Multi-Tenant Support)

Accept JWTs from multiple issuers simultaneously - perfect for multi-tenant SaaS applications, domain migrations, or enterprise deployments.

When to Use What

Choose the right issuer validation approach for your use case:

Approach When to Use Example Use Case
WithIssuer (single) You have one Auth0 tenant or identity provider Simple API with single Auth0 tenant
WithIssuers (static list) You have a fixed set of issuers known at startup - Small number of tenants (< 10)
- Rarely changing issuer list
- Domain migration (old + new)
WithIssuersResolver (dynamic) Issuers determined at request time from database/context - Multi-tenant SaaS with 100s+ tenants
- Tenant-specific issuer configuration
- Dynamic tenant onboarding

Performance Comparison:

  • Single Issuer: ~1ms validation (fastest, no issuer lookup)
  • Static Multiple: ~1ms validation (in-memory list check, very fast)
  • Dynamic Resolver: ~1-5ms validation (with caching), ~10-20ms (cache miss with DB query)

πŸ’‘ Recommendation: Start with WithIssuer or WithIssuers if possible. Only use WithIssuersResolver if you need dynamic tenant-based resolution.

Choosing the Right JWKS Provider

Use the correct JWKS provider based on your issuer validation approach:

JWKS Provider Use With Why
CachingProvider WithIssuer (single issuer) Optimized for single issuer, simpler configuration
MultiIssuerProvider WithIssuers or WithIssuersResolver Handles dynamic JWKS routing per issuer, lazy loading

⚠️ Important:

  • Using CachingProvider with multiple issuers won't work correctly - it only caches JWKS for one issuer
  • Using MultiIssuerProvider with a single issuer works but adds unnecessary overhead
  • Always pair your issuer validation method with the appropriate provider

Example Mismatch (❌ Don't do this):

// WRONG: CachingProvider can't handle multiple issuers
provider := jwks.NewCachingProvider(jwks.WithIssuerURL(url))
validator.New(
    validator.WithIssuers([]string{"issuer1", "issuer2"}), // Won't work!
    validator.WithKeyFunc(provider.KeyFunc),
)

Correct Usage (βœ… Do this):

// RIGHT: MultiIssuerProvider for multiple issuers
provider := jwks.NewMultiIssuerProvider()
validator.New(
    validator.WithIssuers([]string{"issuer1", "issuer2"}),
    validator.WithKeyFunc(provider.KeyFunc),
)

Static Multiple Issuers

Configure a fixed list of allowed issuers:

// Use MultiIssuerProvider for automatic JWKS routing
provider, err := jwks.NewMultiIssuerProvider(
	jwks.WithMultiIssuerCacheTTL(5*time.Minute),
)

jwtValidator, err := validator.New(
	validator.WithKeyFunc(provider.KeyFunc),
	validator.WithAlgorithm(validator.RS256),
	validator.WithIssuers([]string{  // Multiple issuers!
		"https://tenant1.auth0.com/",
		"https://tenant2.auth0.com/",
		"https://tenant3.auth0.com/",
	}),
	validator.WithAudience("your-api-identifier"),
)

Available Options:

Option Description Default
WithMultiIssuerCacheTTL JWKS cache refresh interval 15 minutes
WithMultiIssuerHTTPClient Custom HTTP client for JWKS fetching 30s timeout
WithMultiIssuerCache Custom cache implementation (e.g., Redis) In-memory
WithMaxProviders Maximum issuer providers to cache 100

Large-Scale Multi-Tenant (100+ Tenants)

For applications with many tenants, use Redis and LRU eviction to manage memory:

// Create Redis cache (see examples/http-multi-issuer-redis-example)
redisCache := &RedisCache{
	client: redis.NewClient(&redis.Options{Addr: "localhost:6379"}),
	ttl:    5 * time.Minute,
}

// Configure provider with Redis and LRU eviction
provider, err := jwks.NewMultiIssuerProvider(
	jwks.WithMultiIssuerCacheTTL(5*time.Minute),
	jwks.WithMultiIssuerCache(redisCache),  // Share JWKS across instances
	jwks.WithMaxProviders(1000),            // Keep max 1000 providers in memory
)

jwtValidator, err := validator.New(
	validator.WithKeyFunc(provider.KeyFunc),
	validator.WithAlgorithm(validator.RS256),
	validator.WithIssuers(allowedIssuers),  // Your tenant list
	validator.WithAudience("your-api-identifier"),
)

Why Redis for 100+ Tenants?

  • πŸ“¦ Shared Cache: JWKS data shared across multiple application instances
  • πŸ’Ύ Memory Efficiency: Offload JWKS storage from application memory
  • πŸ”„ Automatic Expiry: Redis handles TTL and eviction
  • πŸ“ˆ Scalability: Handles thousands of tenants without memory bloat

Dynamic Issuer Resolution

Determine allowed issuers at request time based on context (tenant ID, database, etc.):

// For many tenants, use Redis and limit cached providers
provider, err := jwks.NewMultiIssuerProvider(
	jwks.WithMultiIssuerCache(redisCache),  // Optional: Redis for JWKS caching
	jwks.WithMaxProviders(500),             // Optional: LRU limit for memory control
)

jwtValidator, err := validator.New(
	validator.WithKeyFunc(provider.KeyFunc),
	validator.WithAlgorithm(validator.RS256),
	validator.WithIssuersResolver(func(ctx context.Context) ([]string, error) {
		// Extract tenant from context (set by your middleware)
		tenantID, _ := ctx.Value("tenant").(string)

		// Check cache (user-managed caching for optimal performance)
		if cached, found := cache.Get(tenantID); found {
			return cached, nil
		}

		// Query database for tenant's allowed issuers
		issuers, err := database.GetIssuersForTenant(ctx, tenantID)
		if err != nil {
			return nil, err
		}

		// Cache for 5 minutes
		cache.Set(tenantID, issuers, 5*time.Minute)
		return issuers, nil
	}),
	validator.WithAudience("your-api-identifier"),
)

Key Features:

  • πŸ”’ Security: Issuer validated BEFORE fetching JWKS (prevents SSRF attacks)
  • ⚑ Performance: Per-issuer JWKS caching with lazy loading
  • 🎯 Flexibility: User-controlled caching strategy (in-memory, Redis, etc.)
  • πŸ”„ Thread-Safe: Concurrent request handling with double-checked locking

Use Cases:

  • Multi-tenant SaaS applications
  • Domain migration (support old and new domains simultaneously)
  • Enterprise deployments with multiple Auth0 tenants
  • Connected accounts from different identity providers

See the multi-issuer examples for complete working code.

Security Requirements for Multiple Custom Domains (MCD)

When using WithIssuers or WithIssuersResolver to support Multiple Custom Domains (MCD), you are responsible for ensuring that only trusted issuer domains are returned.

Misconfiguring the issuer list or resolver is a critical security risk. It can cause the middleware to:

  • Accept access tokens from unintended issuers
  • Make discovery or JWKS requests to unintended domains

Note for Auth0 users: The WithIssuers / WithIssuersResolver configuration for MCD is intended only for multiple custom domains that belong to the same Auth0 tenant. It is not a supported mechanism for connecting multiple Auth0 tenants to a single API. Each domain in your issuer list should resolve to the same underlying Auth0 tenant.

Dynamic Resolver Warning: If your WithIssuersResolver function uses request-derived values (such as headers, query parameters, or path segments), do not trust those values directly. Use them only to map known and expected request values to a fixed allowlist of issuer domains that you control.

In particular:

  • Request headers like Host, X-Forwarded-Host, or Origin may be influenced by clients, proxies, or load balancers depending on your deployment setup
  • The unverified iss claim from the token has not been signature-verified yet and must not be trusted by itself

If your deployment relies on reverse proxies or load balancers, ensure that host-related request information is treated as trusted only when it comes from trusted infrastructure. Misconfigured proxy handling can cause the middleware to trust unintended issuer domains.

Example of a safe resolver pattern:

The resolver receives context.Context with the unverified iss claim available via validator.IssuerFromContext. Use this only to check membership in a fixed allowlist, never as a trusted value for logging, database queries, or external calls.

// allowedIssuers is a fixed set of trusted issuer URLs.
var allowedIssuers = map[string]bool{
    "https://brand-a.auth.example.com/":    true,
    "https://brand-a-jp.auth.example.com/": true,
    "https://brand-b.auth.example.com/":    true,
}

validator.WithIssuersResolver(func(ctx context.Context) ([]string, error) {
    // Return the full allowlist. The middleware will check the token's
    // iss claim against this list before making any JWKS requests.
    issuers := make([]string, 0, len(allowedIssuers))
    for iss := range allowedIssuers {
        issuers = append(issuers, iss)
    }
    return issuers, nil
})

The resolver's context.Context does not automatically include HTTP request information. If your resolver needs request-derived values (such as the host or tenant ID), an upstream middleware must explicitly set them in the context before the JWT middleware runs. Without that, the context will only contain the unverified iss claim via validator.IssuerFromContext.

For per-request filtering (e.g., tenant-scoped resolution), use trusted context values set by upstream middleware:

validator.WithIssuersResolver(func(ctx context.Context) ([]string, error) {
    // tenantID must be set by a trusted upstream middleware,
    // not derived from the token or untrusted request headers.
    tenantID, ok := ctx.Value(tenantContextKey).(string)
    if !ok {
        return nil, fmt.Errorf("missing tenant in context")
    }

    switch tenantID {
    case "brand-a":
        return []string{
            "https://brand-a.auth.example.com/",
            "https://brand-a-jp.auth.example.com/",
        }, nil
    case "brand-b":
        return []string{
            "https://brand-b.auth.example.com/",
        }, nil
    default:
        return nil, fmt.Errorf("unknown tenant: %s", tenantID)
    }
})

Examples

For complete working examples, check the examples directory:

Supported Algorithms

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)

Symmetric vs Asymmetric: When to Use What

Choose the right algorithm type based on your use case:

Algorithm Type Key Distribution When to Use Example Use Case
Symmetric (HMAC) Shared secret between issuer and API - Simple single-service architecture
- You control both token creation and validation
- Internal microservices communication
Backend API validating its own tokens
Asymmetric (RS256, ES256, EdDSA) Public/private key pair (issuer has private, API has public) - Production with Auth0 or external OAuth providers
- Multiple services validating tokens
- You don't control token creation
- Security-critical applications
Production API with Auth0, Okta, or any OAuth provider

πŸ” Security Recommendations:

  1. For Production with Auth0/OAuth providers: Use RS256 (default)

    • Industry standard for OAuth 2.0 and OpenID Connect
    • Auth0 uses RS256 by default
    • Public key rotation supported via JWKS
    • No shared secrets to manage
  2. For Modern Applications: Consider EdDSA

    • Fastest signing and verification
    • Smaller signatures (better bandwidth)
    • Immune to timing attacks
    • Supported by Auth0 (must enable in dashboard)
  3. For Internal Services Only: HMAC is acceptable

    • Simplest to configure (just a shared secret)
    • Fast performance
    • ⚠️ But: Secret must be protected and distributed securely
  4. Avoid using HMAC with external identity providers

    • Can't use with Auth0/Okta (they use asymmetric keys)
    • Shared secret is a security risk at scale

Example Configurations:

Production with Auth0 (RS256):

provider, _ := jwks.NewCachingProvider(jwks.WithIssuerURL(issuerURL))
validator.New(
    validator.WithKeyFunc(provider.KeyFunc),
    validator.WithAlgorithm(validator.RS256), // Standard for OAuth
    validator.WithIssuer(issuerURL.String()),
    validator.WithAudience("your-api"),
)

Internal microservices (HMAC):

validator.New(
    validator.WithKeyFunc(func(ctx context.Context) (any, error) {
        return []byte(os.Getenv("JWT_SECRET")), nil
    }),
    validator.WithAlgorithm(validator.HS256), // Simple shared secret
    validator.WithIssuer("internal-service"),
    validator.WithAudience("my-api"),
)

High-security applications (EdDSA):

provider, _ := jwks.NewCachingProvider(jwks.WithIssuerURL(issuerURL))
validator.New(
    validator.WithKeyFunc(provider.KeyFunc),
    validator.WithAlgorithm(validator.EdDSA), // Modern, fast, secure
    validator.WithIssuer(issuerURL.String()),
    validator.WithAudience("your-api"),
)

Migration from v2

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

Feedback

Contributing

We appreciate feedback and contribution to this repo! Before you get started, please see the following:

Raise an issue

To provide feedback or report a bug, please raise an issue on our issue tracker.

Vulnerability Reporting

Please do not report security vulnerabilities on the public Github issue tracker. The Responsible Disclosure Program details the procedure for disclosing security issues.


Auth0 Logo

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.

About

A Middleware for Go Programming Language to check for JWTs on HTTP requests

Topics

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors