-
Notifications
You must be signed in to change notification settings - Fork 209
Expand file tree
/
Copy pathdoc.go
More file actions
485 lines (370 loc) · 14.7 KB
/
doc.go
File metadata and controls
485 lines (370 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
/*
Package jwtmiddleware provides HTTP middleware for JWT authentication.
This package implements JWT authentication middleware for standard Go net/http
servers. It validates JWTs, extracts claims, and makes them available in the
request context. The middleware follows the Core-Adapter pattern, with this
package serving as the HTTP transport adapter.
# Quick Start
import (
"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() {
// Create JWKS provider
issuerURL, _ := url.Parse("https://your-domain.auth0.com/")
provider, err := jwks.NewCachingProvider(
jwks.WithIssuerURL(issuerURL),
)
if err != nil {
log.Fatal(err)
}
// Create validator
jwtValidator, err := validator.New(
validator.WithKeyFunc(provider.KeyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer(issuerURL.String()),
validator.WithAudience("your-api-identifier"),
)
if err != nil {
log.Fatal(err)
}
// Create middleware
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
)
if err != nil {
log.Fatal(err)
} // Use with your HTTP server
http.Handle("/api/", middleware.CheckJWT(apiHandler))
http.ListenAndServe(":8080", nil)
}
Security: The validator automatically validates exp (expiration time) and nbf (not
before) claims. You don't need to check these yourself - the middleware is secure
by default.
# Accessing Claims
Use the type-safe generic helpers to access claims in your handlers:
func apiHandler(w http.ResponseWriter, r *http.Request) {
// Type-safe claims retrieval
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Access claims
fmt.Fprintf(w, "Hello, %s!", claims.RegisteredClaims.Subject)
}
Alternative: Check if claims exist without retrieving them:
if jwtmiddleware.HasClaims(r.Context()) {
// Claims are present
}
v2 compatibility (type assertion):
claimsValue := r.Context().Value(jwtmiddleware.ContextKey{})
if claimsValue == nil {
// No claims
}
claims := claimsValue.(*validator.ValidatedClaims)
# Configuration Options
All configuration is done through functional options:
Required:
- WithValidator: A configured validator instance
Optional:
- WithCredentialsOptional: Allow requests without JWT
- WithValidateOnOptions: Validate JWT on OPTIONS requests
- WithErrorHandler: Custom error response handler
- WithTokenExtractor: Custom token extraction logic
- WithExclusionUrls: URLs to skip JWT validation
- WithLogger: Structured logging (compatible with log/slog)
# Optional Credentials
Allow requests without JWT (useful for public + authenticated endpoints):
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 provided - serve public content
fmt.Fprintln(w, "Public content")
return
}
// JWT provided - serve authenticated content
fmt.Fprintf(w, "Hello, %s!", claims.RegisteredClaims.Subject)
}
# Custom Error Handling
Implement custom error responses:
func myErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("JWT error: %v", err)
// Check error type
if errors.Is(err, jwtmiddleware.ErrJWTMissing) {
http.Error(w, "No token provided", http.StatusUnauthorized)
return
}
// Check for ValidationError
var validationErr *core.ValidationError
if errors.As(err, &validationErr) {
switch validationErr.Code {
case core.ErrorCodeTokenExpired:
http.Error(w, "Token expired", 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(myErrorHandler),
)# Token Extraction
Default: Authorization header with Bearer scheme
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Custom extractors:
From Cookie:
extractor := jwtmiddleware.CookieTokenExtractor("jwt")
From Query Parameter:
extractor := jwtmiddleware.ParameterTokenExtractor("token")
Multiple Sources (tries in order):
extractor := jwtmiddleware.MultiTokenExtractor(
jwtmiddleware.AuthHeaderTokenExtractor,
jwtmiddleware.CookieTokenExtractor("jwt"),
)
Use with middleware:
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithTokenExtractor(extractor),
)# URL Exclusions
Skip JWT validation for specific URLs:
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithExclusionUrls([]string{
"/health",
"/metrics",
"/public",
}),
)# Logging
Enable structured logging (compatible with log/slog):
import "log/slog"
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
middleware, err := jwtmiddleware.New(
jwtmiddleware.WithValidator(jwtValidator),
jwtmiddleware.WithLogger(logger),
)Logs will include:
- Token extraction attempts
- Validation success/failure with timing
- Excluded URLs
- OPTIONS request handling
# Error Responses
The DefaultErrorHandler provides RFC 6750 compliant error responses:
401 Unauthorized (missing token):
{
"error": "invalid_request",
"error_description": "Authorization header required"
}
WWW-Authenticate: Bearer realm="api"
401 Unauthorized (invalid token):
{
"error": "invalid_token",
"error_description": "Token has expired",
"error_code": "token_expired"
}
WWW-Authenticate: Bearer error="invalid_token", error_description="Token has expired"
400 Bad Request (extraction error):
{
"error": "invalid_request",
"error_description": "Authorization header format must be Bearer {token}"
}
# Context Key
v3 uses an unexported context key for collision-free claims storage:
type contextKey int
This prevents conflicts with other packages. Always use the provided
helper functions (GetClaims, HasClaims, SetClaims) to access claims.
v2 compatibility: The exported ContextKey{} struct is still available:
claimsValue := r.Context().Value(jwtmiddleware.ContextKey{})
However, the generic helpers are recommended for type safety.
# Custom Claims
Define and use custom claims in your handlers:
type MyCustomClaims struct {
Scope string `json:"scope"`
Permissions []string `json:"permissions"`
}
func (c *MyCustomClaims) Validate(ctx context.Context) error {
if c.Scope == "" {
return errors.New("scope is required")
}
return nil
}
Configure validator with custom claims:
jwtValidator, err := validator.New(
validator.WithKeyFunc(provider.KeyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer(issuerURL.String()),
validator.WithAudience("your-api-identifier"),
validator.WithCustomClaims(func() *MyCustomClaims {
return &MyCustomClaims{}
}),
)
Access in handlers:
func handler(w http.ResponseWriter, r *http.Request) {
claims, _ := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
customClaims := claims.CustomClaims.(*MyCustomClaims)
if contains(customClaims.Permissions, "read:data") {
// User has permission
}
}
# Multiple Issuers
Accept JWTs from multiple issuers simultaneously - ideal for multi-tenant SaaS
applications, domain migrations, or enterprise deployments.
When to use each approach:
- WithIssuer (single): Simple API with one Auth0 tenant
- WithIssuers (static list): Fixed set of issuers (< 10 tenants), domain migration
- WithIssuersResolver (dynamic): Multi-tenant SaaS with 100s+ tenants, DB-backed config
Choosing the right JWKS provider:
- CachingProvider: Use with WithIssuer (single issuer only)
- MultiIssuerProvider: Use with WithIssuers or WithIssuersResolver (multiple issuers)
IMPORTANT: Always pair your issuer validation method with the appropriate provider.
Using CachingProvider with multiple issuers won't work - it only caches one issuer's JWKS.
Performance:
- Single/Static: ~1ms validation (fastest)
- Dynamic: ~1-5ms with caching, ~10-20ms on cache miss
Static issuer list (configured at startup):
provider, _ := jwks.NewMultiIssuerProvider(
jwks.WithMultiIssuerCacheTTL(5*time.Minute),
)
jwtValidator, _ := validator.New(
validator.WithKeyFunc(provider.KeyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuers([]string{
"https://tenant1.auth0.com/",
"https://tenant2.auth0.com/",
"https://tenant3.auth0.com/",
}),
validator.WithAudience("your-api-identifier"),
)
Dynamic issuer resolution (determined at request time):
jwtValidator, _ := validator.New(
validator.WithKeyFunc(provider.KeyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuersResolver(func(ctx context.Context) ([]string, error) {
// Extract tenant from context
tenantID := ctx.Value("tenant").(string)
// Check cache (user-managed)
if cached, found := cache.Get(tenantID); found {
return cached, nil
}
// Query database
issuers, _ := db.GetIssuersForTenant(ctx, tenantID)
// Cache for 5 minutes
cache.Set(tenantID, issuers, 5*time.Minute)
return issuers, nil
}),
validator.WithAudience("your-api-identifier"),
)
The MultiIssuerProvider automatically:
- Routes JWKS requests to the correct issuer based on the token
- Validates issuer BEFORE fetching JWKS (prevents SSRF attacks)
- Caches per-issuer JWKS with configurable TTL
- Handles concurrent requests safely with double-checked locking
Performance: Dynamic resolution should target < 5ms latency.
Implement caching in your resolver for optimal performance (< 1ms cache hit).
See examples/http-multi-issuer-example and examples/http-dynamic-issuer-example
for complete working implementations.
Mixed-algorithm MCD (symmetric + asymmetric issuers):
// Configure MultiIssuerProvider with symmetric issuer support
provider, _ := jwks.NewMultiIssuerProvider(
jwks.WithMultiIssuerCacheTTL(5*time.Minute),
// HS256 issuer: pre-shared secret, no OIDC discovery needed
jwks.WithIssuerKeyConfig("https://internal-service.example.com/", jwks.IssuerKeyConfig{
Secret: []byte("shared-secret"),
Algorithm: validator.HS256,
}),
)
jwtValidator, _ := validator.New(
validator.WithKeyFunc(provider.KeyFunc),
// Allow both RS256 (OIDC) and HS256 (symmetric) tokens
validator.WithAlgorithms([]validator.SignatureAlgorithm{
validator.RS256,
validator.HS256,
}),
validator.WithIssuers([]string{
"https://tenant1.auth0.com/", // RS256 via OIDC discovery
"https://internal-service.example.com/", // HS256 via pre-shared secret
}),
validator.WithAudience("your-api-identifier"),
)
Algorithm enforcement: The validator checks the token's alg header before
fetching JWKS, rejecting tokens with disallowed algorithms immediately.
This prevents algorithm confusion attacks and avoids unnecessary network
requests for invalid tokens.
# Thread Safety
The JWTMiddleware instance is immutable after creation and safe for
concurrent use. The same middleware can be used across multiple routes
and handle concurrent requests.
# Performance
Typical request overhead with JWKS caching:
- Token extraction: <0.1ms
- Signature verification: <1ms (cached keys)
- Claims validation: <0.1ms
- Total: <2ms per request
First request (cold cache):
- OIDC discovery: ~100-300ms
- JWKS fetch: ~50-200ms
- Validation: <1ms
- Total: ~150-500ms
# Architecture
This package is the HTTP adapter in the Core-Adapter pattern:
┌─────────────────────────────────────────────┐
│ HTTP Middleware (THIS PACKAGE) │
│ - Token extraction from HTTP requests │
│ - Error responses (401, 400) │
│ - Context integration │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Core Engine │
│ (Framework-Agnostic Validation Logic) │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Validator │
│ (JWT Parsing & Verification) │
└─────────────────────────────────────────────┘
This design allows the same validation logic to be used with different
transports (HTTP, gRPC, WebSocket, etc.) without code duplication.
# Migration from v2
Key changes from v2 to v3:
1. Options Pattern: All configuration via functional options
// v2
jwtmiddleware.New(validator.New, options...)
// v3
jwtmiddleware.New(
jwtmiddleware.WithValidator(validator),
jwtmiddleware.WithCredentialsOptional(false),
)2. Generic Claims Retrieval: Type-safe with generics
// v2
claims := r.Context().Value(jwtmiddleware.ContextKey{}).(*validator.ValidatedClaims)
// v3
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](r.Context())
3. Validator Options: Pure options pattern
// v2
validator.New(keyFunc, alg, issuer, audience, opts...)
// v3
validator.New(
validator.WithKeyFunc(keyFunc),
validator.WithAlgorithm(validator.RS256),
validator.WithIssuer(issuer),
validator.WithAudience(audience),
)
4. JWKS Provider: Pure options pattern
// v2
jwks.NewProvider(issuerURL, options...)
// v3
jwks.NewCachingProvider(
jwks.WithIssuerURL(issuerURL),
jwks.WithCacheTTL(15*time.Minute),
)
5. ExclusionUrlHandler → ExclusionURLHandler: Proper URL capitalization
See MIGRATION.md for a complete guide.
*/
package jwtmiddleware