From a49fa5d91db7b2230d0af0ea591b85f0fa77da3e Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 8 Nov 2023 14:21:44 +0100 Subject: [PATCH] Exported `NewValidator` (#349) * Exported `NewValidator` Previously, we had `newValidator` as a private function. This PR exports this function so that validation can be done independently of parsing the claim. --- MIGRATION_GUIDE.md | 12 +++++++++++- map_claims_test.go | 14 +++++++------- parser.go | 6 +++--- validator.go | 39 +++++++++++++++++++++++++-------------- validator_test.go | 20 ++++++++++---------- 5 files changed, 56 insertions(+), 35 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 6ad1c22..93f9649 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -17,7 +17,7 @@ and corresponding updates for existing programs. ## Parsing and Validation Options -Under the hood, a new `validator` struct takes care of validating the claims. A +Under the hood, a new `Validator` struct takes care of validating the claims. A long awaited feature has been the option to fine-tune the validation of tokens. This is now possible with several `ParserOption` functions that can be appended to most `Parse` functions, such as `ParseWithClaims`. The most important options @@ -68,6 +68,16 @@ type Claims interface { } ``` +Users that previously directly called the `Valid` function on their claims, +e.g., to perform validation independently of parsing/verifying a token, can now +use the `jwt.NewValidator` function to create a `Validator` independently of the +`Parser`. + +```go +var v = jwt.NewValidator(jwt.WithLeeway(5*time.Second)) +v.Validate(myClaims) +``` + ### Supported Claim Types and Removal of `StandardClaims` The two standard claim types supported by this library, `MapClaims` and diff --git a/map_claims_test.go b/map_claims_test.go index 8cd33db..034173d 100644 --- a/map_claims_test.go +++ b/map_claims_test.go @@ -62,7 +62,7 @@ func TestVerifyAud(t *testing.T) { opts = append(opts, WithAudience(test.Comparison)) } - validator := newValidator(opts...) + validator := NewValidator(opts...) got := validator.Validate(test.MapClaims) if (got == nil) != test.Expected { @@ -77,7 +77,7 @@ func TestMapclaimsVerifyIssuedAtInvalidTypeString(t *testing.T) { "iat": "foo", } want := false - got := newValidator(WithIssuedAt()).Validate(mapClaims) + got := NewValidator(WithIssuedAt()).Validate(mapClaims) if want != (got == nil) { t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil)) } @@ -88,7 +88,7 @@ func TestMapclaimsVerifyNotBeforeInvalidTypeString(t *testing.T) { "nbf": "foo", } want := false - got := newValidator().Validate(mapClaims) + got := NewValidator().Validate(mapClaims) if want != (got == nil) { t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil)) } @@ -99,7 +99,7 @@ func TestMapclaimsVerifyExpiresAtInvalidTypeString(t *testing.T) { "exp": "foo", } want := false - got := newValidator().Validate(mapClaims) + got := NewValidator().Validate(mapClaims) if want != (got == nil) { t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil)) @@ -112,14 +112,14 @@ func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) { "exp": float64(exp.Unix()), } want := false - got := newValidator(WithTimeFunc(func() time.Time { + got := NewValidator(WithTimeFunc(func() time.Time { return exp })).Validate(mapClaims) if want != (got == nil) { t.Fatalf("Failed to verify claims, wanted: %v got %v", want, (got == nil)) } - got = newValidator(WithTimeFunc(func() time.Time { + got = NewValidator(WithTimeFunc(func() time.Time { return exp.Add(1 * time.Second) })).Validate(mapClaims) if want != (got == nil) { @@ -127,7 +127,7 @@ func TestMapClaimsVerifyExpiresAtExpire(t *testing.T) { } want = true - got = newValidator(WithTimeFunc(func() time.Time { + got = NewValidator(WithTimeFunc(func() time.Time { return exp.Add(-1 * time.Second) })).Validate(mapClaims) if want != (got == nil) { diff --git a/parser.go b/parser.go index 1ed2e4e..ecf99af 100644 --- a/parser.go +++ b/parser.go @@ -18,7 +18,7 @@ type Parser struct { // Skip claims validation during token parsing. skipClaimsValidation bool - validator *validator + validator *Validator decodeStrict bool @@ -28,7 +28,7 @@ type Parser struct { // NewParser creates a new Parser with the specified options func NewParser(options ...ParserOption) *Parser { p := &Parser{ - validator: &validator{}, + validator: &Validator{}, } // Loop through our parsing options and apply them @@ -115,7 +115,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 = NewValidator() } if err := p.validator.Validate(claims); err != nil { diff --git a/validator.go b/validator.go index 3082c8c..008ecd8 100644 --- a/validator.go +++ b/validator.go @@ -28,13 +28,12 @@ type ClaimsValidator interface { Validate() error } -// validator is the core of the new Validation API. It is automatically used by +// Validator is the core of the new Validation API. It is automatically used by // a [Parser] during parsing and can be modified with various parser options. // -// Note: This struct is intentionally not exported (yet) as we want to -// internally finalize its API. In the future, we might make it publicly -// available. -type validator struct { +// The [NewValidator] function should be used to create an instance of this +// struct. +type Validator struct { // leeway is an optional leeway that can be provided to account for clock skew. leeway time.Duration @@ -65,16 +64,28 @@ type validator struct { expectedSub string } -// newValidator can be used to create a stand-alone validator with the supplied +// NewValidator can be used to create a stand-alone validator with the supplied // options. This validator can then be used to validate already parsed claims. -func newValidator(opts ...ParserOption) *validator { +// +// Note: Under normal circumstances, explicitly creating a validator is not +// needed and can potentially be dangerous; instead functions of the [Parser] +// class should be used. +// +// The [Validator] is only checking the *validity* of the claims, such as its +// expiration time, but it does NOT perform *signature verification* of the +// token. +func NewValidator(opts ...ParserOption) *Validator { p := NewParser(opts...) return p.validator } // Validate validates the given claims. It will also perform any custom // validation if claims implements the [ClaimsValidator] interface. -func (v *validator) Validate(claims Claims) error { +// +// Note: It will NOT perform any *signature verification* on the token that +// contains the claims and expects that the [Claim] was already successfully +// verified. +func (v *Validator) Validate(claims Claims) error { var ( now time.Time errs []error = make([]error, 0, 6) @@ -153,7 +164,7 @@ func (v *validator) Validate(claims Claims) error { // // Additionally, if any error occurs while retrieving the claim, e.g., when its // the wrong type, an ErrTokenUnverifiable error will be returned. -func (v *validator) verifyExpiresAt(claims Claims, cmp time.Time, required bool) error { +func (v *Validator) verifyExpiresAt(claims Claims, cmp time.Time, required bool) error { exp, err := claims.GetExpirationTime() if err != nil { return err @@ -174,7 +185,7 @@ func (v *validator) verifyExpiresAt(claims Claims, cmp time.Time, required bool) // // Additionally, if any error occurs while retrieving the claim, e.g., when its // the wrong type, an ErrTokenUnverifiable error will be returned. -func (v *validator) verifyIssuedAt(claims Claims, cmp time.Time, required bool) error { +func (v *Validator) verifyIssuedAt(claims Claims, cmp time.Time, required bool) error { iat, err := claims.GetIssuedAt() if err != nil { return err @@ -195,7 +206,7 @@ func (v *validator) verifyIssuedAt(claims Claims, cmp time.Time, required bool) // // Additionally, if any error occurs while retrieving the claim, e.g., when its // the wrong type, an ErrTokenUnverifiable error will be returned. -func (v *validator) verifyNotBefore(claims Claims, cmp time.Time, required bool) error { +func (v *Validator) verifyNotBefore(claims Claims, cmp time.Time, required bool) error { nbf, err := claims.GetNotBefore() if err != nil { return err @@ -215,7 +226,7 @@ func (v *validator) verifyNotBefore(claims Claims, cmp time.Time, required bool) // // Additionally, if any error occurs while retrieving the claim, e.g., when its // the wrong type, an ErrTokenUnverifiable error will be returned. -func (v *validator) verifyAudience(claims Claims, cmp string, required bool) error { +func (v *Validator) verifyAudience(claims Claims, cmp string, required bool) error { aud, err := claims.GetAudience() if err != nil { return err @@ -251,7 +262,7 @@ func (v *validator) verifyAudience(claims Claims, cmp string, required bool) err // // Additionally, if any error occurs while retrieving the claim, e.g., when its // the wrong type, an ErrTokenUnverifiable error will be returned. -func (v *validator) verifyIssuer(claims Claims, cmp string, required bool) error { +func (v *Validator) verifyIssuer(claims Claims, cmp string, required bool) error { iss, err := claims.GetIssuer() if err != nil { return err @@ -271,7 +282,7 @@ func (v *validator) verifyIssuer(claims Claims, cmp string, required bool) error // // Additionally, if any error occurs while retrieving the claim, e.g., when its // the wrong type, an ErrTokenUnverifiable error will be returned. -func (v *validator) verifySubject(claims Claims, cmp string, required bool) error { +func (v *Validator) verifySubject(claims Claims, cmp string, required bool) error { sub, err := claims.GetSubject() if err != nil { return err diff --git a/validator_test.go b/validator_test.go index 869b050..08a6bd7 100644 --- a/validator_test.go +++ b/validator_test.go @@ -20,7 +20,7 @@ func (m MyCustomClaims) Validate() error { return nil } -func Test_validator_Validate(t *testing.T) { +func Test_Validator_Validate(t *testing.T) { type fields struct { leeway time.Duration timeFunc func() time.Time @@ -71,7 +71,7 @@ func Test_validator_Validate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := &validator{ + v := &Validator{ leeway: tt.fields.leeway, timeFunc: tt.fields.timeFunc, verifyIat: tt.fields.verifyIat, @@ -86,7 +86,7 @@ func Test_validator_Validate(t *testing.T) { } } -func Test_validator_verifyExpiresAt(t *testing.T) { +func Test_Validator_verifyExpiresAt(t *testing.T) { type fields struct { leeway time.Duration timeFunc func() time.Time @@ -117,7 +117,7 @@ func Test_validator_verifyExpiresAt(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := &validator{ + v := &Validator{ leeway: tt.fields.leeway, timeFunc: tt.fields.timeFunc, } @@ -130,7 +130,7 @@ func Test_validator_verifyExpiresAt(t *testing.T) { } } -func Test_validator_verifyIssuer(t *testing.T) { +func Test_Validator_verifyIssuer(t *testing.T) { type fields struct { expectedIss string } @@ -160,7 +160,7 @@ func Test_validator_verifyIssuer(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := &validator{ + v := &Validator{ expectedIss: tt.fields.expectedIss, } err := v.verifyIssuer(tt.args.claims, tt.args.cmp, tt.args.required) @@ -171,7 +171,7 @@ func Test_validator_verifyIssuer(t *testing.T) { } } -func Test_validator_verifySubject(t *testing.T) { +func Test_Validator_verifySubject(t *testing.T) { type fields struct { expectedSub string } @@ -201,7 +201,7 @@ func Test_validator_verifySubject(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := &validator{ + v := &Validator{ expectedSub: tt.fields.expectedSub, } err := v.verifySubject(tt.args.claims, tt.args.cmp, tt.args.required) @@ -212,7 +212,7 @@ func Test_validator_verifySubject(t *testing.T) { } } -func Test_validator_verifyIssuedAt(t *testing.T) { +func Test_Validator_verifyIssuedAt(t *testing.T) { type fields struct { leeway time.Duration timeFunc func() time.Time @@ -248,7 +248,7 @@ func Test_validator_verifyIssuedAt(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - v := &validator{ + v := &Validator{ leeway: tt.fields.leeway, timeFunc: tt.fields.timeFunc, verifyIat: tt.fields.verifyIat,