Validation Options: Experiment 1

This commit is contained in:
Christian Banse 2022-05-28 21:12:35 +02:00
parent cf43decf7c
commit 9322ee9417
5 changed files with 61 additions and 18 deletions

View File

@ -22,6 +22,8 @@ type Claims interface {
// //
// See examples for how to use this with your own claim types. // See examples for how to use this with your own claim types.
type RegisteredClaims struct { type RegisteredClaims struct {
v validator
// the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"` Issuer string `json:"iss,omitempty"`
@ -87,10 +89,10 @@ func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool {
// If req is false, it will return true, if exp is unset. // If req is false, it will return true, if exp is unset.
func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool { func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool {
if c.ExpiresAt == nil { if c.ExpiresAt == nil {
return verifyExp(nil, cmp, req) return verifyExp(nil, cmp, req, c.v.leeway)
} }
return verifyExp(&c.ExpiresAt.Time, cmp, req) return verifyExp(&c.ExpiresAt.Time, cmp, req, c.v.leeway)
} }
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). // VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
@ -107,10 +109,10 @@ func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool {
// If req is false, it will return true, if nbf is unset. // If req is false, it will return true, if nbf is unset.
func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool { func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool {
if c.NotBefore == nil { if c.NotBefore == nil {
return verifyNbf(nil, cmp, req) return verifyNbf(nil, cmp, req, c.v.leeway)
} }
return verifyNbf(&c.NotBefore.Time, cmp, req) return verifyNbf(&c.NotBefore.Time, cmp, req, c.v.leeway)
} }
// VerifyIssuer compares the iss claim against cmp. // VerifyIssuer compares the iss claim against cmp.
@ -129,6 +131,8 @@ func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool {
// //
// Deprecated: Use RegisteredClaims instead for a forward-compatible way to access registered claims in a struct. // Deprecated: Use RegisteredClaims instead for a forward-compatible way to access registered claims in a struct.
type StandardClaims struct { type StandardClaims struct {
v validator
Audience string `json:"aud,omitempty"` Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"` ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"` Id string `json:"jti,omitempty"`
@ -180,11 +184,11 @@ func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool {
// If req is false, it will return true, if exp is unset. // If req is false, it will return true, if exp is unset.
func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool {
if c.ExpiresAt == 0 { if c.ExpiresAt == 0 {
return verifyExp(nil, time.Unix(cmp, 0), req) return verifyExp(nil, time.Unix(cmp, 0), req, c.v.leeway)
} }
t := time.Unix(c.ExpiresAt, 0) t := time.Unix(c.ExpiresAt, 0)
return verifyExp(&t, time.Unix(cmp, 0), req) return verifyExp(&t, time.Unix(cmp, 0), req, c.v.leeway)
} }
// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). // VerifyIssuedAt compares the iat claim against cmp (cmp >= iat).
@ -202,11 +206,11 @@ func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool {
// If req is false, it will return true, if nbf is unset. // If req is false, it will return true, if nbf is unset.
func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool {
if c.NotBefore == 0 { if c.NotBefore == 0 {
return verifyNbf(nil, time.Unix(cmp, 0), req) return verifyNbf(nil, time.Unix(cmp, 0), req, c.v.leeway)
} }
t := time.Unix(c.NotBefore, 0) t := time.Unix(c.NotBefore, 0)
return verifyNbf(&t, time.Unix(cmp, 0), req) return verifyNbf(&t, time.Unix(cmp, 0), req, c.v.leeway)
} }
// VerifyIssuer compares the iss claim against cmp. // VerifyIssuer compares the iss claim against cmp.
@ -240,11 +244,12 @@ func verifyAud(aud []string, cmp string, required bool) bool {
return result return result
} }
func verifyExp(exp *time.Time, now time.Time, required bool) bool { func verifyExp(exp *time.Time, now time.Time, required bool, skew time.Duration) bool {
if exp == nil { if exp == nil {
return !required return !required
} }
return now.Before(*exp)
return now.Before((*exp).Add(+skew))
} }
func verifyIat(iat *time.Time, now time.Time, required bool) bool { func verifyIat(iat *time.Time, now time.Time, required bool) bool {
@ -254,11 +259,13 @@ func verifyIat(iat *time.Time, now time.Time, required bool) bool {
return now.After(*iat) || now.Equal(*iat) return now.After(*iat) || now.Equal(*iat)
} }
func verifyNbf(nbf *time.Time, now time.Time, required bool) bool { func verifyNbf(nbf *time.Time, now time.Time, required bool, skew time.Duration) bool {
if nbf == nil { if nbf == nil {
return !required return !required
} }
return now.After(*nbf) || now.Equal(*nbf)
t := (*nbf).Add(-skew)
return now.After(t) || now.Equal(t)
} }
func verifyIss(iss string, cmp string, required bool) bool { func verifyIss(iss string, cmp string, required bool) bool {

View File

@ -45,14 +45,14 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool {
switch exp := v.(type) { switch exp := v.(type) {
case float64: case float64:
if exp == 0 { if exp == 0 {
return verifyExp(nil, cmpTime, req) return verifyExp(nil, cmpTime, req, 0)
} }
return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req) return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req, 0)
case json.Number: case json.Number:
v, _ := exp.Float64() v, _ := exp.Float64()
return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req) return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req, 0)
} }
return false return false
@ -97,14 +97,14 @@ func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool {
switch nbf := v.(type) { switch nbf := v.(type) {
case float64: case float64:
if nbf == 0 { if nbf == 0 {
return verifyNbf(nil, cmpTime, req) return verifyNbf(nil, cmpTime, req, 0)
} }
return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req) return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req, 0)
case json.Number: case json.Number:
v, _ := nbf.Float64() v, _ := nbf.Float64()
return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req) return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req, 0)
} }
return false return false

View File

@ -22,6 +22,8 @@ type Parser struct {
// //
// Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead. // Deprecated: In future releases, this field will not be exported anymore and should be set with an option to NewParser instead.
SkipClaimsValidation bool SkipClaimsValidation bool
v validator
} }
// NewParser creates a new Parser with the specified options // NewParser creates a new Parser with the specified options
@ -80,6 +82,18 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
vErr := &ValidationError{} vErr := &ValidationError{}
// Experimental: inject the validation options of the parser into the
// claims. This provides a backwards compatible way to have validation
// options, but it unfortunately only works for jwt.RegisteredClaims and
// jwt.StandardClaims
if rclaims, ok := claims.(*RegisteredClaims); ok {
rclaims.v = p.v
}
if sclaims, ok := claims.(*StandardClaims); ok {
sclaims.v = p.v
}
// Validate Claims // Validate Claims
if !p.SkipClaimsValidation { if !p.SkipClaimsValidation {
if err := token.Claims.Valid(); err != nil { if err := token.Claims.Valid(); err != nil {

View File

@ -1,5 +1,7 @@
package jwt package jwt
import "time"
// ParserOption is used to implement functional-style options that modify the behavior of the parser. To add // ParserOption is used to implement functional-style options that modify the behavior of the parser. To add
// new options, just create a function (ideally beginning with With or Without) that returns an anonymous function that // new options, just create a function (ideally beginning with With or Without) that returns an anonymous function that
// takes a *Parser type as input and manipulates its configuration accordingly. // takes a *Parser type as input and manipulates its configuration accordingly.
@ -27,3 +29,10 @@ func WithoutClaimsValidation() ParserOption {
p.SkipClaimsValidation = true p.SkipClaimsValidation = true
} }
} }
// WithLeeway returns the ParserOption for specifying the leeway window.
func WithLeeway(d time.Duration) ParserOption {
return func(p *Parser) {
p.v.leeway = d
}
}

View File

@ -321,6 +321,19 @@ var jwtTestData = []struct {
&jwt.Parser{UseJSONNumber: true}, &jwt.Parser{UseJSONNumber: true},
jwt.SigningMethodRS256, jwt.SigningMethodRS256,
}, },
{
"RFC7519 Claims - expired by 100s with 120s skew",
"", // autogen
defaultKeyFunc,
&jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Second * 100)),
},
true,
0,
nil,
jwt.NewParser(jwt.WithLeeway(2 * time.Minute)),
jwt.SigningMethodRS256,
},
} }
// signToken creates and returns a signed JWT token using signingMethod. // signToken creates and returns a signed JWT token using signingMethod.