Skip to content

Commit d489c99

Browse files
ksegunKolawole Segunoxisto
authored
feat: port clockskew support (golang-jwt#139)
Co-authored-by: Kolawole Segun <[email protected]> Co-authored-by: Christian Banse <[email protected]>
1 parent 6de17d3 commit d489c99

File tree

6 files changed

+149
-36
lines changed

6 files changed

+149
-36
lines changed

claims.go

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import (
99
// Claims must just have a Valid method that determines
1010
// if the token is invalid for any supported reason
1111
type Claims interface {
12-
Valid() error
12+
// Valid implements claim validation. The opts are function style options that can
13+
// be used to fine-tune the validation. The type used for the options is intentionally
14+
// un-exported, since its API and its naming is subject to change.
15+
Valid(opts ...validationOption) error
1316
}
1417

1518
// RegisteredClaims are a structured version of the JWT Claims Set,
@@ -48,13 +51,13 @@ type RegisteredClaims struct {
4851
// There is no accounting for clock skew.
4952
// As well, if any of the above claims are not in the token, it will still
5053
// be considered a valid claim.
51-
func (c RegisteredClaims) Valid() error {
54+
func (c RegisteredClaims) Valid(opts ...validationOption) error {
5255
vErr := new(ValidationError)
5356
now := TimeFunc()
5457

5558
// The claims below are optional, by default, so if they are set to the
5659
// default value in Go, let's not fail the verification for them.
57-
if !c.VerifyExpiresAt(now, false) {
60+
if !c.VerifyExpiresAt(now, false, opts...) {
5861
delta := now.Sub(c.ExpiresAt.Time)
5962
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
6063
vErr.Errors |= ValidationErrorExpired
@@ -65,7 +68,7 @@ func (c RegisteredClaims) Valid() error {
6568
vErr.Errors |= ValidationErrorIssuedAt
6669
}
6770

68-
if !c.VerifyNotBefore(now, false) {
71+
if !c.VerifyNotBefore(now, false, opts...) {
6972
vErr.Inner = ErrTokenNotValidYet
7073
vErr.Errors |= ValidationErrorNotValidYet
7174
}
@@ -85,12 +88,16 @@ func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool {
8588

8689
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
8790
// If req is false, it will return true, if exp is unset.
88-
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool {
91+
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool, opts ...validationOption) bool {
92+
validator := validator{}
93+
for _, o := range opts {
94+
o(&validator)
95+
}
8996
if c.ExpiresAt == nil {
90-
return verifyExp(nil, cmp, req)
97+
return verifyExp(nil, cmp, req, validator.leeway)
9198
}
9299

93-
return verifyExp(&c.ExpiresAt.Time, cmp, req)
100+
return verifyExp(&c.ExpiresAt.Time, cmp, req, validator.leeway)
94101
}
95102

96103
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
@@ -105,12 +112,16 @@ func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool {
105112

106113
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
107114
// If req is false, it will return true, if nbf is unset.
108-
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool {
115+
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool, opts ...validationOption) bool {
116+
validator := validator{}
117+
for _, o := range opts {
118+
o(&validator)
119+
}
109120
if c.NotBefore == nil {
110-
return verifyNbf(nil, cmp, req)
121+
return verifyNbf(nil, cmp, req, validator.leeway)
111122
}
112123

113-
return verifyNbf(&c.NotBefore.Time, cmp, req)
124+
return verifyNbf(&c.NotBefore.Time, cmp, req, validator.leeway)
114125
}
115126

116127
// VerifyIssuer compares the iss claim against cmp.
@@ -141,13 +152,13 @@ type StandardClaims struct {
141152
// Valid validates time based claims "exp, iat, nbf". There is no accounting for clock skew.
142153
// As well, if any of the above claims are not in the token, it will still
143154
// be considered a valid claim.
144-
func (c StandardClaims) Valid() error {
155+
func (c StandardClaims) Valid(opts ...validationOption) error {
145156
vErr := new(ValidationError)
146157
now := TimeFunc().Unix()
147158

148159
// The claims below are optional, by default, so if they are set to the
149160
// default value in Go, let's not fail the verification for them.
150-
if !c.VerifyExpiresAt(now, false) {
161+
if !c.VerifyExpiresAt(now, false, opts...) {
151162
delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0))
152163
vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta)
153164
vErr.Errors |= ValidationErrorExpired
@@ -158,7 +169,7 @@ func (c StandardClaims) Valid() error {
158169
vErr.Errors |= ValidationErrorIssuedAt
159170
}
160171

161-
if !c.VerifyNotBefore(now, false) {
172+
if !c.VerifyNotBefore(now, false, opts...) {
162173
vErr.Inner = ErrTokenNotValidYet
163174
vErr.Errors |= ValidationErrorNotValidYet
164175
}
@@ -178,13 +189,17 @@ func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
178189

179190
// VerifyExpiresAt compares the exp claim against cmp (cmp < exp).
180191
// If req is false, it will return true, if exp is unset.
181-
func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool {
192+
func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool, opts ...validationOption) bool {
193+
validator := validator{}
194+
for _, o := range opts {
195+
o(&validator)
196+
}
182197
if c.ExpiresAt == 0 {
183-
return verifyExp(nil, time.Unix(cmp, 0), req)
198+
return verifyExp(nil, time.Unix(cmp, 0), req, validator.leeway)
184199
}
185200

186201
t := time.Unix(c.ExpiresAt, 0)
187-
return verifyExp(&t, time.Unix(cmp, 0), req)
202+
return verifyExp(&t, time.Unix(cmp, 0), req, validator.leeway)
188203
}
189204

190205
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
@@ -200,13 +215,17 @@ func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool {
200215

201216
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
202217
// If req is false, it will return true, if nbf is unset.
203-
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
218+
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool, opts ...validationOption) bool {
219+
validator := validator{}
220+
for _, o := range opts {
221+
o(&validator)
222+
}
204223
if c.NotBefore == 0 {
205-
return verifyNbf(nil, time.Unix(cmp, 0), req)
224+
return verifyNbf(nil, time.Unix(cmp, 0), req, validator.leeway)
206225
}
207226

208227
t := time.Unix(c.NotBefore, 0)
209-
return verifyNbf(&t, time.Unix(cmp, 0), req)
228+
return verifyNbf(&t, time.Unix(cmp, 0), req, validator.leeway)
210229
}
211230

212231
// VerifyIssuer compares the iss claim against cmp.
@@ -240,11 +259,11 @@ func verifyAud(aud []string, cmp string, required bool) bool {
240259
return result
241260
}
242261

243-
func verifyExp(exp *time.Time, now time.Time, required bool) bool {
262+
func verifyExp(exp *time.Time, now time.Time, required bool, skew time.Duration) bool {
244263
if exp == nil {
245264
return !required
246265
}
247-
return now.Before(*exp)
266+
return now.Before((*exp).Add(+skew))
248267
}
249268

250269
func verifyIat(iat *time.Time, now time.Time, required bool) bool {
@@ -254,11 +273,12 @@ func verifyIat(iat *time.Time, now time.Time, required bool) bool {
254273
return now.After(*iat) || now.Equal(*iat)
255274
}
256275

257-
func verifyNbf(nbf *time.Time, now time.Time, required bool) bool {
276+
func verifyNbf(nbf *time.Time, now time.Time, required bool, skew time.Duration) bool {
258277
if nbf == nil {
259278
return !required
260279
}
261-
return now.After(*nbf) || now.Equal(*nbf)
280+
t := (*nbf).Add(-skew)
281+
return now.After(t) || now.Equal(t)
262282
}
263283

264284
func verifyIss(iss string, cmp string, required bool) bool {

map_claims.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,25 +34,30 @@ func (m MapClaims) VerifyAudience(cmp string, req bool) bool {
3434

3535
// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp).
3636
// If req is false, it will return true, if exp is unset.
37-
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
37+
func (m MapClaims) VerifyExpiresAt(cmp int64, req bool, opts ...validationOption) bool {
3838
cmpTime := time.Unix(cmp, 0)
3939

4040
v, ok := m["exp"]
4141
if !ok {
4242
return !req
4343
}
4444

45+
validator := validator{}
46+
for _, o := range opts {
47+
o(&validator)
48+
}
49+
4550
switch exp := v.(type) {
4651
case float64:
4752
if exp == 0 {
48-
return verifyExp(nil, cmpTime, req)
53+
return verifyExp(nil, cmpTime, req, validator.leeway)
4954
}
5055

51-
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req)
56+
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req, validator.leeway)
5257
case json.Number:
5358
v, _ := exp.Float64()
5459

55-
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req)
60+
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req, validator.leeway)
5661
}
5762

5863
return false
@@ -86,25 +91,30 @@ func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool {
8691

8792
// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf).
8893
// If req is false, it will return true, if nbf is unset.
89-
func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
94+
func (m MapClaims) VerifyNotBefore(cmp int64, req bool, opts ...validationOption) bool {
9095
cmpTime := time.Unix(cmp, 0)
9196

9297
v, ok := m["nbf"]
9398
if !ok {
9499
return !req
95100
}
96101

102+
validator := validator{}
103+
for _, o := range opts {
104+
o(&validator)
105+
}
106+
97107
switch nbf := v.(type) {
98108
case float64:
99109
if nbf == 0 {
100-
return verifyNbf(nil, cmpTime, req)
110+
return verifyNbf(nil, cmpTime, req, validator.leeway)
101111
}
102112

103-
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req)
113+
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req, validator.leeway)
104114
case json.Number:
105115
v, _ := nbf.Float64()
106116

107-
return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req)
117+
return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req, validator.leeway)
108118
}
109119

110120
return false
@@ -121,11 +131,11 @@ func (m MapClaims) VerifyIssuer(cmp string, req bool) bool {
121131
// There is no accounting for clock skew.
122132
// As well, if any of the above claims are not in the token, it will still
123133
// be considered a valid claim.
124-
func (m MapClaims) Valid() error {
134+
func (m MapClaims) Valid(opts ...validationOption) error {
125135
vErr := new(ValidationError)
126136
now := TimeFunc().Unix()
127137

128-
if !m.VerifyExpiresAt(now, false) {
138+
if !m.VerifyExpiresAt(now, false, opts...) {
129139
// TODO(oxisto): this should be replaced with ErrTokenExpired
130140
vErr.Inner = errors.New("Token is expired")
131141
vErr.Errors |= ValidationErrorExpired
@@ -137,7 +147,7 @@ func (m MapClaims) Valid() error {
137147
vErr.Errors |= ValidationErrorIssuedAt
138148
}
139149

140-
if !m.VerifyNotBefore(now, false) {
150+
if !m.VerifyNotBefore(now, false, opts...) {
141151
// TODO(oxisto): this should be replaced with ErrTokenNotValidYet
142152
vErr.Inner = errors.New("Token is not valid yet")
143153
vErr.Errors |= ValidationErrorNotValidYet

parser.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ type Parser struct {
2222
//
2323
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
2424
SkipClaimsValidation bool
25+
26+
validationOptions []validationOption
2527
}
2628

2729
// NewParser creates a new Parser with the specified options
@@ -82,8 +84,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
8284

8385
// Validate Claims
8486
if !p.SkipClaimsValidation {
85-
if err := token.Claims.Valid(); err != nil {
86-
87+
if err := token.Claims.Valid(p.validationOptions...); err != nil {
8788
// If the Claims Valid returned an error, check if it is a validation error,
8889
// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
8990
if e, ok := err.(*ValidationError); !ok {

parser_option.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package jwt
22

3+
import "time"
4+
35
// ParserOption is used to implement functional-style options that modify the behavior of the parser. To add
46
// new options, just create a function (ideally beginning with With or Without) that returns an anonymous function that
57
// takes a *Parser type as input and manipulates its configuration accordingly.
@@ -27,3 +29,10 @@ func WithoutClaimsValidation() ParserOption {
2729
p.SkipClaimsValidation = true
2830
}
2931
}
32+
33+
// WithLeeway returns the ParserOption for specifying the leeway window.
34+
func WithLeeway(d time.Duration) ParserOption {
35+
return func(p *Parser) {
36+
p.validationOptions = append(p.validationOptions, withLeeway(d))
37+
}
38+
}

parser_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,28 @@ var jwtTestData = []struct {
7878
nil,
7979
jwt.SigningMethodRS256,
8080
},
81+
{
82+
"basic expired with 60s skew",
83+
"", // autogen
84+
defaultKeyFunc,
85+
jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
86+
false,
87+
jwt.ValidationErrorExpired,
88+
[]error{jwt.ErrTokenExpired},
89+
jwt.NewParser(jwt.WithLeeway(time.Minute)),
90+
jwt.SigningMethodRS256,
91+
},
92+
{
93+
"basic expired with 120s skew",
94+
"", // autogen
95+
defaultKeyFunc,
96+
jwt.MapClaims{"foo": "bar", "exp": float64(time.Now().Unix() - 100)},
97+
true,
98+
0,
99+
nil,
100+
jwt.NewParser(jwt.WithLeeway(2 * time.Minute)),
101+
jwt.SigningMethodRS256,
102+
},
81103
{
82104
"basic nbf",
83105
"", // autogen
@@ -89,6 +111,28 @@ var jwtTestData = []struct {
89111
nil,
90112
jwt.SigningMethodRS256,
91113
},
114+
{
115+
"basic nbf with 60s skew",
116+
"", // autogen
117+
defaultKeyFunc,
118+
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
119+
false,
120+
jwt.ValidationErrorNotValidYet,
121+
[]error{jwt.ErrTokenNotValidYet},
122+
jwt.NewParser(jwt.WithLeeway(time.Minute)),
123+
jwt.SigningMethodRS256,
124+
},
125+
{
126+
"basic nbf with 120s skew",
127+
"", // autogen
128+
defaultKeyFunc,
129+
jwt.MapClaims{"foo": "bar", "nbf": float64(time.Now().Unix() + 100)},
130+
true,
131+
0,
132+
nil,
133+
jwt.NewParser(jwt.WithLeeway(2 * time.Minute)),
134+
jwt.SigningMethodRS256,
135+
},
92136
{
93137
"expired and nbf",
94138
"", // autogen

validator_option.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package jwt
2+
3+
import "time"
4+
5+
// validationOption is used to implement functional-style options that modify the behavior of the parser. To add
6+
// new options, just create a function (ideally beginning with With or Without) that returns an anonymous function that
7+
// takes a *validator type as input and manipulates its configuration accordingly.
8+
//
9+
// Note that this struct is (currently) un-exported, its naming is subject to change and will only be exported once
10+
// the API is more stable.
11+
type validationOption func(*validator)
12+
13+
// validator represents options that can be used for claims validation
14+
//
15+
// Note that this struct is (currently) un-exported, its naming is subject to change and will only be exported once
16+
// the API is more stable.
17+
type validator struct {
18+
leeway time.Duration // Leeway to provide when validating time values
19+
}
20+
21+
// withLeeway is an option to set the clock skew (leeway) window
22+
//
23+
// Note that this function is (currently) un-exported, its naming is subject to change and will only be exported once
24+
// the API is more stable.
25+
func withLeeway(d time.Duration) validationOption {
26+
return func(v *validator) {
27+
v.leeway = d
28+
}
29+
}

0 commit comments

Comments
 (0)