Repository: lestrrat-go/jwx
Pull request #704 (2022-04-18 22:16:33)

Update v2 branch


Patch:
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 95de7989..b2414fcd 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        go: [ '1.17.x', '1.16.x' ]
+        go: [ '1.18', '1.17', '1.16' ]
     name: "Test [ Go ${{ matrix.go }} / JSON Backend ${{ matrix.json_backend }} ]"
     steps:
       - name: Checkout repository
@@ -24,9 +24,10 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-go-
       - name: Install Go stable version
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: ${{ matrix.go }}
+          check-latest: true
       - name: Install benchstat
         run: |
           go install golang.org/x/perf/cmd/benchstat@latest
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 485f4886..dd3addb8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
     strategy:
       matrix:
         go_tags: [ 'stdlib', 'goccy', 'es256k', 'all']
-        go: [ '1.18.x', '1.17.x' ]
+        go: [ '1.18', '1.17', '1.16' ]
     name: "Test [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]"
     steps:
       - name: Checkout repository
@@ -27,9 +27,10 @@ jobs:
             ${{ runner.os }}-go-
       - name: Install Go stable version
         if: matrix.go != 'tip'
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: ${{ matrix.go }}
+          check-latest: true
       - name: Install Go tip
         if: matrix.go == 'tip'
         run: |
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 226e337f..9d7de75a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -5,7 +5,11 @@ jobs:
     name: lint
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-go@v3
+        with:
+          go-version: 1.18
+          check-latest: true
       - uses: golangci/golangci-lint-action@v2
         with:
           version: v1.45.2
diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml
index 1dfd1030..4a6008a3 100644
--- a/.github/workflows/smoke.yml
+++ b/.github/workflows/smoke.yml
@@ -14,7 +14,7 @@ jobs:
     strategy:
       matrix:
         go_tags: [ 'stdlib', 'goccy', 'es256k', 'all' ]
-        go: [ '1.17.x', '1.16.x' ]
+        go: [ '1.18', '1.17', '1.16' ]
     name: "Smoke [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]"
     steps:
       - name: Checkout repository
@@ -32,9 +32,10 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-go-
       - name: Install Go stable version
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: ${{ matrix.go }}
+          check-latest: true
       - name: Install stringer
         run: go install golang.org/x/tools/cmd/stringer@latest
       - name: Install jose
diff --git a/Changes-v2.md b/Changes-v2.md
index 864ae4ce..35fd287b 100644
--- a/Changes-v2.md
+++ b/Changes-v2.md
@@ -90,7 +90,7 @@ jwe.Verify(signed, jwe.WithKeySet(jwks), jwe.WithKeyUsed(&keyUsed))
 
 ## Module
 
-* Module now requires go 1.17
+* Module now requires go 1.16
 
 * Use of github.com/pkg/errors is no more. If you were relying on bevaior
   that depends on the errors being an instance of github.com/pkg/errors
@@ -306,6 +306,9 @@ jws.Parse(serialized,
   The rest of the arguments are treated as options passed to the
   `(jwk.Fetcher).Fetch()` function.
 
+* Remove `jws.WithPayloadSigner()`. This should be completely repleceable
+  using `jws.WithKey()`
+
 * jws.WithKeyProvider() has been added to specify arbitrary
   code to specify which keys to try.
 
@@ -341,6 +344,10 @@ jws.Parse(serialized,
 * `jwt.WithVerify()` has been renamed to `jwt.WithKey()`. The option can
   be used for signing, encryption, and parsing.
 
+* `jwt.Validator` has been changed to return `jwt.ValidationError`.
+  If you provide a custom validator, you should wrap the error with
+  `jwt.NewValidationError()`
+
 * `jwt.UseDefault()` has been removed. You should use `jws.WithUseDefault()`
   as a suboption in the `jwt.WithKeySet()` option.
 
@@ -369,11 +376,6 @@ case ...:
 }
 ```
 
-* Remove `jws.WithPayloadSigner()`. This should be completely repleceable
-  using `jws.WithKey()`
-
-* `jwt.Sign()` has been changed so that it works more like the new `jws.Sign()`
-
 * `jwt.WithHeaders` and `jwt.WithJwsHeaders` have been removed.
   You should be able to use the new `jwt.WithKey` option to pass headers
 
@@ -383,3 +385,6 @@ case ...:
 
 * `jwt.ReadFile` now supports the option `jwt.WithFS` which allows you to
   read data from arbitrary `fs.FS` objects
+
+* `jwt.Sign()` has been changed so that it works more like the new `jws.Sign()`
+
diff --git a/docs/01-jwt.md b/docs/01-jwt.md
index 9bde25f9..2efec2c0 100644
--- a/docs/01-jwt.md
+++ b/docs/01-jwt.md
@@ -767,7 +767,7 @@ import (
 )
 
 func ExampleJWT_ValidateValidator() {
-  validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) error {
+  validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) jwt.ValidationError {
     if t.IssuedAt().Month() != 8 {
       return jwt.NewValidationError(errors.New(`tokens are only valid if issued during August!`))
     }
diff --git a/go.mod b/go.mod
index 0cc6a209..cc0a3b7e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/lestrrat-go/jwx/v2
 
-go 1.15
+go 1.16
 
 require (
 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
@@ -12,3 +12,10 @@ require (
 	github.com/stretchr/testify v1.7.1
 	golang.org/x/crypto v0.0.0-20220214200702-86341886e292
 )
+
+require (
+	github.com/davecgh/go-spew v1.1.0 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
+)
diff --git a/jwk/interface.go b/jwk/interface.go
index a8a3763f..31b2f07e 100644
--- a/jwk/interface.go
+++ b/jwk/interface.go
@@ -54,9 +54,8 @@ type Set interface {
 	AddKey(Key) error
 
 	// Clear resets the list of keys associated with this set, emptying the
-	// internal list of `jwk.Key`s
-	// This method will be changed in the future to clear all contents in the
-	// `jwk.Set` instead of just the keys.
+	// internal list of `jwk.Key`s, as well as clearing any other non-key
+	// fields
 	Clear() error
 
 	// Get returns the key at index `idx`. If the index is out of range,
diff --git a/jwt/validate.go b/jwt/validate.go
index 750b165c..d8148d5e 100644
--- a/jwt/validate.go
+++ b/jwt/validate.go
@@ -119,7 +119,7 @@ func MinDeltaIs(c1, c2 string, dur time.Duration) Validator {
 	}
 }
 
-func (iitr *isInTimeRange) Validate(ctx context.Context, t Token) error {
+func (iitr *isInTimeRange) Validate(ctx context.Context, t Token) ValidationError {
 	clock := ValidationCtxClock(ctx) // MUST be populated
 	skew := ValidationCtxSkew(ctx)   // MUST be populated
 	// We don't check if the claims already exist, because we already did that
@@ -142,6 +142,7 @@ func (iitr *isInTimeRange) Validate(ctx context.Context, t Token) error {
 type ValidationError interface {
 	error
 	isValidationError()
+	Unwrap() error
 }
 
 func NewValidationError(err error) ValidationError {
@@ -154,6 +155,9 @@ type validationError struct {
 }
 
 func (validationError) isValidationError() {}
+func (err *validationError) Unwrap() error {
+	return err.error
+}
 
 var errTokenExpired = NewValidationError(fmt.Errorf(`"exp" not satisfied`))
 var errInvalidIssuedAt = NewValidationError(fmt.Errorf(`"iat" not satisfied`))
@@ -161,34 +165,31 @@ var errTokenNotYetValid = NewValidationError(fmt.Errorf(`"nbf" not satisfied`))
 
 // ErrTokenExpired returns the immutable error used when `exp` claim
 // is not satisfied
-func ErrTokenExpired() error {
+func ErrTokenExpired() ValidationError {
 	return errTokenExpired
 }
 
 // ErrInvalidIssuedAt returns the immutable error used when `iat` claim
 // is not satisfied
-func ErrInvalidIssuedAt() error {
+func ErrInvalidIssuedAt() ValidationError {
 	return errInvalidIssuedAt
 }
 
-func ErrTokenNotYetValid() error {
+func ErrTokenNotYetValid() ValidationError {
 	return errTokenNotYetValid
 }
 
 // Validator describes interface to validate a Token.
 type Validator interface {
 	// Validate should return an error if a required conditions is not met.
-	// This method will be changed in the next major release to return
-	// jwt.ValidationError instead of error to force users to return
-	// a validation error even for user-specified validators
-	Validate(context.Context, Token) error
+	Validate(context.Context, Token) ValidationError
 }
 
 // ValidatorFunc is a type of Validator that does not have any
 // state, that is implemented as a function
-type ValidatorFunc func(context.Context, Token) error
+type ValidatorFunc func(context.Context, Token) ValidationError
 
-func (vf ValidatorFunc) Validate(ctx context.Context, tok Token) error {
+func (vf ValidatorFunc) Validate(ctx context.Context, tok Token) ValidationError {
 	return vf(ctx, tok)
 }
 
@@ -227,7 +228,7 @@ func IsExpirationValid() Validator {
 	return ValidatorFunc(isExpirationValid)
 }
 
-func isExpirationValid(ctx context.Context, t Token) error {
+func isExpirationValid(ctx context.Context, t Token) ValidationError {
 	if tv := t.Expiration(); !tv.IsZero() && tv.Unix() != 0 {
 		clock := ValidationCtxClock(ctx) // MUST be populated
 		now := clock.Now().Truncate(time.Second)
@@ -251,7 +252,7 @@ func IsIssuedAtValid() Validator {
 	return ValidatorFunc(isIssuedAtValid)
 }
 
-func isIssuedAtValid(ctx context.Context, t Token) error {
+func isIssuedAtValid(ctx context.Context, t Token) ValidationError {
 	if tv := t.IssuedAt(); !tv.IsZero() && tv.Unix() != 0 {
 		clock := ValidationCtxClock(ctx) // MUST be populated
 		now := clock.Now().Truncate(time.Second)
@@ -275,7 +276,7 @@ func IsNbfValid() Validator {
 	return ValidatorFunc(isNbfValid)
 }
 
-func isNbfValid(ctx context.Context, t Token) error {
+func isNbfValid(ctx context.Context, t Token) ValidationError {
 	if tv := t.NotBefore(); !tv.IsZero() && tv.Unix() != 0 {
 		clock := ValidationCtxClock(ctx) // MUST be populated
 		now := clock.Now().Truncate(time.Second)
@@ -319,7 +320,7 @@ func IsValidationError(err error) bool {
 	}
 }
 
-func (ccs claimContainsString) Validate(_ context.Context, t Token) error {
+func (ccs claimContainsString) Validate(_ context.Context, t Token) ValidationError {
 	v, ok := t.Get(ccs.name)
 	if !ok {
 		return NewValidationError(fmt.Errorf(`claim %q not found`, ccs.name))
@@ -356,7 +357,7 @@ func ClaimValueIs(name string, value interface{}) Validator {
 	return &claimValueIs{name: name, value: value}
 }
 
-func (cv *claimValueIs) Validate(_ context.Context, t Token) error {
+func (cv *claimValueIs) Validate(_ context.Context, t Token) ValidationError {
 	v, ok := t.Get(cv.name)
 	if !ok {
 		return NewValidationError(fmt.Errorf(`%q not satisfied: claim %q does not exist`, cv.name, cv.name))
@@ -375,7 +376,7 @@ func IsRequired(name string) Validator {
 
 type isRequired string
 
-func (ir isRequired) Validate(_ context.Context, t Token) error {
+func (ir isRequired) Validate(_ context.Context, t Token) ValidationError {
 	_, ok := t.Get(string(ir))
 	if !ok {
 		return NewValidationError(fmt.Errorf(`%q not satisfied: required claim not found`, string(ir)))


Test patch:
diff --git a/examples/jwt_validate_validator_example_test.go b/examples/jwt_validate_validator_example_test.go
index d1124706..9813ee07 100644
--- a/examples/jwt_validate_validator_example_test.go
+++ b/examples/jwt_validate_validator_example_test.go
@@ -10,7 +10,7 @@ import (
 )
 
 func ExampleJWT_ValidateValidator() {
-	validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) error {
+	validator := jwt.ValidatorFunc(func(_ context.Context, t jwt.Token) jwt.ValidationError {
 		if t.IssuedAt().Month() != 8 {
 			return jwt.NewValidationError(errors.New(`tokens are only valid if issued during August!`))
 		}
diff --git a/jwt/validate_test.go b/jwt/validate_test.go
index c64ae851..6c5873ee 100644
--- a/jwt/validate_test.go
+++ b/jwt/validate_test.go
@@ -397,10 +397,10 @@ func TestClaimValidator(t *testing.T) {
 	t.Parallel()
 	const myClaim = "my-claim"
 	err0 := errors.New(myClaim + " does not exist")
-	v := jwt.ValidatorFunc(func(_ context.Context, tok jwt.Token) error {
+	v := jwt.ValidatorFunc(func(_ context.Context, tok jwt.Token) jwt.ValidationError {
 		_, ok := tok.Get(myClaim)
 		if !ok {
-			return err0
+			return jwt.NewValidationError(err0)
 		}
 		return nil
 	})