Repository: lestrrat-go/jwx
Pull request #574 (2022-02-16 01:32:53)

Add IgnoreParseError option

codecov-commenter: # [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/574?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) Report
> Merging [#574](https://codecov.io/gh/lestrrat-go/jwx/pull/574?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (2c74f56) into [main](https://codecov.io/gh/lestrrat-go/jwx/commit/ceca5325c05791d6c97d8fd6080d5a77785a9fec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (ceca532) will **increase** coverage by `0.07%`.
> The diff coverage is `88.57%`.

[![Impacted file tree graph](https://codecov.io/gh/lestrrat-go/jwx/pull/574/graphs/tree.svg?width=650&height=150&src=pr&token=kU2RSDosPt&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)](https://codecov.io/gh/lestrrat-go/jwx/pull/574?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)

```diff
@@            Coverage Diff             @@
##             main     #574      +/-   ##
==========================================
+ Coverage   72.65%   72.73%   +0.07%     
==========================================
  Files          84       80       -4     
  Lines        9561     9560       -1     
==========================================
+ Hits         6947     6953       +6     
+ Misses       1794     1789       -5     
+ Partials      820      818       -2     
```


| [Impacted Files](https://codecov.io/gh/lestrrat-go/jwx/pull/574?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | Coverage Δ | |
|---|---|---|
| [jwk/ecdsa\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL2VjZHNhX2dlbi5nbw==) | `72.16% <ø> (ø)` | |
| [jwk/okp\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL29rcF9nZW4uZ28=) | `70.57% <ø> (ø)` | |
| [jwk/rsa\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL3JzYV9nZW4uZ28=) | `71.62% <ø> (+0.25%)` | :arrow_up: |
| [jwk/symmetric\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL3N5bW1ldHJpY19nZW4uZ28=) | `78.16% <ø> (ø)` | |
| [jwk/option.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL29wdGlvbi5nbw==) | `68.18% <25.00%> (-1.82%)` | :arrow_down: |
| [jwk/refresh.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL3JlZnJlc2guZ28=) | `81.18% <93.33%> (+2.05%)` | :arrow_up: |
| [jwk/jwk.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL2p3ay5nbw==) | `61.18% <100.00%> (+0.85%)` | :arrow_up: |
| [jwk/set.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL3NldC5nbw==) | `86.27% <100.00%> (+1.71%)` | :arrow_up: |
| [internal/ecutil/ecutil.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvZWN1dGlsL2VjdXRpbC5nbw==) | `76.92% <0.00%> (-5.13%)` | :arrow_down: |
| [jws/es256k.go](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andzL2VzMjU2ay5nbw==) | | |
| ... and [6 more](https://codecov.io/gh/lestrrat-go/jwx/pull/574/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/574?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/574?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Last update [ceca532...2c74f56](https://codecov.io/gh/lestrrat-go/jwx/pull/574?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).


Patch:
diff --git a/Changes b/Changes
index b11493a5..4b38209f 100644
--- a/Changes
+++ b/Changes
@@ -1,6 +1,17 @@
 Changes
 =======
 
+v1.2.19 UNRELEASED
+[New Feature]
+  * [jwk] jwk.Parse (and (jwk.AutoRefresh).Configure) can accept a new
+    option `jwk.WithIgnoreParseError(bool)`, which allows users to ignore
+    errors during parsing of each key contained in the JWKS, allowing
+    you to "skip" invalid keys.
+
+    This option should not be used lightly, as it hides the presence of
+    possibly faulty keys. However, this can be an escape hatch if you are
+    faced with a faulty JWKS that you do not control.
+
 v1.2.18 23 Jan 2022
 [Bug fixes]
   * [jwe] When presented with jwk.Key with a key ID, the jwe encryption
diff --git a/jwk/ecdsa_gen.go b/jwk/ecdsa_gen.go
index 6e5524b7..e5d6374b 100644
--- a/jwk/ecdsa_gen.go
+++ b/jwk/ecdsa_gen.go
@@ -49,7 +49,7 @@ type ecdsaPublicKey struct {
 	y                      []byte
 	privateParams          map[string]interface{}
 	mu                     *sync.RWMutex
-	dc                     DecodeCtx
+	dc                     json.DecodeCtx
 }
 
 func NewECDSAPublicKey() ECDSAPublicKey {
@@ -391,13 +391,13 @@ func (k *ecdsaPublicKey) Clone() (Key, error) {
 	return cloneKey(k)
 }
 
-func (k *ecdsaPublicKey) DecodeCtx() DecodeCtx {
+func (k *ecdsaPublicKey) DecodeCtx() json.DecodeCtx {
 	k.mu.RLock()
 	defer k.mu.RUnlock()
 	return k.dc
 }
 
-func (k *ecdsaPublicKey) SetDecodeCtx(dc DecodeCtx) {
+func (k *ecdsaPublicKey) SetDecodeCtx(dc json.DecodeCtx) {
 	k.mu.Lock()
 	defer k.mu.Unlock()
 	k.dc = dc
@@ -611,7 +611,7 @@ type ecdsaPrivateKey struct {
 	y                      []byte
 	privateParams          map[string]interface{}
 	mu                     *sync.RWMutex
-	dc                     DecodeCtx
+	dc                     json.DecodeCtx
 }
 
 func NewECDSAPrivateKey() ECDSAPrivateKey {
@@ -973,13 +973,13 @@ func (k *ecdsaPrivateKey) Clone() (Key, error) {
 	return cloneKey(k)
 }
 
-func (k *ecdsaPrivateKey) DecodeCtx() DecodeCtx {
+func (k *ecdsaPrivateKey) DecodeCtx() json.DecodeCtx {
 	k.mu.RLock()
 	defer k.mu.RUnlock()
 	return k.dc
 }
 
-func (k *ecdsaPrivateKey) SetDecodeCtx(dc DecodeCtx) {
+func (k *ecdsaPrivateKey) SetDecodeCtx(dc json.DecodeCtx) {
 	k.mu.Lock()
 	defer k.mu.Unlock()
 	k.dc = dc
diff --git a/jwk/interface.go b/jwk/interface.go
index f2c6cd12..9182f712 100644
--- a/jwk/interface.go
+++ b/jwk/interface.go
@@ -138,8 +138,14 @@ type HTTPClient interface {
 	Do(*http.Request) (*http.Response, error)
 }
 
-type DecodeCtx = json.DecodeCtx
-type KeyWithDecodeCtx = json.DecodeCtxContainer
+type DecodeCtx interface {
+	json.DecodeCtx
+	IgnoreParseError() bool
+}
+type KeyWithDecodeCtx interface {
+	SetDecodeCtx(DecodeCtx)
+	DecodeCtx() DecodeCtx
+}
 
 type AutoRefreshError struct {
 	Error error
diff --git a/jwk/internal/cmd/genheader/main.go b/jwk/internal/cmd/genheader/main.go
index ebc7e099..2b71adf1 100644
--- a/jwk/internal/cmd/genheader/main.go
+++ b/jwk/internal/cmd/genheader/main.go
@@ -284,7 +284,7 @@ func generateObject(o *codegen.Output, kt *KeyType, obj *codegen.Object) error {
 	}
 	o.L("privateParams map[string]interface{}")
 	o.L("mu *sync.RWMutex")
-	o.L("dc DecodeCtx")
+	o.L("dc json.DecodeCtx")
 	o.L("}")
 
 	o.LL("func New%[1]s() %[1]s {", ifName)
@@ -492,13 +492,13 @@ func generateObject(o *codegen.Output, kt *KeyType, obj *codegen.Object) error {
 	o.L("return cloneKey(k)")
 	o.L("}")
 
-	o.LL("func (k *%s) DecodeCtx() DecodeCtx {", structName)
+	o.LL("func (k *%s) DecodeCtx() json.DecodeCtx {", structName)
 	o.L("k.mu.RLock()")
 	o.L("defer k.mu.RUnlock()")
 	o.L("return k.dc")
 	o.L("}")
 
-	o.LL("func (k *%s) SetDecodeCtx(dc DecodeCtx) {", structName)
+	o.LL("func (k *%s) SetDecodeCtx(dc json.DecodeCtx) {", structName)
 	o.L("k.mu.Lock()")
 	o.L("defer k.mu.Unlock()")
 	o.L("k.dc = dc")
diff --git a/jwk/jwk.go b/jwk/jwk.go
index 522b400d..84e3766c 100644
--- a/jwk/jwk.go
+++ b/jwk/jwk.go
@@ -380,6 +380,15 @@ func parsePEMEncodedRawKey(src []byte) (interface{}, []byte, error) {
 	}
 }
 
+type setDecodeCtx struct {
+	json.DecodeCtx
+	ignoreParseError bool
+}
+
+func (ctx *setDecodeCtx) IgnoreParseError() bool {
+	return ctx.ignoreParseError
+}
+
 // ParseKey parses a single key JWK. Unlike `jwk.Parse` this method will
 // report failure if you attempt to pass a JWK set. Only use this function
 // when you know that the data is a single JWK.
@@ -410,6 +419,8 @@ func ParseKey(data []byte, options ...ParseOption) (Key, error) {
 				localReg = json.NewRegistry()
 			}
 			localReg.Register(pair.Name, pair.Value)
+		case identIgnoreParseError{}:
+			return nil, errors.Errorf(`jwk.WithIgnoreParseError() cannot be used for ParseKey()`)
 		}
 	}
 
@@ -457,7 +468,7 @@ func ParseKey(data []byte, options ...ParseOption) (Key, error) {
 	}
 
 	if localReg != nil {
-		dcKey, ok := key.(KeyWithDecodeCtx)
+		dcKey, ok := key.(json.DecodeCtxContainer)
 		if !ok {
 			return nil, errors.Errorf(`typed field was requested, but the key (%T) does not support DecodeCtx`, key)
 		}
@@ -490,11 +501,14 @@ func ParseKey(data []byte, options ...ParseOption) (Key, error) {
 func Parse(src []byte, options ...ParseOption) (Set, error) {
 	var parsePEM bool
 	var localReg *json.Registry
+	var ignoreParseError bool
 	for _, option := range options {
 		//nolint:forcetypeassert
 		switch option.Ident() {
 		case identPEM{}:
 			parsePEM = option.Value().(bool)
+		case identIgnoreParseError{}:
+			ignoreParseError = option.Value().(bool)
 		case identTypedField{}:
 			pair := option.Value().(typedFieldPair)
 			if localReg == nil {
@@ -523,12 +537,15 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
 		return s, nil
 	}
 
-	if localReg != nil {
+	if localReg != nil || ignoreParseError {
 		dcKs, ok := s.(KeyWithDecodeCtx)
 		if !ok {
 			return nil, errors.Errorf(`typed field was requested, but the key set (%T) does not support DecodeCtx`, s)
 		}
-		dc := json.NewDecodeCtx(localReg)
+		dc := &setDecodeCtx{
+			DecodeCtx:        json.NewDecodeCtx(localReg),
+			ignoreParseError: ignoreParseError,
+		}
 		dcKs.SetDecodeCtx(dc)
 		defer func() { dcKs.SetDecodeCtx(nil) }()
 	}
diff --git a/jwk/okp_gen.go b/jwk/okp_gen.go
index 65acf610..022c285f 100644
--- a/jwk/okp_gen.go
+++ b/jwk/okp_gen.go
@@ -45,7 +45,7 @@ type okpPublicKey struct {
 	x509URL                *string           // https://tools.ietf.org/html/rfc7515#section-4.1.5
 	privateParams          map[string]interface{}
 	mu                     *sync.RWMutex
-	dc                     DecodeCtx
+	dc                     json.DecodeCtx
 }
 
 func NewOKPPublicKey() OKPPublicKey {
@@ -367,13 +367,13 @@ func (k *okpPublicKey) Clone() (Key, error) {
 	return cloneKey(k)
 }
 
-func (k *okpPublicKey) DecodeCtx() DecodeCtx {
+func (k *okpPublicKey) DecodeCtx() json.DecodeCtx {
 	k.mu.RLock()
 	defer k.mu.RUnlock()
 	return k.dc
 }
 
-func (k *okpPublicKey) SetDecodeCtx(dc DecodeCtx) {
+func (k *okpPublicKey) SetDecodeCtx(dc json.DecodeCtx) {
 	k.mu.Lock()
 	defer k.mu.Unlock()
 	k.dc = dc
@@ -577,7 +577,7 @@ type okpPrivateKey struct {
 	x509URL                *string           // https://tools.ietf.org/html/rfc7515#section-4.1.5
 	privateParams          map[string]interface{}
 	mu                     *sync.RWMutex
-	dc                     DecodeCtx
+	dc                     json.DecodeCtx
 }
 
 func NewOKPPrivateKey() OKPPrivateKey {
@@ -919,13 +919,13 @@ func (k *okpPrivateKey) Clone() (Key, error) {
 	return cloneKey(k)
 }
 
-func (k *okpPrivateKey) DecodeCtx() DecodeCtx {
+func (k *okpPrivateKey) DecodeCtx() json.DecodeCtx {
 	k.mu.RLock()
 	defer k.mu.RUnlock()
 	return k.dc
 }
 
-func (k *okpPrivateKey) SetDecodeCtx(dc DecodeCtx) {
+func (k *okpPrivateKey) SetDecodeCtx(dc json.DecodeCtx) {
 	k.mu.Lock()
 	defer k.mu.Unlock()
 	k.dc = dc
diff --git a/jwk/option.go b/jwk/option.go
index b7111642..47bf493c 100644
--- a/jwk/option.go
+++ b/jwk/option.go
@@ -20,6 +20,7 @@ type identPEM struct{}
 type identTypedField struct{}
 type identLocalRegistry struct{}
 type identFetchWhitelist struct{}
+type identIgnoreParseError struct{}
 
 // AutoRefreshOption is a type of Option that can be passed to the
 // AutoRefresh object.
@@ -35,8 +36,8 @@ type autoRefreshOption struct {
 func (*autoRefreshOption) autoRefreshOption() {}
 
 // FetchOption is a type of Option that can be passed to `jwk.Fetch()`
-// This type also implements the `AutoRefreshOption`, and thus can be
-// safely passed to `(*jwk.AutoRefresh).Configure()`
+// FetchOption also implements the `AutoRefreshOption`, and thus can
+// safely be passed to `(*jwk.AutoRefresh).Configure()`
 type FetchOption interface {
 	AutoRefreshOption
 	fetchOption()
@@ -50,8 +51,11 @@ func (*fetchOption) autoRefreshOption() {}
 func (*fetchOption) fetchOption()       {}
 
 // ParseOption is a type of Option that can be passed to `jwk.Parse()`
+// ParseOption also implmentsthe `ReadFileOPtion` and `AutoRefreshOption`,
+// and thus safely be passed to `jwk.ReadFile` and `(*jwk.AutoRefresh).Configure()`
 type ParseOption interface {
 	ReadFileOption
+	AutoRefreshOption
 	parseOption()
 }
 
@@ -59,8 +63,9 @@ type parseOption struct {
 	Option
 }
 
-func (*parseOption) parseOption()    {}
-func (*parseOption) readFileOption() {}
+func (*parseOption) autoRefreshOption() {}
+func (*parseOption) parseOption()       {}
+func (*parseOption) readFileOption()    {}
 
 // WithHTTPClient allows users to specify the "net/http".Client object that
 // is used when fetching jwk.Set objects.
@@ -167,3 +172,26 @@ func withLocalRegistry(r *json.Registry) ParseOption {
 func WithFetchWhitelist(w Whitelist) FetchOption {
 	return &fetchOption{option.New(identFetchWhitelist{}, w)}
 }
+
+// WithIgnoreParseError is only applicable when used with `jwk.Parse()`
+// (i.e. to parse JWK sets). If passed to `jwk.ParseKey()`, the function
+// will return an error no matter what the input is.
+//
+// DO NOT USE WITHOUT EXHAUSTING ALL OTHER ROUTES FIRST.
+//
+// The option specifies that errors found during parsing of individual
+// keys are ignored. For example, if you had keys A, B, C where B is
+// invalid (e.g. it does not contain the required fields), then the
+// resulting JWKS will contain keys A and C only.
+//
+// This options exists as an escape hatch for those times when a
+// key in a JWKS that is irrelevant for your use case is causing
+// your JWKS parsing to fail, and you want to get to the rest of the
+// keys in the JWKS.
+//
+// Again, DO NOT USE unless you have exhausted all other routes.
+// When you use this option, you will not be able to tell if you are
+// using a faulty JWKS, except for when there are JSON syntax errors.
+func WithIgnoreParseError(b bool) ParseOption {
+	return &parseOption{option.New(identIgnoreParseError{}, b)}
+}
diff --git a/jwk/refresh.go b/jwk/refresh.go
index d79c49ba..05126325 100644
--- a/jwk/refresh.go
+++ b/jwk/refresh.go
@@ -87,7 +87,8 @@ type target struct {
 	lastRefresh time.Time
 	nextRefresh time.Time
 
-	wl Whitelist
+	wl           Whitelist
+	parseOptions []ParseOption
 }
 
 type resetTimerReq struct {
@@ -168,9 +169,15 @@ func (af *AutoRefresh) Configure(url string, options ...AutoRefreshOption) {
 	var hasRefreshInterval bool
 	var refreshInterval time.Duration
 	var wl Whitelist
+	var parseOptions []ParseOption
 	minRefreshInterval := time.Hour
 	bo := backoff.Null()
 	for _, option := range options {
+		if v, ok := option.(ParseOption); ok {
+			parseOptions = append(parseOptions, v)
+			continue
+		}
+
 		//nolint:forcetypeassert
 		switch option.Ident() {
 		case identFetchBackoff{}:
@@ -187,39 +194,34 @@ func (af *AutoRefresh) Configure(url string, options ...AutoRefreshOption) {
 		}
 	}
 
-	var doReconfigure bool
 	af.muRegistry.Lock()
 	t, ok := af.registry[url]
 	if ok {
 		if t.httpcl != httpcl {
 			t.httpcl = httpcl
-			doReconfigure = true
 		}
 
 		if t.minRefreshInterval != minRefreshInterval {
 			t.minRefreshInterval = minRefreshInterval
-			doReconfigure = true
 		}
 
 		if t.refreshInterval != nil {
 			if !hasRefreshInterval {
 				t.refreshInterval = nil
-				doReconfigure = true
 			} else if *t.refreshInterval != refreshInterval {
 				*t.refreshInterval = refreshInterval
-				doReconfigure = true
 			}
 		} else {
 			if hasRefreshInterval {
 				t.refreshInterval = &refreshInterval
-				doReconfigure = true
 			}
 		}
 
 		if t.wl != wl {
 			t.wl = wl
-			doReconfigure = true
 		}
+
+		t.parseOptions = parseOptions
 	} else {
 		t = &target{
 			backoff:            bo,
@@ -230,8 +232,9 @@ func (af *AutoRefresh) Configure(url string, options ...AutoRefreshOption) {
 			// This is a placeholder timer so we can call Reset() on it later
 			// Make it sufficiently in the future so that we don't have bogus
 			// events firing
-			timer: time.NewTimer(24 * time.Hour),
-			wl:    wl,
+			timer:        time.NewTimer(24 * time.Hour),
+			wl:           wl,
+			parseOptions: parseOptions,
 		}
 		if hasRefreshInterval {
 			t.refreshInterval = &refreshInterval
@@ -239,14 +242,11 @@ func (af *AutoRefresh) Configure(url string, options ...AutoRefreshOption) {
 
 		// Record this in the registry
 		af.registry[url] = t
-		doReconfigure = true
 	}
 	af.muRegistry.Unlock()
 
-	if doReconfigure {
-		// Tell the backend to reconfigure itself
-		af.configureCh <- struct{}{}
-	}
+	// Tell the backend to reconfigure itself
+	af.configureCh <- struct{}{}
 }
 
 func (af *AutoRefresh) releaseFetching(url string) {
@@ -497,16 +497,16 @@ func (af *AutoRefresh) doRefreshRequest(ctx context.Context, url string, enableB
 
 	// In case the refresh fails due to errors in fetching/parsing the JWKS,
 	// we want to retry. Create a backoff object,
-
-	options := []FetchOption{WithHTTPClient(t.httpcl)}
+	parseOptions := t.parseOptions
+	fetchOptions := []FetchOption{WithHTTPClient(t.httpcl)}
 	if enableBackoff {
-		options = append(options, WithFetchBackoff(t.backoff))
+		fetchOptions = append(fetchOptions, WithFetchBackoff(t.backoff))
 	}
 	if t.wl != nil {
-		options = append(options, WithFetchWhitelist(t.wl))
+		fetchOptions = append(fetchOptions, WithFetchWhitelist(t.wl))
 	}
 
-	res, err := fetch(ctx, url, options...)
+	res, err := fetch(ctx, url, fetchOptions...)
 	if err == nil {
 		if res.StatusCode != http.StatusOK {
 			// now, can there be a remote resource that responds with a status code
@@ -514,7 +514,7 @@ func (af *AutoRefresh) doRefreshRequest(ctx context.Context, url string, enableB
 			err = errors.Errorf(`bad response status code (%d)`, res.StatusCode)
 		} else {
 			defer res.Body.Close()
-			keyset, parseErr := ParseReader(res.Body)
+			keyset, parseErr := ParseReader(res.Body, parseOptions...)
 			if parseErr == nil {
 				// Got a new key set. replace the keyset in the target
 				af.muCache.Lock()
diff --git a/jwk/rsa_gen.go b/jwk/rsa_gen.go
index bea4602e..21de3bf1 100644
--- a/jwk/rsa_gen.go
+++ b/jwk/rsa_gen.go
@@ -51,7 +51,7 @@ type rsaPublicKey struct {
 	x509URL                *string           // https://tools.ietf.org/html/rfc7515#section-4.1.5
 	privateParams          map[string]interface{}
 	mu                     *sync.RWMutex
-	dc                     DecodeCtx
+	dc                     json.DecodeCtx
 }
 
 func NewRSAPublicKey() RSAPublicKey {
@@ -370,13 +370,13 @@ func (k *rsaPublicKey) Clone() (Key, error) {
 	return cloneKey(k)
 }
 
-func (k *rsaPublicKey) DecodeCtx() DecodeCtx {
+func (k *rsaPublicKey) DecodeCtx() json.DecodeCtx {
 	k.mu.RLock()
 	defer k.mu.RUnlock()
 	return k.dc
 }
 
-func (k *rsaPublicKey) SetDecodeCtx(dc DecodeCtx) {
+func (k *rsaPublicKey) SetDecodeCtx(dc json.DecodeCtx) {
 	k.mu.Lock()
 	defer k.mu.Unlock()
 	k.dc = dc
@@ -588,7 +588,7 @@ type rsaPrivateKey struct {
 	x509URL                *string           // https://tools.ietf.org/html/rfc7515#section-4.1.5
 	privateParams          map[string]interface{}
 	mu                     *sync.RWMutex
-	dc                     DecodeCtx
+	dc                     json.DecodeCtx
 }
 
 func NewRSAPrivateKey() RSAPrivateKey {
@@ -1027,13 +1027,13 @@ func (k *rsaPrivateKey) Clone() (Key, error) {
 	return cloneKey(k)
 }
 
-func (k *rsaPrivateKey) DecodeCtx() DecodeCtx {
+func (k *rsaPrivateKey) DecodeCtx() json.DecodeCtx {
 	k.mu.RLock()
 	defer k.mu.RUnlock()
 	return k.dc
 }
 
-func (k *rsaPrivateKey) SetDecodeCtx(dc DecodeCtx) {
+func (k *rsaPrivateKey) SetDecodeCtx(dc json.DecodeCtx) {
 	k.mu.Lock()
 	defer k.mu.Unlock()
 	k.dc = dc
diff --git a/jwk/set.go b/jwk/set.go
index 127fa421..d5e844af 100644
--- a/jwk/set.go
+++ b/jwk/set.go
@@ -189,10 +189,12 @@ func (s *set) UnmarshalJSON(data []byte) error {
 	s.keys = nil
 
 	var options []ParseOption
+	var ignoreParseError bool
 	if dc := s.dc; dc != nil {
 		if localReg := dc.Registry(); localReg != nil {
 			options = append(options, withLocalRegistry(localReg))
 		}
+		ignoreParseError = dc.IgnoreParseError()
 	}
 
 	var sawKeysField bool
@@ -225,7 +227,10 @@ LOOP:
 				for i, keysrc := range list {
 					key, err := ParseKey(keysrc, options...)
 					if err != nil {
-						return errors.Wrapf(err, `failed to code key #%d in "keys"`, i)
+						if !ignoreParseError {
+							return errors.Wrapf(err, `failed to decode key #%d in "keys"`, i)
+						}
+						continue
 					}
 					s.keys = append(s.keys, key)
 				}
diff --git a/jwk/symmetric_gen.go b/jwk/symmetric_gen.go
index 9369f30f..60b0fd97 100644
--- a/jwk/symmetric_gen.go
+++ b/jwk/symmetric_gen.go
@@ -41,7 +41,7 @@ type symmetricKey struct {
 	x509URL                *string           // https://tools.ietf.org/html/rfc7515#section-4.1.5
 	privateParams          map[string]interface{}
 	mu                     *sync.RWMutex
-	dc                     DecodeCtx
+	dc                     json.DecodeCtx
 }
 
 func NewSymmetricKey() SymmetricKey {
@@ -340,13 +340,13 @@ func (k *symmetricKey) Clone() (Key, error) {
 	return cloneKey(k)
 }
 
-func (k *symmetricKey) DecodeCtx() DecodeCtx {
+func (k *symmetricKey) DecodeCtx() json.DecodeCtx {
 	k.mu.RLock()
 	defer k.mu.RUnlock()
 	return k.dc
 }
 
-func (k *symmetricKey) SetDecodeCtx(dc DecodeCtx) {
+func (k *symmetricKey) SetDecodeCtx(dc json.DecodeCtx) {
 	k.mu.Lock()
 	defer k.mu.Unlock()
 	k.dc = dc


Test patch:
diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go
index 1e0c4a92..df0e6886 100644
--- a/jwk/jwk_test.go
+++ b/jwk/jwk_test.go
@@ -8,6 +8,7 @@ import (
 	"crypto/ed25519"
 	"crypto/rsa"
 	"fmt"
+	"io"
 	"math/big"
 	"net/http"
 	"net/http/httptest"
@@ -1909,3 +1910,100 @@ func TestFetch(t *testing.T) {
 		})
 	}
 }
+
+func TestGH567(t *testing.T) {
+	const src = `{
+  "keys": [
+    {
+      "kty": "RSA",
+      "use": "sig",
+      "kid": "20595A4BE9F566771792BC3DBC7DF78FF9C36575",
+      "x5t": "20595A4BE9F566771792BC3DBC7DF78FF9C36575",
+      "e": "AQAB",
+      "n": "tAN2xCfMuGpZukiGJl_-aQi_HGd4voyEwuOyL79wZphgtAmMAeOEO9QgxSX00ZczonlOm_I1Xpv2RVnNzSiHfB0bTqn4bLt15JVCBhE1vXaRf63QXn5oZ38fxm_aNctfnmkf65sF3lSzcZmfp1934L1KxJObq4BEOpIxvj00gIOpZQ4Mqw1khfsLhIVeXh8xtiEJwQZPdwIUQD03Yt5XQ_QU3NhxmyXiG8c6auOstdZybbGw10uJQEN4PrW0ESvp_GMnLssYrq6x9PhyvJhZhMFX3rBsYhOI7ILMaqo-QeDYUo0lQ1ENoQFyvtWrNQ_6A-CbmJvL9HdN6AuMkujtUmZzEfbT-k3FRyaZL0JE4-yQikdPGHVK6Q2Ho_Zggx2OTNmLbEORBHNe8cbb7t_5fmK6Fk4TpW3795PR8dG-v-AUGpQEgipg5j-3ONxefyBVZyWyjXaxrhQk6nCeRKcXJ0dKiZQX6ykYrtkgwE3mlcuw9-WzUqvHVEMZgAqBhBhL",
+      "x5c": [
+        "MIIGpDCCBYygAwIBAgIEX5xBDDANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJESzESMBAGA1UECgwJVFJVU1QyNDA4MSYwJAYDVQQDDB1UUlVTVDI0MDggU3lzdGVtdGVzdCBYWFhJViBDQTAeFw0yMTA0MjYxMjI1NDBaFw0yNDA0MjYxMTM5NDBaMIGNMQswCQYDVQQGEwJESzEsMCoGA1UECgwjU0lHTkFUVVJHUlVQUEVOIEEvUyAvLyBDVlI6Mjk5MTU5MzgxUDAgBgNVBAUTGUNWUjoyOTkxNTkzOC1VSUQ6NTk5MTEyMjcwLAYDVQQDDCVTSUdOQVRVUkdSVVBQRU4gQS9TIC0gTkVCIFRyYW5zYWN0IFBQMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAtAN2xCfMuGpZukiGJl/\u002BaQi/HGd4voyEwuOyL79wZphgtAmMAeOEO9QgxSX00ZczonlOm/I1Xpv2RVnNzSiHfB0bTqn4bLt15JVCBhE1vXaRf63QXn5oZ38fxm/aNctfnmkf65sF3lSzcZmfp1934L1KxJObq4BEOpIxvj00gIOpZQ4Mqw1khfsLhIVeXh8xtiEJwQZPdwIUQD03Yt5XQ/QU3NhxmyXiG8c6auOstdZybbGw10uJQEN4PrW0ESvp/GMnLssYrq6x9PhyvJhZhMFX3rBsYhOI7ILMaqo\u002BQeDYUo0lQ1ENoQFyvtWrNQ/6A\u002BCbmJvL9HdN6AuMkujtUmZzEfbT\u002Bk3FRyaZL0JE4\u002ByQikdPGHVK6Q2Ho/Zggx2OTNmLbEORBHNe8cbb7t/5fmK6Fk4TpW3795PR8dG\u002Bv\u002BAUGpQEgipg5j\u002B3ONxefyBVZyWyjXaxrhQk6nCeRKcXJ0dKiZQX6ykYrtkgwE3mlcuw9\u002BWzUqvHVEMZgAqBhBhLAgMBAAGjggLNMIICyTAOBgNVHQ8BAf8EBAMCA7gwgZcGCCsGAQUFBwEBBIGKMIGHMDwGCCsGAQUFBzABhjBodHRwOi8vb2NzcC5zeXN0ZW10ZXN0MzQudHJ1c3QyNDA4LmNvbS9yZXNwb25kZXIwRwYIKwYBBQUHMAKGO2h0dHA6Ly92LmFpYS5zeXN0ZW10ZXN0MzQudHJ1c3QyNDA4LmNvbS9zeXN0ZW10ZXN0MzQtY2EuY2VyMIIBIAYDVR0gBIIBFzCCARMwggEPBg0rBgEEAYH0UQIEBgMFMIH9MC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LnRydXN0MjQwOC5jb20vcmVwb3NpdG9yeTCByQYIKwYBBQUHAgIwgbwwDBYFRGFuSUQwAwIBARqBq0RhbklEIHRlc3QgY2VydGlmaWthdGVyIGZyYSBkZW5uZSBDQSB1ZHN0ZWRlcyB1bmRlciBPSUQgMS4zLjYuMS40LjEuMzEzMTMuMi40LjYuMy41LiBEYW5JRCB0ZXN0IGNlcnRpZmljYXRlcyBmcm9tIHRoaXMgQ0EgYXJlIGlzc3VlZCB1bmRlciBPSUQgMS4zLjYuMS40LjEuMzEzMTMuMi40LjYuMy41LjCBrQYDVR0fBIGlMIGiMDygOqA4hjZodHRwOi8vY3JsLnN5c3RlbXRlc3QzNC50cnVzdDI0MDguY29tL3N5c3RlbXRlc3QzNC5jcmwwYqBgoF6kXDBaMQswCQYDVQQGEwJESzESMBAGA1UECgwJVFJVU1QyNDA4MSYwJAYDVQQDDB1UUlVTVDI0MDggU3lzdGVtdGVzdCBYWFhJViBDQTEPMA0GA1UEAwwGQ1JMMTUwMB8GA1UdIwQYMBaAFM1saJc5chmkNatk6vQRo4GH\u002BGk7MB0GA1UdDgQWBBSJJABtTjZRzzFHsb1JwED0qCo49TAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQA2yBdGVPbKoYUYRZj4bHLj5Xwqk/yh6sodxwAPYrwxkJBzUKFAwUCTCigpwq8NE00kp3xhFT7Hz/9Z7aZLV4N/D94tc0qDU0JIwlbkrn/i3wx8N8UXf242WVaunJLdKqGtV4ijqjZFGa68XBc3elGjnAMY8eoF56dGg35Ps8Cw2BZyG2aXyEQNs6JYUNzp57\u002B6lJWl0T9Zniaut7Aw2rCII8XRe9WY\u002BHIQX6GuBSw0Q4v9wAfyYftnoULsgfVklkxtQI4kAO5rG17Z5NJuvRkXJD8jp\u002B2jRzcaD8Ud\u002Bpe4keTvuJZ\u002BRj\u002BBDLn/3\u002ByPbs3arD3CIO\u002BlW3Nndr34Le/s"
+      ],
+      "alg": "RS256"
+    },
+    {
+      "kty": "RSA",
+      "use": "sig",
+      "kid": "048058BB59F4D3007045896FD488CE81F4EB4923",
+      "x5t": "048058BB59F4D3007045896FD488CE81F4EB4923",
+      "e": "AQAB",
+      "n": "4bOwMSWoWqOSJoLvwFOCVrKmIO_XX5BCI8KYDIgWjII3a83vwia0a11UHm3B6oPlR3L5udworbH7axrmnPz4GEamQp57Yf0uhnGctlVpVVZHOvXPaMlZgTTGhWpJAGnLnyihZbARgyJefuxZ6ZIqeNyjgc_fC-0J7RWFMxNKS_n6ZKFqQlIlmJInJPWR-YZTuooIb4T4C0JAwFDEvXiAs_fX34Tj1FvD1nv01VPGF5Wx6cBV6fejbRCjY4uFfovhE-dtKX0IakZI8jks-uqMjIOB2x1pOuaqrmrINrTYzKTCKrnMpfaW4urhFmQRKNIgaoLnPgzIb3W9F-vpgrdTjw",
+      "x5c": [
+        "MIID/DCCAeSgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBBMQswCQYDVQQGEwJESzEyMDAGA1UEAxMpTmV0cyBlSUQgQnJva2VyIFRva2VuIFNpZ25pbmcgUm9vdCBQUCBFbnYwHhcNMjEwMjI0MDAwMDAwWhcNNDEwMjI0MDAwMDAwWjA\u002BMQswCQYDVQQGEwJESzEvMC0GA1UEAxMmTmV0cyBlSUQgQnJva2VyIFRva2VuIFNpZ25pbmcgMSBQUCBFbnYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDhs7AxJahao5Imgu/AU4JWsqYg79dfkEIjwpgMiBaMgjdrze/CJrRrXVQebcHqg\u002BVHcvm53CitsftrGuac/PgYRqZCnnth/S6GcZy2VWlVVkc69c9oyVmBNMaFakkAacufKKFlsBGDIl5\u002B7Fnpkip43KOBz98L7QntFYUzE0pL\u002BfpkoWpCUiWYkick9ZH5hlO6ighvhPgLQkDAUMS9eICz99ffhOPUW8PWe/TVU8YXlbHpwFXp96NtEKNji4V\u002Bi\u002BET520pfQhqRkjyOSz66oyMg4HbHWk65qquasg2tNjMpMIqucyl9pbi6uEWZBEo0iBqguc\u002BDMhvdb0X6\u002BmCt1OPAgMBAAGjAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQAFKs2PbOIHLgLTNPIhqKCv7Uj\u002Buj0tpF6vO7CFtw1Q0NKo0UB4wD0T9AYu\u002B8sq3M\u002BZjR77eHP6IkvVBEaqBrZgjq6wU1pijPuIUliXF772\u002B/Hr8Wa23iILWevk\u002BleOXDI8kN7E1JPfr0ADsnCJxaXApDqE6Mysd68\u002BGN2adRuvGAcEcKauVfYLAGPQVctkmQSH6VhIJoDKni7gFF/oMZUZ362sOguhlYltcNUMIILJZkFkksRRriHOlz4I8HJiTzWI1Ufuw6iqFWMquOR4BsQZzBSsdPVGlQvjyqOiBia7rPTJE1Z3Kj0mujIbgKTri8YsnFsBynyHq8puYWvMwoGLWu0goxq9rFrINTe39/YpRE6lZUUZU50DddS\u002B0syBTs1H1gX00ofqt6FgWmACc20zJZm2GyhWDtqtiMurn5WKLoZBQrwN5/a6c6HNCStSVxn8o0g9xCgmWM855S8TqHFYXKJSMG00xZZEbsOAPqujkbKakhC/kJQU7XKGjFRskQZhHvpGipFTK4ZapHYYoo5KqZTytAvFENIcuibIk0u8zlCZuXU/PsowMN4G54FftVVyNHuj4TqiKIvB\u002BZNj/zcPopQHHUISVRApR6YO6fqwPxVaJmSTzZ/0uPTaAdnMz5j1wIYf\u002BZDk1ywTxOBRS7/FwNnWAyIuYGFzewY4H2QUNg=="
+      ],
+      "alg": "RS256"
+    },
+    {
+      "kty": "RSA",
+      "use": "enc",
+      "kid": "A2E10A6BAF4E43E86273F57F218A44B824203176",
+      "x5t": "A2E10A6BAF4E43E86273F57F218A44B824203176",
+      "x5c": [
+        "MIIDGjCCAgKgAwIBAgIIQFkx7rDLvyowDQYJKoZIhvcNAQELBQAwNDEyMDAGA1UEAxMpTmV0cyBlSUQgQnJva2VyIGNsaWVudCByZXF1ZXN0IGVuY3J5cHRpb24wIBcNMjIwMjA5MTQxMDQ1WhgPMjExMjAyMTAxNDEwNDVaMDQxMjAwBgNVBAMTKU5ldHMgZUlEIEJyb2tlciBjbGllbnQgcmVxdWVzdCBlbmNyeXB0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAps2R1sxEGxyYfP\u002BK2jq4uDONxrEP4W4oAKJkTv71oOJiK3CBK4ybyk597WufUZfpy8isUKtuqx1x6DTK6vnecUv5KV77/ql6Ac8LJaj5zQBRGuPoPezbD2tPglxp4XEU\u002BqivoyMJiZoLl75MklxvmVVlJQgu1H4RmvdCcp0xYmV2CBX4GKvK6g26y3IZ1FW4Vj5G8eB\u002Bxw423EtsBO3iKPx24BbVZWXC56lhke\u002BQbpbagU/hpQnOdW5tdWdm7oHGUfGwsvg1f6b2Yllwv57ANJkk\u002BfBr\u002BoN0eD2DRNyHbExzJfOBPkDt2FEq1u30kfLP\u002B6ecSByaxSFTl\u002BgFeUUw0wIDAQABoy4wLDALBgNVHQ8EBAMCBBAwHQYDVR0OBBYEFPK08YkmSYK1xNIFEr1AdscfYKnZMA0GCSqGSIb3DQEBCwUAA4IBAQAYeP7IAv3ND\u002B6UMGr9X\u002BP1wURz7UQd66oRldhcdkdS\u002BBNMcU/gVeiU31Es5Y/GhdmKPiuQGdIQdPO88u9A7STWIYUj/lnrgtKif\u002BJ8V/PtfsvHbBYD5f7wFd7fqOpcDFQ2dobOathhrqJ1r3ShFaObpVBX3PL3\u002BSFK3ofHaMYWuIoD\u002BroiOJfIYlL02rrKiiw9r2L9nUCZfSAq3G9rMw\u002BzL38D9BQvrEe9yvwqM3im0m3seiNODiArcY/ee\u002B538\u002BYaToaMPHxUAizREeJFm4aOtwzGND48XHQzbNyfhoSsJesCJsugcNfHUe6o0nuPZ\u002BzvdLrboutrrtxEN8yOm489"
+      ],
+      "alg": "http://www.w3.org/2001/04/xmlenc#rsa-oaep"
+    }
+  ]
+}`
+
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		w.Header().Set(`Content-Type`, `application/json`)
+		w.WriteHeader(http.StatusOK)
+
+		io.WriteString(w, src)
+	}))
+	defer srv.Close()
+
+	for _, ignoreParseError := range []bool{true, false} {
+		ignoreParseError := ignoreParseError
+		t.Run(fmt.Sprintf(`Parse with ignoreParseError=%t`, ignoreParseError), func(t *testing.T) {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			ar := jwk.NewAutoRefresh(ctx)
+			ar.Configure(srv.URL, jwk.WithIgnoreParseError(ignoreParseError))
+
+			set, err := ar.Fetch(ctx, srv.URL)
+			if ignoreParseError {
+				if !assert.NoError(t, err, `ar.Fetch should succeed`) {
+					return
+				}
+				if !assert.Equal(t, set.Len(), 2, `JWKS should contain two keys`) {
+					return
+				}
+			} else {
+				if !assert.Error(t, err, `ar.Fetch should fail`) {
+					return
+				}
+			}
+		})
+	}
+
+	// Test the case when WithIgnoreParseError is passed to ParseKey
+	t.Run(`ParseKey + WithIgnoreParseError should be an error`, func(t *testing.T) {
+		key, err := jwxtest.GenerateRsaJwk()
+		if !assert.NoError(t, err, `jwxtest.GenerateRsaJwk() should succeed`) {
+			return
+		}
+
+		buf, err := json.Marshal(key)
+		if !assert.NoError(t, err, `json.Marshal should succeed`) {
+			return
+		}
+
+		_, err = jwk.ParseKey(buf)
+		if !assert.NoError(t, err, `jwk.ParseKey (no WithIgnoreParseError) should succeed`) {
+			return
+		}
+
+		_, err = jwk.ParseKey(buf, jwk.WithIgnoreParseError(true))
+		if !assert.Error(t, err, `jwk.ParseKey (no WithIgnoreParseError) should fail`) {
+			return
+		}
+	})
+}