π 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 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 |
|---|---|---|---|
| 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
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.
Accept JWTs from multiple issuers simultaneously - perfect for multi-tenant SaaS applications, domain migrations, or enterprise deployments.
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.
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 |
- 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),
)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 |
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
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.
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)
}
})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
- 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) |
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"),
)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.
