diff --git a/claims.go b/claims.go index 6248906..c5f302e 100644 --- a/claims.go +++ b/claims.go @@ -6,29 +6,37 @@ import ( "time" ) -// For a type to be a Claims object, it must just have a Valid method that determines -// if the token is invalid for any supported reason +// Claims must support the following. +// +// - Valid determines if token is invalid for any supported reason. type Claims interface { Valid() error } +// Leeway must support the following. +// +// - Allow set allowed leeway when validating iat and/or nbf claim. +type Leeway interface { + Allow(n time.Duration) Claims +} + // Structured version of Claims Section, as referenced at // https://tools.ietf.org/html/rfc7519#section-4.1 // See examples for how to use this with your own claim types type StandardClaims struct { Audience []string `json:"aud,omitempty"` - ExpiresAt int64 `json:"exp,omitempty"` - Id string `json:"jti,omitempty"` - IssuedAt int64 `json:"iat,omitempty"` - Issuer string `json:"iss,omitempty"` - NotBefore int64 `json:"nbf,omitempty"` - Subject string `json:"sub,omitempty"` + ExpiresAt int64 `json:"exp,omitempty"` + Id string `json:"jti,omitempty"` + IssuedAt int64 `json:"iat,omitempty"` + Issuer string `json:"iss,omitempty"` + NotBefore int64 `json:"nbf,omitempty"` + Subject string `json:"sub,omitempty"` + leeway int64 } -// Validates time based claims "exp, iat, nbf". -// There is no accounting for clock skew. -// As well, if any of the above claims are not in the token, it will still -// be considered a valid claim. +// Validates time based claims "exp, iat, nbf", as well, if any of the claims +// are not in the token, it will still be considered a valid claim. Leeway can +// be applied when optional through Leeway.Allow() through calling routine. func (c StandardClaims) Valid() error { vErr := new(ValidationError) now := TimeFunc().Unix() @@ -58,6 +66,12 @@ func (c StandardClaims) Valid() error { return vErr } +// Allow sets allowed leeway when validating claims nbf and iat. +func (c StandardClaims) Allow(n time.Duration) Claims { + c.leeway = int64(n.Seconds()) + return c +} + // Compares the aud claim against cmp. // If required is false, this method will return true if the value matches or is unset func (c *StandardClaims) VerifyAudience(cmp string, req bool) bool { @@ -73,7 +87,7 @@ func (c *StandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { // Compares the iat claim against cmp. // If required is false, this method will return true if the value matches or is unset func (c *StandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { - return verifyIat(c.IssuedAt, cmp, req) + return verifyIat(c.IssuedAt-c.leeway, cmp, req) } // Compares the iss claim against cmp. @@ -85,7 +99,7 @@ func (c *StandardClaims) VerifyIssuer(cmp string, req bool) bool { // Compares the nbf claim against cmp. // If required is false, this method will return true if the value matches or is unset func (c *StandardClaims) VerifyNotBefore(cmp int64, req bool) bool { - return verifyNbf(c.NotBefore, cmp, req) + return verifyNbf(c.NotBefore-c.leeway, cmp, req) } // ----- helpers diff --git a/hmac_example_test.go b/hmac_example_test.go index 98a4e4b..8bcb759 100644 --- a/hmac_example_test.go +++ b/hmac_example_test.go @@ -51,7 +51,7 @@ func ExampleParse_hmac() { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) } - + // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") return hmacSampleSecret, nil }) diff --git a/map_claims.go b/map_claims.go index 14b434c..e613350 100644 --- a/map_claims.go +++ b/map_claims.go @@ -3,11 +3,10 @@ package jwt import ( "encoding/json" "errors" - // "fmt" ) // Claims type that uses the map[string]interface{} for JSON decoding -// This is the default claims type if you don't supply one +// This is the default claims type if you don't supply one. type MapClaims map[string]interface{} // Compares the aud claim against cmp. diff --git a/parser.go b/parser.go index d6901d9..9047758 100644 --- a/parser.go +++ b/parser.go @@ -5,12 +5,14 @@ import ( "encoding/json" "fmt" "strings" + "time" ) type Parser struct { - ValidMethods []string // If populated, only these methods will be considered valid - UseJSONNumber bool // Use JSON Number format in JSON decoder - SkipClaimsValidation bool // Skip claims validation during token parsing + ValidMethods []string // If populated, only these methods will be considered valid + UseJSONNumber bool // Use JSON Number format in JSON decoder + SkipClaimsValidation bool // Skip claims validation during token parsing + Leeway time.Duration // Allowed leeway when validating iat, nbf claims. } // Parse, validate, and return a token. @@ -55,13 +57,15 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf } return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} } - vErr := &ValidationError{} - - // Validate Claims + // Validate Claims. if !p.SkipClaimsValidation { - if err := token.Claims.Valid(); err != nil { - + if t, ok := token.Claims.(Leeway); ok { + err = t.Allow(p.Leeway).Valid() + } else { + err = token.Claims.Valid() + } + if err != nil { // If the Claims Valid returned an error, check if it is a validation error, // If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set if e, ok := err.(*ValidationError); !ok { diff --git a/parser_test.go b/parser_test.go index 8a4a408..6665464 100644 --- a/parser_test.go +++ b/parser_test.go @@ -172,6 +172,38 @@ var jwtTestData = []struct { jwt.ValidationErrorNotValidYet | jwt.ValidationErrorExpired, &jwt.Parser{UseJSONNumber: true}, }, + { + "Validate iat and nbf with sufficient leeway", + "", // autogen + defaultKeyFunc, + &jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Minute * 5).Unix(), + IssuedAt: time.Now().Add(time.Minute * 4).Unix(), + NotBefore: time.Now().Add(time.Minute * 4).Unix(), + }, + true, + 0, + &jwt.Parser{ + UseJSONNumber: true, + Leeway: 5 * time.Minute, + }, + }, + { + "Validate iat and nbf with insufficient leeway", + "", // autogen + defaultKeyFunc, + &jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Minute * 5).Unix(), + IssuedAt: time.Now().Add(time.Minute * 4).Unix(), + NotBefore: time.Now().Add(time.Minute * 4).Unix(), + }, + false, + 0, + &jwt.Parser{ + UseJSONNumber: true, + Leeway: 1 * time.Minute, + }, + }, { "SkipClaimsValidation during token parsing", "", // autogen