From a49fa5d91db7b2230d0af0ea591b85f0fa77da3e Mon Sep 17 00:00:00 2001 From: Christian Banse Date: Wed, 8 Nov 2023 14:21:44 +0100 Subject: [PATCH 1/3] 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 6ad1c22b..93f96495 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 8cd33db3..034173d2 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 1ed2e4e4..ecf99af7 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 3082c8c7..008ecd87 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 869b0507..08a6bd71 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, From b05644bf946495734b07010a9af7e357a1f239d8 Mon Sep 17 00:00:00 2001 From: Laurin-Notemann <47634664+Laurin-Notemann@users.noreply.github.com> Date: Fri, 17 Nov 2023 19:45:07 +0100 Subject: [PATCH 2/3] Improve ErrInvalidKeyType error messages (#361) * Improve ErrInvalidKeyType error message * add specific expected type to error message * fix ErrInvalidKey error to ErrInvalidKeyType in rsa and rsapss * format * revert changes from example_test.go remove the comments * fix: udpate the signing names to uppercase --- ecdsa.go | 4 ++-- ed25519.go | 4 ++-- hmac.go | 4 ++-- rsa.go | 4 ++-- rsa_pss.go | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ecdsa.go b/ecdsa.go index 4ccae2a8..ca85659b 100644 --- a/ecdsa.go +++ b/ecdsa.go @@ -62,7 +62,7 @@ func (m *SigningMethodECDSA) Verify(signingString string, sig []byte, key interf case *ecdsa.PublicKey: ecdsaKey = k default: - return ErrInvalidKeyType + return newError("ECDSA verify expects *ecsda.PublicKey", ErrInvalidKeyType) } if len(sig) != 2*m.KeySize { @@ -96,7 +96,7 @@ func (m *SigningMethodECDSA) Sign(signingString string, key interface{}) ([]byte case *ecdsa.PrivateKey: ecdsaKey = k default: - return nil, ErrInvalidKeyType + return nil, newError("ECDSA sign expects *ecsda.PrivateKey", ErrInvalidKeyType) } // Create the hasher diff --git a/ed25519.go b/ed25519.go index eb6bdf01..c2138119 100644 --- a/ed25519.go +++ b/ed25519.go @@ -38,7 +38,7 @@ func (m *SigningMethodEd25519) Verify(signingString string, sig []byte, key inte var ok bool if ed25519Key, ok = key.(ed25519.PublicKey); !ok { - return ErrInvalidKeyType + return newError("Ed25519 verify expects ed25519.PublicKey", ErrInvalidKeyType) } if len(ed25519Key) != ed25519.PublicKeySize { @@ -60,7 +60,7 @@ func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) ([]by var ok bool if ed25519Key, ok = key.(crypto.Signer); !ok { - return nil, ErrInvalidKeyType + return nil, newError("Ed25519 sign expects crypto.Signer", ErrInvalidKeyType) } if _, ok := ed25519Key.Public().(ed25519.PublicKey); !ok { diff --git a/hmac.go b/hmac.go index 91b688ba..96c62722 100644 --- a/hmac.go +++ b/hmac.go @@ -59,7 +59,7 @@ func (m *SigningMethodHMAC) Verify(signingString string, sig []byte, key interfa // Verify the key is the right type keyBytes, ok := key.([]byte) if !ok { - return ErrInvalidKeyType + return newError("HMAC verify expects []byte", ErrInvalidKeyType) } // Can we use the specified hashing method? @@ -91,7 +91,7 @@ func (m *SigningMethodHMAC) Verify(signingString string, sig []byte, key interfa func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) ([]byte, error) { if keyBytes, ok := key.([]byte); ok { if !m.Hash.Available() { - return nil, ErrHashUnavailable + return nil, newError("HMAC sign expects []byte", ErrInvalidKeyType) } hasher := hmac.New(m.Hash.New, keyBytes) diff --git a/rsa.go b/rsa.go index daff0943..83cbee6a 100644 --- a/rsa.go +++ b/rsa.go @@ -51,7 +51,7 @@ func (m *SigningMethodRSA) Verify(signingString string, sig []byte, key interfac var ok bool if rsaKey, ok = key.(*rsa.PublicKey); !ok { - return ErrInvalidKeyType + return newError("RSA verify expects *rsa.PublicKey", ErrInvalidKeyType) } // Create hasher @@ -73,7 +73,7 @@ func (m *SigningMethodRSA) Sign(signingString string, key interface{}) ([]byte, // Validate type of key if rsaKey, ok = key.(*rsa.PrivateKey); !ok { - return nil, ErrInvalidKey + return nil, newError("RSA sign expects *rsa.PrivateKey", ErrInvalidKeyType) } // Create the hasher diff --git a/rsa_pss.go b/rsa_pss.go index 9599f0a4..28c386ec 100644 --- a/rsa_pss.go +++ b/rsa_pss.go @@ -88,7 +88,7 @@ func (m *SigningMethodRSAPSS) Verify(signingString string, sig []byte, key inter case *rsa.PublicKey: rsaKey = k default: - return ErrInvalidKey + return newError("RSA-PSS verify expects *rsa.PublicKey", ErrInvalidKeyType) } // Create hasher @@ -115,7 +115,7 @@ func (m *SigningMethodRSAPSS) Sign(signingString string, key interface{}) ([]byt case *rsa.PrivateKey: rsaKey = k default: - return nil, ErrInvalidKeyType + return nil, newError("RSA-PSS sign expects *rsa.PrivateKey", ErrInvalidKeyType) } // Create the hasher From b27c88965d976075194dc8125f1f9c60bd8cf162 Mon Sep 17 00:00:00 2001 From: John Barham Date: Fri, 24 Nov 2023 18:36:09 +1100 Subject: [PATCH 3/3] Update MIGRATION_GUIDE.md (#363) Use correct path for v4 migration --- MIGRATION_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 93f96495..ff9c57e1 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -179,7 +179,7 @@ be a drop-in replacement, if you're having troubles migrating, please open an issue. You can replace all occurrences of `github.com/dgrijalva/jwt-go` or -`github.com/golang-jwt/jwt` with `github.com/golang-jwt/jwt/v5`, either manually +`github.com/golang-jwt/jwt` with `github.com/golang-jwt/jwt/v4`, either manually or by using tools such as `sed` or `gofmt`. And then you'd typically run: