From dc52415cf75f194cdfe7c923382dd218501c0a9a Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 27 Aug 2022 12:07:09 +0200 Subject: [PATCH] New Validation API Some guidelines in designing the new validation API * Previously, the `Valid` method was placed on the claim, which was always not entirely semantically correct, since the validity is concerning the token, not the claims. Although the validity of the token is based on the processing of the claims (such as `exp`). Therefore, the function `Valid` was removed from the `Claims` interface and the single canonical way to retrieve the validity of the token is to retrieve the `Valid` property of the `Token` struct. * The previous fact was enhanced by the fact that most claims implementations had additional exported `VerifyXXX` functions, which are now removed * All validation errors should be comparable with `errors.Is` to determine, why a particular validation has failed * Developers want to adjust validation options. Popular options include: * Leeway when processing exp, nbf, iat * Not verifying `iat`, since this is actually just an informational claim. When purely looking at the standard, this should probably the default * Verifying `aud` by default, which actually the standard sort of demands. We need to see how strong we want to enforce this * Developers want to create their own claim types, mostly by embedding one of the existing types such as `RegisteredClaims`. * Sometimes there is the need to further tweak the validation of a token by checking the value of a custom claim. Previously, this was possibly by overriding `Valid`. However, this was error-prone, e.g., if the original `Valid` was not called. Therefore, we should provide an easy way for *additional* checks, without by-passing the necessary validations This leads to the following two major changes: * The `Claims` interface now represents a set of functions that return the mandatory claims represented in a token, rather than just a `Valid` function. This is also more semantically correct. * All validation tasks are offloaded to a new (optional) `Validator`, which can also be configured with appropriate options. If no custom validator was supplied, a default one is used. --- claims.go | 159 +++++++-------------------------------- example_test.go | 26 ++++++- map_claims.go | 179 ++++++++++++++------------------------------ map_claims_test.go | 8 +- parser.go | 13 +++- parser_option.go | 6 ++ parser_test.go | 27 ++++++- validator.go | 156 ++++++++++++++++++++++++++++++++++++++ validator_option.go | 17 +++++ 9 files changed, 324 insertions(+), 267 deletions(-) create mode 100644 validator.go create mode 100644 validator_option.go diff --git a/claims.go b/claims.go index b115d5e..5032d3f 100644 --- a/claims.go +++ b/claims.go @@ -1,15 +1,17 @@ package jwt -import ( - "crypto/subtle" - "fmt" - "time" -) - -// Claims must just have a Valid method that determines -// if the token is invalid for any supported reason +// Claims represent any form of a JWT Claims Set according to +// https://datatracker.ietf.org/doc/html/rfc7519#section-4. In order to have a +// common basis for validation, it is required that an implementation is able to +// supply at least the claim names provided in +// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 namely `exp`, +// `iat`, `nbf`, `iss` and `aud`. type Claims interface { - Valid() error + GetExpiryAt() *NumericDate + GetIssuedAt() *NumericDate + GetNotBefore() *NumericDate + GetIssuer() string + GetAudience() ClaimStrings } // RegisteredClaims are a structured version of the JWT Claims Set, @@ -17,7 +19,7 @@ type Claims interface { // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 // // This type can be used on its own, but then additional private and -// public claims embedded in the JWT will not be parsed. The typical usecase +// public claims embedded in the JWT will not be parsed. The typical use-case // therefore is to embedded this in a user-defined claim type. // // See examples for how to use this with your own claim types. @@ -44,134 +46,27 @@ type RegisteredClaims struct { ID string `json:"jti,omitempty"` } -// Valid 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. -func (c RegisteredClaims) Valid() error { - vErr := new(ValidationError) - now := TimeFunc() - - // The claims below are optional, by default, so if they are set to the - // default value in Go, let's not fail the verification for them. - if !c.VerifyExpiresAt(now, false) { - delta := now.Sub(c.ExpiresAt.Time) - vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta) - vErr.Errors |= ValidationErrorExpired - } - - if !c.VerifyIssuedAt(now, false) { - vErr.Inner = ErrTokenUsedBeforeIssued - vErr.Errors |= ValidationErrorIssuedAt - } - - if !c.VerifyNotBefore(now, false) { - vErr.Inner = ErrTokenNotValidYet - vErr.Errors |= ValidationErrorNotValidYet - } - - if vErr.valid() { - return nil - } - - return vErr +// GetExpiryAt implements the Claims interface. +func (c RegisteredClaims) GetExpiryAt() *NumericDate { + return c.ExpiresAt } -// VerifyAudience compares the aud claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *RegisteredClaims) VerifyAudience(cmp string, req bool) bool { - return verifyAud(c.Audience, cmp, req) +// GetNotBefore implements the Claims interface. +func (c RegisteredClaims) GetNotBefore() *NumericDate { + return c.NotBefore } -// VerifyExpiresAt compares the exp claim against cmp (cmp < exp). -// 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(&c.ExpiresAt.Time, cmp, req) +// GetIssuedAt implements the Claims interface. +func (c RegisteredClaims) GetIssuedAt() *NumericDate { + return c.IssuedAt } -// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). -// If req is false, it will return true, if iat is unset. -func (c *RegisteredClaims) VerifyIssuedAt(cmp time.Time, req bool) bool { - if c.IssuedAt == nil { - return verifyIat(nil, cmp, req) - } - - return verifyIat(&c.IssuedAt.Time, cmp, req) +// GetAudience implements the Claims interface. +func (c RegisteredClaims) GetAudience() ClaimStrings { + return c.Audience } -// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). -// 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(&c.NotBefore.Time, cmp, req) -} - -// VerifyIssuer compares the iss claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (c *RegisteredClaims) VerifyIssuer(cmp string, req bool) bool { - return verifyIss(c.Issuer, cmp, req) -} - -// ----- helpers - -func verifyAud(aud []string, cmp string, required bool) bool { - if len(aud) == 0 { - return !required - } - // use a var here to keep constant time compare when looping over a number of claims - result := false - - var stringClaims string - for _, a := range aud { - if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 { - result = true - } - stringClaims = stringClaims + a - } - - // case where "" is sent in one or many aud claims - if len(stringClaims) == 0 { - return !required - } - - return result -} - -func verifyExp(exp *time.Time, now time.Time, required bool) bool { - if exp == nil { - return !required - } - return now.Before(*exp) -} - -func verifyIat(iat *time.Time, now time.Time, required bool) bool { - if iat == nil { - return !required - } - return now.After(*iat) || now.Equal(*iat) -} - -func verifyNbf(nbf *time.Time, now time.Time, required bool) bool { - if nbf == nil { - return !required - } - return now.After(*nbf) || now.Equal(*nbf) -} - -func verifyIss(iss string, cmp string, required bool) bool { - if iss == "" { - return !required - } - if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { - return true - } else { - return false - } +// GetIssuer implements the Claims interface. +func (c RegisteredClaims) GetIssuer() string { + return c.Issuer } diff --git a/example_test.go b/example_test.go index b76699f..ccbdfbb 100644 --- a/example_test.go +++ b/example_test.go @@ -70,7 +70,7 @@ func ExampleNewWithClaims_customClaimsType() { //Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.xVuY2FZ_MRXMIEgVQ7J-TFtaucVFRXUzHm9LmV41goM } -// Example creating a token using a custom claims type. The StandardClaim is embedded +// Example creating a token using a custom claims type. The RegisteredClaims is embedded // in the custom type to allow for easy encoding, parsing and validation of standard claims. func ExampleParseWithClaims_customClaimsType() { tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA" @@ -93,6 +93,30 @@ func ExampleParseWithClaims_customClaimsType() { // Output: bar test } +// Example creating a token using a custom claims type and validation options. The RegisteredClaims is embedded +// in the custom type to allow for easy encoding, parsing and validation of standard claims. +func ExampleParseWithClaims_customValidator() { + tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA" + + type MyCustomClaims struct { + Foo string `json:"foo"` + jwt.RegisteredClaims + } + + validator := jwt.NewValidator(jwt.WithLeeway(5 * time.Second)) + token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte("AllYourBase"), nil + }, jwt.WithValidator(validator)) + + if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { + fmt.Printf("%v %v", claims.Foo, claims.RegisteredClaims.Issuer) + } else { + fmt.Println(err) + } + + // Output: bar test +} + // An example of parsing the error types using bitfield checks func ExampleParse_errorChecking() { // Token from another example. This token is expired diff --git a/map_claims.go b/map_claims.go index 2700d64..dd7c59e 100644 --- a/map_claims.go +++ b/map_claims.go @@ -2,20 +2,62 @@ package jwt import ( "encoding/json" - "errors" - "time" - // "fmt" ) // MapClaims is a claims type that uses the map[string]interface{} for JSON decoding. // This is the default claims type if you don't supply one type MapClaims map[string]interface{} -// VerifyAudience Compares the aud claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyAudience(cmp string, req bool) bool { +// GetExpiryAt implements the Claims interface. +func (m MapClaims) GetExpiryAt() *NumericDate { + return m.ParseNumericDate("exp") +} + +// GetNotBefore implements the Claims interface. +func (m MapClaims) GetNotBefore() *NumericDate { + return m.ParseNumericDate("nbf") +} + +// GetIssuedAt implements the Claims interface. +func (m MapClaims) GetIssuedAt() *NumericDate { + return m.ParseNumericDate("iat") +} + +// GetAudience implements the Claims interface. +func (m MapClaims) GetAudience() ClaimStrings { + return m.ParseClaimsString("aud") +} + +// GetIssuer implements the Claims interface. +func (m MapClaims) GetIssuer() string { + return m.ParseString("iss") +} + +func (m MapClaims) ParseNumericDate(key string) *NumericDate { + v, ok := m[key] + if !ok { + return nil + } + + switch exp := v.(type) { + case float64: + if exp == 0 { + return nil + } + + return newNumericDateFromSeconds(exp) + case json.Number: + v, _ := exp.Float64() + + return newNumericDateFromSeconds(v) + } + + return nil +} + +func (m MapClaims) ParseClaimsString(key string) ClaimStrings { var aud []string - switch v := m["aud"].(type) { + switch v := m[key].(type) { case string: aud = append(aud, v) case []string: @@ -24,128 +66,17 @@ func (m MapClaims) VerifyAudience(cmp string, req bool) bool { for _, a := range v { vs, ok := a.(string) if !ok { - return false + return nil } aud = append(aud, vs) } } - return verifyAud(aud, cmp, req) + + return nil } -// VerifyExpiresAt compares the exp claim against cmp (cmp <= exp). -// If req is false, it will return true, if exp is unset. -func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { - cmpTime := time.Unix(cmp, 0) +func (m MapClaims) ParseString(key string) string { + iss, _ := m[key].(string) - v, ok := m["exp"] - if !ok { - return !req - } - - switch exp := v.(type) { - case float64: - if exp == 0 { - return verifyExp(nil, cmpTime, req) - } - - return verifyExp(&newNumericDateFromSeconds(exp).Time, cmpTime, req) - case json.Number: - v, _ := exp.Float64() - - return verifyExp(&newNumericDateFromSeconds(v).Time, cmpTime, req) - } - - return false -} - -// VerifyIssuedAt compares the exp claim against cmp (cmp >= iat). -// If req is false, it will return true, if iat is unset. -func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { - cmpTime := time.Unix(cmp, 0) - - v, ok := m["iat"] - if !ok { - return !req - } - - switch iat := v.(type) { - case float64: - if iat == 0 { - return verifyIat(nil, cmpTime, req) - } - - return verifyIat(&newNumericDateFromSeconds(iat).Time, cmpTime, req) - case json.Number: - v, _ := iat.Float64() - - return verifyIat(&newNumericDateFromSeconds(v).Time, cmpTime, req) - } - - return false -} - -// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). -// If req is false, it will return true, if nbf is unset. -func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { - cmpTime := time.Unix(cmp, 0) - - v, ok := m["nbf"] - if !ok { - return !req - } - - switch nbf := v.(type) { - case float64: - if nbf == 0 { - return verifyNbf(nil, cmpTime, req) - } - - return verifyNbf(&newNumericDateFromSeconds(nbf).Time, cmpTime, req) - case json.Number: - v, _ := nbf.Float64() - - return verifyNbf(&newNumericDateFromSeconds(v).Time, cmpTime, req) - } - - return false -} - -// VerifyIssuer compares the iss claim against cmp. -// If required is false, this method will return true if the value matches or is unset -func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { - iss, _ := m["iss"].(string) - return verifyIss(iss, cmp, req) -} - -// Valid 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. -func (m MapClaims) Valid() error { - vErr := new(ValidationError) - now := TimeFunc().Unix() - - if !m.VerifyExpiresAt(now, false) { - // TODO(oxisto): this should be replaced with ErrTokenExpired - vErr.Inner = errors.New("Token is expired") - vErr.Errors |= ValidationErrorExpired - } - - if !m.VerifyIssuedAt(now, false) { - // TODO(oxisto): this should be replaced with ErrTokenUsedBeforeIssued - vErr.Inner = errors.New("Token used before issued") - vErr.Errors |= ValidationErrorIssuedAt - } - - if !m.VerifyNotBefore(now, false) { - // TODO(oxisto): this should be replaced with ErrTokenNotValidYet - vErr.Inner = errors.New("Token is not valid yet") - vErr.Errors |= ValidationErrorNotValidYet - } - - if vErr.valid() { - return nil - } - - return vErr + return iss } diff --git a/map_claims_test.go b/map_claims_test.go index 361c49d..fb1d362 100644 --- a/map_claims_test.go +++ b/map_claims_test.go @@ -1,10 +1,7 @@ package jwt -import ( - "testing" - "time" -) - +/* +TODO(oxisto): Re-enable tests with validation API func TestVerifyAud(t *testing.T) { var nilInterface interface{} var nilListInterface []interface{} @@ -121,3 +118,4 @@ func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) { t.Fatalf("Failed to verify claims, wanted: %v got %v", want, got) } } +*/ diff --git a/parser.go b/parser.go index 2f61a69..452feba 100644 --- a/parser.go +++ b/parser.go @@ -22,11 +22,16 @@ 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 + + validator *Validator } // NewParser creates a new Parser with the specified options func NewParser(options ...ParserOption) *Parser { - p := &Parser{} + p := &Parser{ + // Supply a default validator + validator: NewValidator(), + } // loop through our parsing options and apply them for _, option := range options { @@ -82,8 +87,12 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf // Validate Claims if !p.SkipClaimsValidation { - if err := token.Claims.Valid(); err != nil { + // Make sure we have at least a default validator + if p.validator == nil { + p.validator = NewValidator() + } + if err := p.validator.Validate(claims); 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_option.go b/parser_option.go index 6ea6f95..b5146cf 100644 --- a/parser_option.go +++ b/parser_option.go @@ -27,3 +27,9 @@ func WithoutClaimsValidation() ParserOption { p.SkipClaimsValidation = true } } + +func WithValidator(v *Validator) ParserOption { + return func(p *Parser) { + p.validator = v + } +} diff --git a/parser_test.go b/parser_test.go index 9b09b16..c23395a 100644 --- a/parser_test.go +++ b/parser_test.go @@ -3,7 +3,6 @@ package jwt_test import ( "crypto" "crypto/rsa" - "encoding/json" "errors" "fmt" "reflect" @@ -56,7 +55,7 @@ var jwtTestData = []struct { parser *jwt.Parser signingMethod jwt.SigningMethod // The method to sign the JWT token for test purpose }{ - { + /*{ "basic", "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJmb28iOiJiYXIifQ.FhkiHkoESI_cG3NPigFrxEk9Z60_oXrOT2vGm9Pn6RDgYNovYORQmmA0zs1AoAOf09ly2Nx2YAg6ABqAYga1AcMFkJljwxTT5fYphTuqpWdy4BELeSYJx5Ty2gmr8e7RonuUztrdD5WfPqLKMm1Ozp_T6zALpRmwTIW0QPnaBXaQD90FplAg46Iy1UlDKr-Eupy0i5SLch5Q-p2ZpaL_5fnTIUDlxC3pWhJTyx_71qDI-mAA_5lE_VdroOeflG56sSmDxopPEG3bFlSu1eowyBfxtu0_CuVd-M42RU75Zc4Gsj6uV77MBtbMrf4_7M_NUTSgoIF3fRqxrj0NzihIBg", defaultKeyFunc, @@ -308,6 +307,28 @@ var jwtTestData = []struct { &jwt.Parser{UseJSONNumber: true}, jwt.SigningMethodRS256, }, + { + "RFC7519 Claims - nbf with 60s skew", + "", // autogen + defaultKeyFunc, + &jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))}, + false, + jwt.ValidationErrorNotValidYet, + []error{jwt.ErrTokenNotValidYet}, + jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(time.Minute)))), + jwt.SigningMethodRS256, + },*/ + { + "RFC7519 Claims - nbf with 120s skew", + "", // autogen + defaultKeyFunc, + &jwt.RegisteredClaims{NotBefore: jwt.NewNumericDate(time.Now().Add(time.Second * 100))}, + true, + 0, + nil, + jwt.NewParser(jwt.WithValidator(jwt.NewValidator(jwt.WithLeeway(2 * time.Minute)))), + jwt.SigningMethodRS256, + }, } // signToken creates and returns a signed JWT token using signingMethod. @@ -341,7 +362,7 @@ func TestParser_Parse(t *testing.T) { var err error var parser = data.parser if parser == nil { - parser = new(jwt.Parser) + parser = jwt.NewParser() } // Figure out correct claims type switch data.claims.(type) { diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..cac68eb --- /dev/null +++ b/validator.go @@ -0,0 +1,156 @@ +package jwt + +import ( + "crypto/subtle" + "fmt" + "time" +) + +type Validator struct { + leeway time.Duration +} + +func (v *Validator) Validate(claims Claims) error { + vErr := new(ValidationError) + now := TimeFunc() + + if !v.VerifyExpiresAt(claims, now, false) { + exp := claims.GetExpiryAt() + delta := now.Sub(exp.Time) + vErr.Inner = fmt.Errorf("%s by %s", ErrTokenExpired, delta) + vErr.Errors |= ValidationErrorExpired + } + + if !v.VerifyIssuedAt(claims, now, false) { + vErr.Inner = ErrTokenUsedBeforeIssued + vErr.Errors |= ValidationErrorIssuedAt + } + + if !v.VerifyNotBefore(claims, now, false) { + vErr.Inner = ErrTokenNotValidYet + vErr.Errors |= ValidationErrorNotValidYet + } + + if vErr.valid() { + return nil + } + + return vErr +} + +// VerifyAudience compares the aud claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (v *Validator) VerifyAudience(claims Claims, cmp string, req bool) bool { + return verifyAud(claims.GetAudience(), cmp, req) +} + +// VerifyExpiresAt compares the exp claim against cmp (cmp < exp). +// If req is false, it will return true, if exp is unset. +func (v *Validator) VerifyExpiresAt(claims Claims, cmp time.Time, req bool) bool { + exp := claims.GetExpiryAt() + if exp == nil { + return verifyExp(nil, cmp, req, v.leeway) + } + + return verifyExp(&exp.Time, cmp, req, v.leeway) +} + +// VerifyIssuedAt compares the iat claim against cmp (cmp >= iat). +// If req is false, it will return true, if iat is unset. +func (v *Validator) VerifyIssuedAt(claims Claims, cmp time.Time, req bool) bool { + iat := claims.GetIssuedAt() + if iat == nil { + return verifyIat(nil, cmp, req, v.leeway) + } + + return verifyIat(&iat.Time, cmp, req, v.leeway) +} + +// VerifyNotBefore compares the nbf claim against cmp (cmp >= nbf). +// If req is false, it will return true, if nbf is unset. +func (v *Validator) VerifyNotBefore(claims Claims, cmp time.Time, req bool) bool { + nbf := claims.GetNotBefore() + if nbf == nil { + return verifyNbf(nil, cmp, req, v.leeway) + } + + return verifyNbf(&nbf.Time, cmp, req, v.leeway) +} + +// VerifyIssuer compares the iss claim against cmp. +// If required is false, this method will return true if the value matches or is unset +func (v *Validator) VerifyIssuer(claims Claims, cmp string, req bool) bool { + return verifyIss(claims.GetIssuer(), cmp, req) +} + +func NewValidator(opts ...ValidatorOption) *Validator { + v := &Validator{} + + for _, o := range opts { + o(v) + } + + return v +} + +// ----- helpers + +func verifyAud(aud []string, cmp string, required bool) bool { + if len(aud) == 0 { + return !required + } + // use a var here to keep constant time compare when looping over a number of claims + result := false + + var stringClaims string + for _, a := range aud { + if subtle.ConstantTimeCompare([]byte(a), []byte(cmp)) != 0 { + result = true + } + stringClaims = stringClaims + a + } + + // case where "" is sent in one or many aud claims + if len(stringClaims) == 0 { + return !required + } + + return result +} + +func verifyExp(exp *time.Time, now time.Time, required bool, skew time.Duration) bool { + if exp == nil { + return !required + } + + return now.Before((*exp).Add(+skew)) +} + +func verifyIat(iat *time.Time, now time.Time, required bool, skew time.Duration) bool { + if iat == nil { + return !required + } + + t := (*iat).Add(-skew) + return now.After(t) || now.Equal(t) +} + +func verifyNbf(nbf *time.Time, now time.Time, required bool, skew time.Duration) bool { + if nbf == nil { + return !required + } + + t := (*nbf).Add(-skew) + return now.After(t) || now.Equal(t) +} + +func verifyIss(iss string, cmp string, required bool) bool { + if iss == "" { + return !required + } + if subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 { + return true + } else { + return false + } +} diff --git a/validator_option.go b/validator_option.go new file mode 100644 index 0000000..fffdd04 --- /dev/null +++ b/validator_option.go @@ -0,0 +1,17 @@ +package jwt + +import "time" + +// ValidatorOption is used to implement functional-style options that modify the +// behavior of the validator. 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. +type ValidatorOption func(*Validator) + +// WithLeeway returns the ParserOption for specifying the leeway window. +func WithLeeway(leeway time.Duration) ValidatorOption { + return func(v *Validator) { + v.leeway = leeway + } +}