auth0/go-jwt-middleware
auth0/go-jwt-middleware — 1.2k★ on GitHub (Go). A Middleware for Go Programming Language to check for JWTs on HTTP requests
Snapshot summary built from the project's own GitHub metadata — there's no written TopGit review yet. The page will update automatically when a full review is published.
TopGit writes full reviews for the most-starred, most-requested repositories. This page is a snapshot until then — see the READ ME tab for the original README in full.
Snapshot
Top contributors
Show top contributors

📚 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
kidmatching - 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
gRPC Interceptor → Core Engine → Validator
🚀 gRPC Support
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.
🎁 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.
On-Behalf-Of / Token Exchange (RFC 8693)
When an access token is issued through On-Behalf-Of Token Exchange (RFC 8693), for example an MCP server exchanging an incoming user token for one scoped to a downstream API, it carries an act (actor) claim describing who is acting on behalf of the user, along with azp and, for organization-bound tokens, org_id/org_name. These claims are parsed automatically during validation, so there is nothing extra to configure. This SDK validates and inspects such tokens; performing the exchange itself is done by your authorization server, not this middleware.
Per RFC 8693 §4.1, only the current actor (the outermost act.sub, the client that performed the most recent exchange) may be used for access-control decisions. The nested actors form a delegation chain that is informational only and intended for audit and logging. ValidatedClaims exposes helpers that keep this distinction clear:
claims, err := jwtValidator.ValidateToken(ctx, tokenString)
if err != nil {
// handle validation error
}
validated := claims.(*validator.ValidatedClaims)
// The user the request is being made on behalf of.
userID := validated.RegisteredClaims.Subject // "auth0|user123"
// The current actor: the client that performed the token exchange.
// Use ONLY this for authorization decisions.
if actor := validated.CurrentActor(); actor != "" && !authorizedActors[actor] {
http.Error(w, "actor not authorized", http.StatusForbidden)
return
}
// The full delegation chain, ordered from current actor to the original
// client. For audit/logging only; never use nested actors for authorization.
// Per RFC 8693 §4.1 verification does not fail on the actor claim, so a
// malformed chain (an actor with an empty sub) is reported here as
// validator.ErrMalformedDelegationChain rather than at ValidateToken. The
// subjects gathered before the break are still returned.
chain, err := validated.DelegationChain() // ["mcp_server_client_id", "spa_client_id"]
if err != nil {
// audit: the delegation chain was truncated at a malformed actor
}
// Organization context is preserved on org-bound exchanged tokens.
orgID := validated.RegisteredClaims.OrgID
Delegation chains are limited to 5 levels by default; a token whose act claim nests more actors than the limit is rejected with an invalid_claims validation error. You can change the limit with validator.WithMaxActorChainDepth.
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
CachingProviderwith multiple issuers won't work correctly - it only caches JWKS for one issuer - Using
MultiIssuerProviderwith 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/WithIssuersResolverconfiguration 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, orOriginmay be influenced by clients, proxies, or load balancers depending on your deployment setup - The unverified
issclaim 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:
- http-example - Basic HTTP server with HMAC
- http-jwks-example - Production setup with JWKS and Auth0
- http-multi-issuer-example - Multiple issuers with static list
- http-multi-issuer-redis-example - Multi-tenant with Redis cache and LRU eviction
- http-dynamic-issuer-example - Dynamic issuer resolution with caching
- 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
- grpc-example - gRPC server with JWT authentication
- gin-example - Integration with Gin framework
- echo-example - Integration with Echo framework
- iris-example - Integration with Iris framework
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:
-
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
-
For Modern Applications: Consider EdDSA
- Fastest signing and verification
- Smaller signatures (better bandwidth)
- Immune to timing attacks
- Supported by Auth0 (must enable in dashboard)
-
For Internal Services Only: HMAC is acceptable
- Simplest to configure (just a shared secret)
- Fast performance
- ⚠️ But: Secret must be protected and distributed securely
-
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:
- Contribution Guide
- Auth0's General Contribution Guidelines
- Auth0's Code of Conduct Guidelines
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 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.
Related repositories
Master programming by recreating your favorite technologies from scratch.
😎 Awesome lists about all kinds of interesting topics
Public APIs is a community-curated GitHub repository listing free, publicly accessible APIs across a wide range of categories, with auth type, HTTPS, and CORS noted for each entry. It's a browsable reference, not a library to install.
freeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.
Quick answers
Does auth0/go-jwt-middleware have a project website?
No homepage URL was recorded for auth0/go-jwt-middleware in TopGit's last sync. The README tab above frequently contains screenshots and demo links, or check the repository description on GitHub.
Is auth0/go-jwt-middleware open source?
Yes — auth0/go-jwt-middleware ships under the MIT license, which makes its source code freely readable (and, depending on license terms, forkable and reusable). Source: github.com/auth0/go-jwt-middleware.
What is auth0/go-jwt-middleware?
auth0/go-jwt-middleware (auth0/go-jwt-middleware) is a Go project on GitHub. From the project's own README: A Middleware for Go Programming Language to check for JWTs on HTTP requests
What license does auth0/go-jwt-middleware use?
auth0/go-jwt-middleware is released under the MIT license. Always verify the LICENSE file directly on GitHub for the authoritative terms — license strings can be edited out of sync with a project's actual stance.
Where do I read more about auth0/go-jwt-middleware?
This TopGit page is a snapshot — the READ ME tab shows the project's own README content (links stripped, images preserved). The GitHub repository at github.com/auth0/go-jwt-middleware is the definitive source.
Read full README in the tab above.