From 4990d2cdf3d4d1ede9d8e7b91841183cca57edf9 Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Sat, 27 Aug 2022 13:36:45 +0200 Subject: [PATCH] Added timeFunc, made iat optional --- example_test.go | 38 ++++++++++++++++++++++++++-- parser.go | 12 ++++----- token.go | 6 ----- validator.go | 60 ++++++++++++++++++++++++++++++++++++--------- validator_option.go | 19 +++++++++++++- 5 files changed, 108 insertions(+), 27 deletions(-) diff --git a/example_test.go b/example_test.go index ccbdfbb8..55dfa398 100644 --- a/example_test.go +++ b/example_test.go @@ -95,7 +95,7 @@ func ExampleParseWithClaims_customClaimsType() { // 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() { +func ExampleParseWithClaims_validationOptions() { tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA" type MyCustomClaims struct { @@ -117,7 +117,41 @@ func ExampleParseWithClaims_customValidator() { // Output: bar test } -// An example of parsing the error types using bitfield checks +type MyCustomClaims struct { + Foo string `json:"foo"` + jwt.RegisteredClaims +} + +func (m MyCustomClaims) CustomValidation() error { + if m.Foo != "bar" { + return errors.New("must be foobar") + } + + return nil +} + +// 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 and the function +// CustomValidation is implemented. +func ExampleParseWithClaims_customValidation() { + tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJ0ZXN0IiwiYXVkIjoic2luZ2xlIn0.QAWg1vGvnqRuCFTMcPkjZljXHh8U3L_qUjszOtQbeaA" + + 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 errors.Is. func ExampleParse_errorChecking() { // Token from another example. This token is expired var tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJleHAiOjE1MDAwLCJpc3MiOiJ0ZXN0In0.HE7fK0xOQwFEr4WDgRWj4teRPZ6i3GLwD5YCm6Pwu_c" diff --git a/parser.go b/parser.go index 452febaf..6b44a953 100644 --- a/parser.go +++ b/parser.go @@ -7,6 +7,9 @@ import ( "strings" ) +// DefaultValidator is the default validator that is used, if no custom validator is supplied in a Parser. +var DefaultValidator = NewValidator() + type Parser struct { // If populated, only these methods will be considered valid. // @@ -28,12 +31,9 @@ type Parser struct { // NewParser creates a new Parser with the specified options func NewParser(options ...ParserOption) *Parser { - p := &Parser{ - // Supply a default validator - validator: NewValidator(), - } + p := &Parser{} - // loop through our parsing options and apply them + // Loop through our parsing options and apply them for _, option := range options { option(p) } @@ -89,7 +89,7 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf if !p.SkipClaimsValidation { // Make sure we have at least a default validator if p.validator == nil { - p.validator = NewValidator() + p.validator = DefaultValidator } if err := p.validator.Validate(claims); err != nil { diff --git a/token.go b/token.go index 3cb0f3f0..738eef0e 100644 --- a/token.go +++ b/token.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "encoding/json" "strings" - "time" ) // DecodePaddingAllowed will switch the codec used for decoding JWTs respectively. Note that the JWS RFC7515 @@ -14,11 +13,6 @@ import ( // To use the non-recommended decoding, set this boolean to `true` prior to using this package. var DecodePaddingAllowed bool -// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time). -// You can override it to use another time value. This is useful for testing or if your -// server uses a different time zone than your tokens. -var TimeFunc = time.Now - // Keyfunc will be used by the Parse methods as a callback function to supply // the key for verification. The function receives the parsed, // but unverified Token. This allows you to use properties in the diff --git a/validator.go b/validator.go index d85a2890..0483e3a1 100644 --- a/validator.go +++ b/validator.go @@ -6,13 +6,48 @@ import ( "time" ) +// Validator is the core of the new Validation API. It is type Validator struct { + // leeway is an optional leeway that can be provided to account for clock skew. leeway time.Duration + + // timeFunc is used to supply the current time that is needed for + // validation. If unspecified, this defaults to time.Now. + timeFunc func() time.Time + + // verifyIat specifies whether the iat (Issued At) claim will be verified. + // According to https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 this + // only specifies the age of the token, but no validation check is + // necessary. However, if wanted, it can be checked if the iat is + // unrealistic, i.e., in the future. + verifyIat bool +} + +type customValidationType interface { + CustomValidation() error +} + +func NewValidator(opts ...ValidatorOption) *Validator { + v := &Validator{} + + // Apply the validator options + for _, o := range opts { + o(v) + } + + return v } func (v *Validator) Validate(claims Claims) error { + var now time.Time vErr := new(ValidationError) - now := TimeFunc() + + // Check, if we have a time func + if v.timeFunc != nil { + now = v.timeFunc() + } else { + now = time.Now() + } if !v.VerifyExpiresAt(claims, now, false) { exp := claims.GetExpirationTime() @@ -21,7 +56,8 @@ func (v *Validator) Validate(claims Claims) error { vErr.Errors |= ValidationErrorExpired } - if !v.VerifyIssuedAt(claims, now, false) { + // Check iat if the option is enabled + if v.verifyIat && !v.VerifyIssuedAt(claims, now, false) { vErr.Inner = ErrTokenUsedBeforeIssued vErr.Errors |= ValidationErrorIssuedAt } @@ -31,6 +67,16 @@ func (v *Validator) Validate(claims Claims) error { vErr.Errors |= ValidationErrorNotValidYet } + // Finally, we want to give the claim itself some possibility to do some + // additional custom validation based on their custom claims + cvt, ok := claims.(customValidationType) + if ok { + if err := cvt.CustomValidation(); err != nil { + vErr.Inner = err + vErr.Errors |= ValidationErrorClaimsInvalid + } + } + if vErr.valid() { return nil } @@ -83,16 +129,6 @@ 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 { diff --git a/validator_option.go b/validator_option.go index fffdd047..4cc81c0e 100644 --- a/validator_option.go +++ b/validator_option.go @@ -9,9 +9,26 @@ import "time" // accordingly. type ValidatorOption func(*Validator) -// WithLeeway returns the ParserOption for specifying the leeway window. +// WithLeeway returns the ValidatorOption for specifying the leeway window. func WithLeeway(leeway time.Duration) ValidatorOption { return func(v *Validator) { v.leeway = leeway } } + +// WithTimeFunc returns the ValidatorOption for specifying the time func. The +// primary use-case for this is testing. If you are looking for a way to account +// for clock-skew, WithLeeway should be used instead. +func WithTimeFunc(f func() time.Time) ValidatorOption { + return func(v *Validator) { + v.timeFunc = f + } +} + +// WithIssuedAtVerification returns the ValidatorOption to enable verification +// of issued-at. +func WithIssuedAtVerification() ValidatorOption { + return func(v *Validator) { + v.verifyIat = true + } +}