diff --git a/claims.go b/claims.go index 9d95cad..08ee0e1 100644 --- a/claims.go +++ b/claims.go @@ -22,6 +22,8 @@ type Claims interface { // // See examples for how to use this with your own claim types. type RegisteredClaims struct { + v validator + // the `iss` (Issuer) claim. See https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 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. func (c *RegisteredClaims) VerifyExpiresAt(cmp time.Time, req bool) bool { 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). @@ -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. func (c *RegisteredClaims) VerifyNotBefore(cmp time.Time, req bool) bool { 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. @@ -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. type StandardClaims struct { + v validator + Audience string `json:"aud,omitempty"` ExpiresAt int64 `json:"exp,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. func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { 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) - 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). @@ -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. func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { 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) - 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. @@ -240,11 +244,12 @@ func verifyAud(aud []string, cmp string, required bool) bool { 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 { return !required } - return now.Before(*exp) + + return now.Before((*exp).Add(+skew)) } 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) } -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 { 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 { diff --git a/map_claims.go b/map_claims.go index 2700d64..8d7a883 100644 --- a/map_claims.go +++ b/map_claims.go @@ -45,14 +45,14 @@ func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { switch exp := v.(type) { case float64: 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: v, _ := exp.Float64() - return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req) + return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req, 0) } return false @@ -97,14 +97,14 @@ func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { switch nbf := v.(type) { case float64: 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: v, _ := nbf.Float64() - return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req) + return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req, 0) } return false diff --git a/parser.go b/parser.go index 2f61a69..53b4e0f 100644 --- a/parser.go +++ b/parser.go @@ -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. SkipClaimsValidation bool + + v validator } // 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{} + // 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 if !p.SkipClaimsValidation { if err := token.Claims.Valid(); err != nil { diff --git a/parser_option.go b/parser_option.go index 6ea6f95..133acbb 100644 --- a/parser_option.go +++ b/parser_option.go @@ -1,5 +1,7 @@ package jwt +import "time" + // 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 // takes a *Parser type as input and manipulates its configuration accordingly. @@ -27,3 +29,10 @@ func WithoutClaimsValidation() ParserOption { 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 + } +} diff --git a/parser_test.go b/parser_test.go index 68aa6a9..97e8fe0 100644 --- a/parser_test.go +++ b/parser_test.go @@ -321,6 +321,19 @@ var jwtTestData = []struct { &jwt.Parser{UseJSONNumber: true}, 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.