Repository navigation
Expand file tree
/
Copy pathjwt.go
More file actions
545 lines (529 loc) · 21.6 KB
/
Copy pathjwt.go
File metadata and controls
545 lines (529 loc) · 21.6 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
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
package jwt
import (
"encoding/json"
jsontext "encoding/json/jsontext"
jsonv2 "encoding/json/v2"
"errors"
"os"
"reflect"
"time"
)
// Map is a convenient type alias for map[string]any, commonly used for dynamic JWT claims.
//
// This type provides a flexible way to work with JWT payloads that contain dynamic
// or unknown claim structures. It's particularly useful when:
// - Working with tokens from external sources with varying claim structures
// - Building generic JWT processing utilities
// - Handling tokens where claim types are determined at runtime
// - Prototyping and development scenarios
//
// While Map provides flexibility, consider using typed structs for production
// applications where claim structure is known and type safety is important.
//
// Example usage:
//
// // Create claims dynamically
// claims := jwt.Map{
// "sub": "user123",
// "role": "admin",
// "permissions": []string{"read", "write"},
// "iat": time.Now().Unix(),
// "exp": time.Now().Add(time.Hour).Unix(),
// }
//
// token, err := jwt.Sign(jwt.HS256, secretKey, claims)
//
// // Verify and extract claims
// var verifiedClaims jwt.Map
// verifiedToken, err := jwt.Verify(jwt.HS256, secretKey, token)
// err = verifiedToken.Claims(&verifiedClaims)
//
// // Access claims dynamically
// userID := verifiedClaims["sub"].(string)
// role := verifiedClaims["role"].(string)
// permissions := verifiedClaims["permissions"].([]any)
type Map = map[string]any
// Clock provides the current time for JWT expiration and time-based claim validation.
//
// This variable is used throughout the library for all time-related validations including:
// - Token expiration (exp claim) verification
// - Not-before (nbf claim) validation
// - Issued-at (iat claim) verification
// - Leeway calculations for time-based validators
//
// The default implementation uses time.Now(), but it can be overridden for:
// - Testing: Set fixed times to create deterministic test scenarios
// - Time zones: Use custom time sources for specific timezone requirements
// - Simulation: Simulate future or past times for testing edge cases
// - Synchronized time: Use network time protocols for distributed systems
//
// Thread Safety: Modifications to Clock should be done during application
// initialization before concurrent operations begin. The function itself should
// be thread-safe as it may be called from multiple goroutines simultaneously.
//
// Example usage:
//
// // Default usage (production)
// now := jwt.Clock() // Returns current time
//
// // Testing with fixed time
// fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
// jwt.Clock = func() time.Time { return fixedTime }
//
// // Testing with relative time
// startTime := time.Now()
// jwt.Clock = func() time.Time { return startTime.Add(2 * time.Hour) }
//
// // Reset to default
// jwt.Clock = time.Now
//
// When testing time-sensitive functionality, remember to restore the original
// Clock function after tests complete to avoid affecting other tests.
var Clock = time.Now
// CompareHeader is the global header validator used for JWT token verification.
//
// This package-level variable defines the default behavior for validating JWT headers
// across all verification operations in the application. It implements the HeaderValidator
// interface and is responsible for algorithm validation and basic header consistency checking.
//
// Default Behavior: The default implementation (compareHeader) performs:
// - Algorithm matching between expected and token header
// - Header structure validation (standard vs reversed field order)
// - Support for headers with and without "typ": "JWT" field
// - Fast byte-level comparison for known algorithm patterns
//
// Customization Use Cases:
// - Cross-platform compatibility: Handle tokens from other JWT libraries
// - Legacy token support: Accept non-standard header formats
// - Third-party integration: Support tokens from external services
// - Custom validation: Add application-specific header validation logic
//
// Global vs Per-Token Validation:
// - Modifying CompareHeader affects ALL token verification in the application
// - For per-token validation, use VerifyWithHeaderValidator instead
// - Consider thread safety when modifying this global variable
//
// Thread Safety: Changes should be made during application initialization
// before concurrent verification operations begin.
//
// Example customization:
//
// // Custom validator for third-party tokens
// jwt.CompareHeader = func(alg string, headerDecoded []byte) (jwt.Alg, jwt.PublicKey, jwt.InjectFunc, error) {
// // Parse header to extract algorithm
// var header struct {
// Alg string `json:"alg"`
// Typ string `json:"typ"`
// // Add custom fields as needed
// }
//
// if err := json.Unmarshal(headerDecoded, &header); err != nil {
// return nil, nil, nil, err
// }
//
// // Validate algorithm matches expectation
// if alg != "" && header.Alg != alg {
// return nil, nil, nil, jwt.ErrTokenAlg
// }
//
// // Accept both "JWT" and "jwt" type values (case insensitive)
// if header.Typ != "" && strings.ToUpper(header.Typ) != "JWT" {
// return nil, nil, nil, jwt.ErrTokenAlg
// }
//
// return nil, nil, nil, nil // Use provided algorithm and key
// }
//
// There is no exported way back to the default, so keep a copy before replacing it:
//
// defaultCompare := jwt.CompareHeader
var CompareHeader HeaderValidator = compareHeader
// ReadFile defines the file reading function used by key loading utilities.
//
// This package-level variable allows customization of how the library reads
// key files when using helper functions like MustLoadRSA, LoadRSA, and similar
// key loading utilities. The default implementation uses os.ReadFile for
// standard filesystem access.
//
// Use Cases for Customization:
// - Embedded files: Read keys from embedded filesystem (embed.FS)
// - Remote storage: Fetch keys from cloud storage, databases, or APIs
// - Encrypted storage: Read and decrypt keys from secure storage
// - Virtual filesystems: Support non-standard file sources
// - Testing: Use in-memory file systems for unit tests
//
// Function Signature: Must match `func(filename string) ([]byte, error)`
// to be compatible with standard os.ReadFile behavior.
//
// Thread Safety: This variable should be set during application initialization
// before concurrent key loading operations. The function itself should be thread-safe
// as it may be called from multiple goroutines.
//
// Example customizations:
//
// // Embedded filesystem (Go 1.16+)
// //go:embed keys/*
// var keyFiles embed.FS
// jwt.ReadFile = keyFiles.ReadFile
//
// // Remote key storage
// jwt.ReadFile = func(filename string) ([]byte, error) {
// resp, err := http.Get("https://keyserver.com/keys/" + filename)
// if err != nil {
// return nil, err
// }
// defer resp.Body.Close()
// return io.ReadAll(resp.Body)
// }
//
// // Database storage
// jwt.ReadFile = func(filename string) ([]byte, error) {
// var keyData []byte
// err := db.QueryRow("SELECT key_data FROM keys WHERE filename = ?", filename).Scan(&keyData)
// return keyData, err
// }
//
// // Testing with in-memory files
// testFiles := map[string][]byte{
// "test.pem": []byte("-----BEGIN PRIVATE KEY-----\n..."),
// }
// jwt.ReadFile = func(filename string) ([]byte, error) {
// if data, ok := testFiles[filename]; ok {
// return data, nil
// }
// return nil, os.ErrNotExist
// }
var ReadFile = os.ReadFile
// marshalOptions reproduces today's json.Marshal (v1) output exactly, using
// encoding/json/v2 directly for its lower allocation cost:
//
// - Deterministic(true): sorted map keys, so a map[string]any claims value with more
// than one key signs the same bytes every run. Required, not cosmetic: without it
// testdata/golden.json and every signature over map claims would be irreproducible.
// - EscapeForHTML/EscapeForJS(true): matches v1's default escaping of <, >, &, U+2028
// and U+2029 inside strings.
// - AllowInvalidUTF8(true): v1 silently replaces invalid UTF-8 rather than erroring on
// Marshal; keep that leniency so a claim value that signs successfully today still
// does.
//
// Deliberately not included: OmitEmptyWithLegacySemantics. Claims.NotBefore/IssuedAt/
// Expiry are tagged "omitzero" rather than "omitempty" for exactly this reason: under
// bare v2 semantics, omitempty on an int64 does not omit a zero value, so an unset "exp"
// would start being serialized as "exp":0 instead of left out.
var marshalOptions = jsonv2.JoinOptions(
jsonv2.Deterministic(true),
jsontext.EscapeForHTML(true),
jsontext.EscapeForJS(true),
jsontext.AllowInvalidUTF8(true),
)
// Marshal defines the JSON marshaling function used for encoding JWT payloads.
//
// This package-level variable allows customization of how claims are serialized
// to JSON before being included in JWT tokens. The default implementation handles
// both raw byte slices and arbitrary Go values, using encoding/json/v2 with the
// option set above to reproduce encoding/json's (v1) output exactly, at v2's cost.
//
// Default Behavior:
// - Raw []byte values are passed through unchanged (for pre-serialized JSON)
// - All other values are marshaled using encoding/json/v2, with options chosen to
// match json.Marshal's byte-for-byte output
// - Supports any type that implements json.Marshaler interface
//
// Customization Use Cases:
// - Custom JSON encoding: Use alternative JSON libraries (e.g., jsoniter, gojay)
// - Field transformation: Apply custom field naming, omission, or formatting
// - Compression: Compress payloads before encoding for large claims
// - Encryption: Encrypt sensitive fields before marshaling
// - Validation: Add payload validation during marshaling
// - Debugging: Add logging or monitoring to track payload sizes
//
// Performance Considerations: The Marshal function is called for every token
// generation, so performance optimizations here can significantly impact throughput.
//
// Thread Safety: This variable should be set during application initialization.
// The function itself must be thread-safe as it's called from concurrent operations.
//
// Example customizations:
//
// // Use jsoniter for better performance
// import jsoniter "github.com/json-iterator/go"
// jwt.Marshal = jsoniter.Marshal
//
// // Add payload size logging
// originalMarshal := jwt.Marshal
// jwt.Marshal = func(v any) ([]byte, error) {
// data, err := originalMarshal(v)
// if err == nil {
// log.Printf("JWT payload size: %d bytes", len(data))
// }
// return data, err
// }
//
// // Custom field formatting
// jwt.Marshal = func(v any) ([]byte, error) {
// if b, ok := v.([]byte); ok {
// return b, nil
// }
//
// // Apply custom transformations
// transformed := applyCustomFormatting(v)
// return json.Marshal(transformed)
// }
var Marshal = func(v any) ([]byte, error) {
if b, ok := v.([]byte); ok {
return b, nil
}
return jsonv2.Marshal(v, marshalOptions)
}
// Unmarshal defines the JSON unmarshaling function used for decoding JWT payloads.
//
// This package-level variable allows customization of how JWT payload JSON is
// deserialized into Go data structures. The default implementation uses
// encoding/json/v2 with a numberUnmarshalers hook that reproduces what
// json.Decoder.UseNumber() gave v1: numeric precision preserved when the
// destination is `any`, rather than lost to a float64 conversion.
//
// Default Behavior (defaultUnmarshal):
// - Uses encoding/json/v2 with a custom unmarshal hook for numbers into `any`
// - Prevents automatic conversion of all numbers to float64
// - Preserves numeric precision for large integers
// - Decodes JSON numbers as json.Number type when destination is any
// - Tolerates duplicate JSON object keys (Merge/Enrich can produce them) and
// case-differing standard claim names, matching today's behavior
//
// Why Numeric Precision Matters:
// - A destination-agnostic JSON decode converts all numbers to float64
// - Float64 cannot accurately represent large integers (>53 bits)
// - JWT claims like "iat", "exp", "nbf" are Unix timestamps (large integers)
// - User IDs and other identifiers may be large integers
//
// Customization Use Cases:
// - Alternative JSON libraries: Use faster or feature-rich JSON libraries
// - Custom number handling: Different numeric type preferences
// - Field validation: Add validation during unmarshaling
// - Field transformation: Apply custom transformations to incoming data
// - Debugging: Add logging to monitor claim structures
// - Security: Add input sanitization or filtering
//
// Thread Safety: This variable should be set during application initialization.
// The function itself must be thread-safe as it's called from concurrent operations.
//
// Example customizations:
//
// // Use jsoniter for better performance
// import jsoniter "github.com/json-iterator/go"
// jwt.Unmarshal = jsoniter.Unmarshal
//
// // Add claim logging for debugging
// originalUnmarshal := jwt.Unmarshal
// jwt.Unmarshal = func(data []byte, v any) error {
// log.Printf("Unmarshaling JWT claims: %s", string(data))
// return originalUnmarshal(data, v)
// }
//
// // Custom number handling without UseNumber()
// jwt.Unmarshal = func(data []byte, v any) error {
// return json.Unmarshal(data, v) // Standard behavior
// }
//
// // Add validation during unmarshaling
// jwt.Unmarshal = func(data []byte, v any) error {
// if err := defaultUnmarshal(data, v); err != nil {
// return err
// }
// return validateClaimsStructure(v) // Custom validation
// }
var Unmarshal = defaultUnmarshal
// UnmarshalWithRequired provides JSON unmarshaling with required field validation.
//
// This function extends the standard unmarshaling behavior by validating that all
// fields marked with the "required" JSON tag are present and non-empty in the
// JWT payload. It's particularly useful for enforcing strict claim requirements
// in security-critical applications.
//
// Required Field Validation:
// - Checks struct fields tagged with `json:"fieldname,required"`
// - Validates that required fields are present in the JSON payload
// - Ensures required fields have non-zero values (not nil, empty string, etc.)
// - Returns ErrMissingKey if any required field is missing or empty
//
// Usage Pattern: Replace the global Unmarshal function to enable required
// field validation for all token verification operations in the application.
//
// Field Tag Format: Use standard JSON tags with "required" option:
//
// `json:"field_name,required"` - Field is required and must be present
// `json:"field_name,omitempty,required"` - Cannot combine omitempty with required
//
// Validation Rules:
// - String fields: Must not be empty ("")
// - Numeric fields: Must not be zero value (0, 0.0)
// - Boolean fields: Must be explicitly set (false is valid)
// - Slice/Map fields: Must not be nil or empty
// - Pointer fields: Must not be nil
// - Interface fields: Must not be nil
//
// Performance: Adds reflection-based validation overhead after unmarshaling.
// Consider the performance impact for high-throughput applications.
//
// Parameters:
// - payload: Raw JSON bytes from the JWT payload
// - dest: Pointer to destination struct with required field tags
//
// Returns:
// - error: JSON unmarshaling errors or ErrMissingKey for missing required fields
//
// Example usage:
//
// // Define claims struct with required fields
// type UserClaims struct {
// Username string `json:"username,required"` // Must be present and non-empty
// UserID int `json:"user_id,required"` // Must be present and non-zero
// Role string `json:"role,required"` // Must be present and non-empty
// Email string `json:"email"` // Optional field
// IssuedAt time.Time `json:"iat,required"` // Must be present
// }
//
// // Enable required field validation globally
// jwt.Unmarshal = jwt.UnmarshalWithRequired
//
// // Verify token - will fail if required fields are missing
// var claims UserClaims
// verifiedToken, err := jwt.Verify(jwt.HS256, secretKey, token)
// err = verifiedToken.Claims(&claims)
// if err != nil {
// // Could be ErrMissingKey if required fields are missing
// log.Printf("Token verification failed: %v", err)
// }
//
// // Token with missing required field will fail:
// // {"user_id": 123, "email": "user@example.com"} // Missing username and role
//
// // Valid token:
// // {"username": "john", "user_id": 123, "role": "admin", "iat": 1609459200}
func UnmarshalWithRequired(payload []byte, dest any) error {
if err := defaultUnmarshal(payload, dest); err != nil {
return err
}
return meetRequirements(reflect.ValueOf(dest))
}
// decodeOptions is used by every JSON payload decode in this package: the package-level
// Unmarshal var (via unmarshalOptions below) and verify.go's two claims decodes.
//
// - AllowDuplicateNames(true): required. Merge and Enrich can legitimately splice
// duplicate object keys (documented as an accepted interoperability hazard, not a
// bug, since Merge never reparses its own output). The decoder must keep reading the
// last occurrence, exactly as encoding/json (v1) does today.
// - AllowInvalidUTF8(true): symmetric with marshalOptions above.
// - MatchCaseInsensitiveNames(true): not new behavior — v1 has always case-folded
// struct field matching; this option just makes that explicit for v2. A token whose
// issuer capitalized a claim name differently, and that verified under Go 1.26, must
// keep verifying.
var decodeOptions = jsonv2.JoinOptions(
jsontext.AllowDuplicateNames(true),
jsontext.AllowInvalidUTF8(true),
jsonv2.MatchCaseInsensitiveNames(true),
)
// numberUnmarshalers reproduces json.Decoder.UseNumber()'s behavior for a v2 decode into
// an `any` destination. v2 has no built-in equivalent — a bare v2 decode of a JSON number
// into `any` produces float64, which loses precision for exp/iat/nbf and large numeric
// IDs above 2^53. This hook is the only mechanism v2 offers for the any-destination case.
var numberUnmarshalers = jsonv2.WithUnmarshalers(jsonv2.UnmarshalFromFunc(
func(dec *jsontext.Decoder, v *any) error {
if dec.PeekKind() == '0' { // jsontext.Kind for "number"
val, err := dec.ReadValue()
if err != nil {
return err
}
*v = json.Number(val)
return nil
}
return errors.ErrUnsupported // fall through to v2's default handling
}))
var unmarshalOptions = jsonv2.JoinOptions(decodeOptions, numberUnmarshalers)
func defaultUnmarshal(payload []byte, dest any) error {
return jsonv2.Unmarshal(payload, &dest, unmarshalOptions)
}
// InjectFunc defines a function type for modifying JWT payload data during token processing.
//
// This function type enables payload transformation before signing (encoding) or after
// verification but before unmarshaling (decoding). It's the foundation for advanced
// JWT features like payload encryption, compression, and custom data transformations.
//
// Function Signature:
// - Input: plainPayload []byte - The raw payload data to transform
// - Output: []byte - The transformed payload data
// - Output: error - Any transformation error
//
// Use Cases:
// - Encryption: Encrypt sensitive payload data (see GCM function)
// - Compression: Compress large payloads to reduce token size
// - Encoding: Apply custom encoding schemes (base32, hex, etc.)
// - Validation: Add payload validation with transformation
// - Filtering: Remove or redact sensitive fields
// - Augmentation: Add computed fields or metadata
//
// Usage Contexts:
// - Signing: Applied before payload is base64-encoded and signed
// - Verification: Applied after signature verification but before claims extraction
// - Multi-key scenarios: Different InjectFunc per key in Keys registry
// - Custom algorithms: Algorithm-specific payload processing
//
// Implementation Requirements:
// - Must be deterministic for the same input (especially for signing)
// - Should be reversible if used for both encoding and decoding
// - Must handle edge cases gracefully (empty payloads, invalid data)
// - Should be thread-safe for concurrent operations
// - Error handling should be comprehensive and descriptive
//
// Built-in Implementations:
// - GCM(): Creates encrypt/decrypt function pair for AES-GCM encryption
// - Custom implementations for specific transformation needs
//
// Example implementations:
//
// // Simple compression function
// func compress(payload []byte) ([]byte, error) {
// var buf bytes.Buffer
// writer := gzip.NewWriter(&buf)
// if _, err := writer.Write(payload); err != nil {
// return nil, err
// }
// if err := writer.Close(); err != nil {
// return nil, err
// }
// return buf.Bytes(), nil
// }
//
// // Field filtering function
// func filterSensitiveFields(payload []byte) ([]byte, error) {
// var claims map[string]any
// if err := json.Unmarshal(payload, &claims); err != nil {
// return nil, err
// }
//
// // Remove sensitive fields
// delete(claims, "password")
// delete(claims, "ssn")
//
// return json.Marshal(claims)
// }
//
// // Usage with signing
// encryptFunc, decryptFunc, err := jwt.GCM(encryptionKey, nil)
// if err != nil {
// return err
// }
//
// token, err := jwt.SignEncrypted(jwt.HS256, secretKey, encryptFunc, claims)
//
// // Usage with verification (automatic with Keys)
// keys := make(jwt.Keys)
// keys["key1"] = &jwt.Key{
// Alg: jwt.HS256,
// Private: secretKey,
// Public: secretKey,
// Encrypt: encryptFunc,
// Decrypt: decryptFunc,
// }
type InjectFunc func(plainPayload []byte) ([]byte, error)