A Middleware for Go Programming Language to check for JWTs on HTTP requests
A Middleware for Go Programming Language to check for JWTs on HTTP requests
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 → ValidatorNative gRPC integration with interceptors for unary and streaming methods:
…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…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"
}…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:
…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:
…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:
…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.
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:
…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.
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 ( 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:
Host, X-Forwarded-Host, or Origin may be influenced by clients, proxies, or load balancers depending on your deployment setupiss claim from the token has not been signature-verified yet and must not be trusted by itselfIf yo
No open issues yet, or sync has not completed.