Skip to content

Commit e4d09c8

Browse files
committed
Work with numeric dates
1 parent 80801cc commit e4d09c8

File tree

24 files changed

+1058
-87
lines changed

24 files changed

+1058
-87
lines changed

internal/json/json.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ func EncodeAudience(enc *Encoder, aud []string, flatten bool) error {
105105
type DecodeCtx interface {
106106
SetRegistry(*Registry)
107107
Registry() *Registry
108-
SetPedantic(bool)
109-
Pedantic() bool // returns true if pedantic mode is enabled
108+
SetPedantic(*bool) // Changed from bool to *bool
109+
Pedantic() *bool // Changed from bool to *bool
110110
}
111111

112112
// DecodeCtxContainer is used to differentiate objects that can carry extra
@@ -119,7 +119,7 @@ type DecodeCtxContainer interface {
119119
// stock decodeCtx. should cover 80% of the cases
120120
type decodeCtx struct {
121121
registry *Registry
122-
pedantic bool
122+
pedantic *bool // Changed from bool to *bool
123123
}
124124

125125
func NewDecodeCtx() DecodeCtx {
@@ -134,11 +134,11 @@ func (dc *decodeCtx) Registry() *Registry {
134134
return dc.registry
135135
}
136136

137-
func (dc *decodeCtx) SetPedantic(pedantic bool) {
137+
func (dc *decodeCtx) SetPedantic(pedantic *bool) {
138138
dc.pedantic = pedantic
139139
}
140140

141-
func (dc *decodeCtx) Pedantic() bool {
141+
func (dc *decodeCtx) Pedantic() *bool {
142142
return dc.pedantic
143143
}
144144

internal/tokens/tokens.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const (
1111
Period = '.'
1212
)
1313

14+
const (
15+
Zero = '0'
16+
Nine = '9'
17+
)
18+
1419
// Cryptographic key sizes
1520
const (
1621
KeySize16 = 16

jwe/headers_gen.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jwk/ecdsa_gen.go

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jwk/okp_gen.go

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jwk/rsa_gen.go

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jwk/symmetric_gen.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jws/headers_gen.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jws/interface.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ type Base64Encoder = base64.Encoder
2222

2323
type DecodeCtx interface {
2424
CollectRaw() bool
25-
SetPedantic(bool)
26-
Pedantic() bool
25+
SetPedantic(*bool)
26+
Pedantic() *bool
2727
}
2828

2929
// Message represents a full JWS encoded message. Flattened serialization

jwt/internal/types/date.go

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"strconv"
66
"strings"
7+
"sync/atomic"
78
"time"
89

910
"github.com/lestrrat-go/jwx/v3/internal/json"
@@ -52,21 +53,61 @@ func intToTime(v any, t *time.Time) bool {
5253
return true
5354
}
5455

55-
func parseNumericString(x string) (time.Time, error) {
56-
var t time.Time // empty time for empty return value
56+
// shouldParseIntOnly determines whether strict integer-only parsing should be enforced.
57+
// This function implements option-override semantics:
58+
//
59+
// If parseIntOnly is non-nil (explicitly set):
60+
// - Return the dereferenced value, ignoring the global Pedantic flag
61+
// - This allows callers to override the global setting for specific operations
62+
//
63+
// If parseIntOnly is nil (unspecified):
64+
// - Fall back to the global Pedantic flag value
65+
// - This maintains the default behavior when no override is specified
66+
//
67+
// This design allows three distinct behaviors:
68+
// - Pass nil: use global setting (backward compatible default)
69+
// - Pass ptr to false: explicitly lenient, even if global is strict
70+
// - Pass ptr to true: explicitly strict, even if global is lenient
71+
func shouldParseIntOnly(parseIntOnly *bool) bool {
72+
if parseIntOnly != nil {
73+
// Explicit override: use the specified value, ignore global
74+
return *parseIntOnly
75+
}
76+
// No override: fall back to global setting
77+
return atomic.LoadUint32(&Pedantic) == 1
78+
}
5779

58-
// Only check for the escape hatch if it's the pedantic
59-
// flag is off
60-
if Pedantic != 1 {
61-
// This is an escape hatch for non-conformant providers
62-
// that gives us RFC3339 instead of epoch time
63-
for _, r := range x {
64-
// 0x30 = '0', 0x39 = '9', 0x2E = tokens.Period
65-
if (r >= 0x30 && r <= 0x39) || r == 0x2E {
66-
continue
67-
}
80+
func detectRFC3339(r rune) bool {
81+
return !(r >= tokens.Zero && r <= tokens.Nine) && r != tokens.Period
82+
}
83+
84+
// parseNumericString parses a string representation of a Unix timestamp and returns
85+
// the corresponding time.Time value. The function handles both integer and fractional
86+
// (floating-point) timestamp values.
87+
//
88+
// The parseIntOnly parameter controls whether RFC3339 timestamp strings are accepted.
89+
// When parseIntOnly is true, only numeric string representations (epoch timestamps) are
90+
// accepted, and the function will return an error for RFC3339 formatted strings.
91+
// When parseIntOnly is false, the function will attempt to parse RFC3339 timestamps
92+
// as a fallback for non-conformant providers.
93+
//
94+
// IMPORTANT: This function does not consult global state. The caller is responsible
95+
// for determining the effective parseIntOnly value by combining any per-parse flags
96+
// with the global Pedantic flag (see shouldParseIntOnly helper).
97+
//
98+
// For fractional timestamps, the ParsePrecision variable controls how many fractional
99+
// digits are considered. The function pads or truncates fractional values to 9 digits
100+
// (nanosecond precision) as needed.
101+
func parseNumericString(x string, parseIntOnly bool) (time.Time, error) {
102+
var t time.Time // empty time for empty return value
68103

69-
// if it got here, then it probably isn't epoch time
104+
// RFC3339 escape hatch for non-conformant providers.
105+
// Only active when parseIntOnly is false (lenient mode).
106+
if !parseIntOnly {
107+
// Quick check: if string contains any non-numeric character (excluding period),
108+
// try parsing as RFC3339. RFC3339 timestamps always contain non-numeric characters
109+
// (T, Z, :, -) so this provides an early detection mechanism.
110+
if strings.ContainsFunc(x, detectRFC3339) {
70111
tv, err := time.Parse(time.RFC3339, x)
71112
if err != nil {
72113
return t, fmt.Errorf(`value is not number of seconds since the epoch, and attempt to parse it as RFC3339 timestamp failed: %w`, err)
@@ -107,7 +148,27 @@ func parseNumericString(x string) (time.Time, error) {
107148
return time.Unix(n, nsecs).UTC(), nil
108149
}
109150

110-
func (n *NumericDate) Accept(v any, errOnNull bool) error {
151+
// Accept validates and assigns a value to the NumericDate. The method supports
152+
// multiple input types including numeric values (int, float), json.Number, string
153+
// representations of numbers, and time.Time values.
154+
//
155+
// The errOnNull parameter controls whether nil values should be rejected (true)
156+
// or accepted as zero time (false). When errOnNull is true and v is nil, an error
157+
// is returned. When errOnNull is false and v is nil, the NumericDate is set to
158+
// the zero time value.
159+
//
160+
// The parseIntOnly parameter controls whether RFC3339 timestamp strings should
161+
// be rejected. This parameter uses option-override semantics:
162+
// - If parseIntOnly is nil: use the global Pedantic flag setting
163+
// - If parseIntOnly is non-nil: use the specified value, ignoring the global flag
164+
//
165+
// This allows callers to explicitly override the global setting when needed, while
166+
// defaulting to the global setting when no override is specified.
167+
//
168+
// Supported input types include int8, int16, int32, int64, int, float32, float64,
169+
// json.Number, string (numeric or RFC3339 when allowed), and time.Time. Any other
170+
// type will result in an error.
171+
func (n *NumericDate) Accept(v any, errOnNull bool, parseIntOnly *bool) error {
111172
// Handle nil case first
112173
if v == nil {
113174
if errOnNull {
@@ -118,28 +179,31 @@ func (n *NumericDate) Accept(v any, errOnNull bool) error {
118179
return nil
119180
}
120181

182+
// Resolve effective pedantic mode by combining parameter with global flag
183+
effectivePedantic := shouldParseIntOnly(parseIntOnly)
184+
121185
var t time.Time
122186
switch x := v.(type) {
123187
case float32:
124-
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x))
188+
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x), effectivePedantic)
125189
if err != nil {
126190
return fmt.Errorf(`failed to accept float32 %.9f: %w`, x, err)
127191
}
128192
t = tv
129193
case float64:
130-
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x))
194+
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x), effectivePedantic)
131195
if err != nil {
132196
return fmt.Errorf(`failed to accept float32 %.9f: %w`, x, err)
133197
}
134198
t = tv
135199
case json.Number:
136-
tv, err := parseNumericString(x.String())
200+
tv, err := parseNumericString(x.String(), effectivePedantic)
137201
if err != nil {
138202
return fmt.Errorf(`failed to accept json.Number %q: %w`, x.String(), err)
139203
}
140204
t = tv
141205
case string:
142-
tv, err := parseNumericString(x)
206+
tv, err := parseNumericString(x, effectivePedantic)
143207
if err != nil {
144208
return fmt.Errorf(`failed to accept string %q: %w`, x, err)
145209
}
@@ -194,7 +258,8 @@ func (n *NumericDate) UnmarshalJSON(data []byte) error {
194258
}
195259

196260
var n2 NumericDate
197-
if err := n2.Accept(v, false); err != nil {
261+
// Pass nil to use global flag (no explicit override for direct unmarshaling)
262+
if err := n2.Accept(v, false, nil); err != nil {
198263
return fmt.Errorf(`invalid value for NumericDate: %w`, err)
199264
}
200265
*n = n2

0 commit comments

Comments
 (0)