Repository: lestrrat-go/jwx
Pull request #697 (2022-04-16 06:45:25)

Merge develop/v2 for v2.0.0-beta2


Patch:
diff --git a/.github/workflows/autodoc.yml b/.github/workflows/autodoc.yml
index b89281ea..7b2ab76d 100644
--- a/.github/workflows/autodoc.yml
+++ b/.github/workflows/autodoc.yml
@@ -2,7 +2,7 @@ name: Auto-Doc
 on:
   pull_request:
     branches:
-      - v2
+      - develop/v2
     types:
       - closed
 
@@ -17,4 +17,6 @@ jobs:
       - name: Process markdown files
         run: |
           find . -name '*.md' | xargs perl tools/autodoc.pl
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2984df76..485f4886 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.17.x', '1.16.x' ]
+        go: [ '1.18.x', '1.17.x' ]
     name: "Test [ Go ${{ matrix.go }} / Tags ${{ matrix.go_tags }} ]"
     steps:
       - name: Checkout repository
diff --git a/Changes b/Changes
index 4f758198..d39deb0c 100644
--- a/Changes
+++ b/Changes
@@ -2,7 +2,22 @@ Changes
 =======
 
 v2 has many incompatibilities with v1. To see the full list of differences between
-v1 and v2, please read the Changes.v2 file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md)
+v1 and v2, please read the Changes-v2.md file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md)
+
+v2.0.0-beta2 - 16 Apr 2022
+[jwk]
+  * Updated `jwk.Set` API and reflected pending changes from v1 which were
+    left over. Please see Changes-v2.md file for details.
+
+  * Added `jwk.CachedSet`, a shim over `jwk.Cache` that allows you to
+    have to write wrappers around `jwk.Cache` that retrieves a particular
+    `jwk.Set` out of it. You can use it to, for example, pass `jwk.CachedSet`
+    to a `jws.Verify`
+
+      cache := jwk.NewCache(ctx)
+      cache.Register(ctx, jwksURL)
+      cachedSet := jwk.NewCachedSet(cache, jwksURL)
+      jws.Verify(signed, jws.WithKeySet(cachedSet))
 
 v2.0.0-beta1 - 09 Apr 2022
 [Miscellaneous]
diff --git a/Changes-v2.md b/Changes-v2.md
index f3dd77cf..864ae4ce 100644
--- a/Changes-v2.md
+++ b/Changes-v2.md
@@ -174,6 +174,28 @@ jwe.Verify(signed, jwe.WithKeySet(jwks), jwe.WithKeyUsed(&keyUsed))
 * `jwk.New()` has been renamed to `jwk.FromRaw()`, which hopefully will
   make it easier for the users what the input should be.
 
+* `jwk.Set` has many interface changes:
+  * Changed methods to match jwk.Key and its semantics:
+    * Field is now Get() (returns values for arbitrary fields other than keys). Fetching a key is done via Key()
+    * Remove() now removes arbitrary fields, not keys. to remove keys, use RemoveKey()
+    * Iterate has been added to iterate through all non-key fields.
+  * Add is now AddKey(Key) string
+  * Get is now Key(int) (Key, bool)
+  * Remove is now RemoveKey(Key) error
+  * Iterate is now Keys(context.Context) KeyIterator
+  * Clear is now Clear() error
+
+* `jwk.CachedSet` has been added. You can create a `jwk.Set` that is backed by
+  `jwk.Cache` so you can do this:
+
+```go
+cache := jkw.NewCache(ctx)
+cachedSet := jwk.NewCachedSet(cache, jwksURI)
+
+// cachedSet is always the refreshed, cached version from jwk.Cache
+jws.Verify(signed, jws.WithKeySet(cachedSet))
+```
+
 * `jwk.NewRSAPRivateKey()`, `jwk.NewECDSAPrivateKey()`, etc have been removed.
   There is no longer any way to create concrete types of `jwk.Key` 
 
diff --git a/bench/comparison/go.mod b/bench/comparison/go.mod
index 01bc350a..64c3a0c8 100644
--- a/bench/comparison/go.mod
+++ b/bench/comparison/go.mod
@@ -3,9 +3,3 @@ module github.com/lestrrat-go/jwx/v2/bench/comparison
 go 1.15
 
 replace github.com/lestrrat-go/jwx/v2 => ../..
-
-require (
-	github.com/golang-jwt/jwt/v4 v4.4.0
-	github.com/lestrrat-go/jwx/v2 v2.0.0-00010101000000-000000000000
-	github.com/stretchr/testify v1.7.1
-)
diff --git a/bench/comparison/go.sum b/bench/comparison/go.sum
index 5334ece7..e69de29b 100644
--- a/bench/comparison/go.sum
+++ b/bench/comparison/go.sum
@@ -1,38 +0,0 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
-github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E=
-github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/golang-jwt/jwt/v4 v4.4.0 h1:EmVIxB5jzbllGIjiCV5JG4VylbK3KE400tLGLI1cdfU=
-github.com/golang-jwt/jwt/v4 v4.4.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
-github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80=
-github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
-github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
-github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
-github.com/lestrrat-go/httprc v1.0.1 h1:Cnc4NxIySph38pQPzKbjg5OkKsGR/Cf5xcWt5OlSUDI=
-github.com/lestrrat-go/httprc v1.0.1/go.mod h1:5Ml+nB++j6IC0e6LzefJnrpMQDKgDwDCaIQQzhbqhJM=
-github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
-github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
-github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
-github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE=
-golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/cmd/jwx/jwe.go b/cmd/jwx/jwe.go
index c44cff1d..fa8e0377 100644
--- a/cmd/jwx/jwe.go
+++ b/cmd/jwx/jwe.go
@@ -98,7 +98,7 @@ func makeJweEncryptCmd() *cli.Command {
 		if keyset.Len() != 1 {
 			return fmt.Errorf(`jwk file must contain exactly one key`)
 		}
-		key, _ := keyset.Get(0)
+		key, _ := keyset.Key(0)
 
 		pubkey, err := jwk.PublicKeyOf(key)
 		if err != nil {
@@ -151,7 +151,7 @@ func makeJweDecryptCmd() *cli.Command {
 		if keyset.Len() != 1 {
 			return fmt.Errorf(`jwk file must contain exactly one key`)
 		}
-		key, _ := keyset.Get(0)
+		key, _ := keyset.Key(0)
 
 		var decrypted []byte
 
diff --git a/cmd/jwx/jwk.go b/cmd/jwx/jwk.go
index 4371b972..c1a1b745 100644
--- a/cmd/jwx/jwk.go
+++ b/cmd/jwx/jwk.go
@@ -76,7 +76,7 @@ func dumpJWKSet(dst io.Writer, keyset jwk.Set, format string, preserve bool) err
 				return fmt.Errorf(`failed to marshal keyset into JSON format: %w`, err)
 			}
 		} else {
-			key, _ := keyset.Get(0)
+			key, _ := keyset.Key(0)
 			if err := dumpJSON(dst, key); err != nil {
 				return fmt.Errorf(`failed to marshal key into JSON format: %w`, err)
 			}
@@ -203,7 +203,7 @@ func makeJwkGenerateCmd() *cli.Command {
 		}
 
 		keyset := jwk.NewSet()
-		keyset.Add(key)
+		keyset.AddKey(key)
 
 		if c.Bool("public-key") {
 			pubks, err := jwk.PublicSetOf(keyset)
diff --git a/cmd/jwx/jws.go b/cmd/jwx/jws.go
index 44d8e39b..a500fa68 100644
--- a/cmd/jwx/jws.go
+++ b/cmd/jwx/jws.go
@@ -183,7 +183,7 @@ func makeJwsVerifyCmd() *cli.Command {
 			}
 
 			ctx := context.Background()
-			for iter := keyset.Iterate(ctx); iter.Next(ctx); {
+			for iter := keyset.Keys(ctx); iter.Next(ctx); {
 				pair := iter.Pair()
 				key := pair.Value.(jwk.Key)
 				payload, err := jws.Verify(buf, jws.WithKey(alg, key))
@@ -232,7 +232,7 @@ func makeJwsSignCmd() *cli.Command {
 		if keyset.Len() != 1 {
 			return fmt.Errorf(`jwk file must contain exactly one key`)
 		}
-		key, _ := keyset.Get(0)
+		key, _ := keyset.Key(0)
 
 		src, err := getSource(c.Args().Get(0))
 		if err != nil {
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 00000000..130effd7
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,2 @@
+codecov:
+  allow_coverage_offsets: true
diff --git a/docs/01-jwt.md b/docs/01-jwt.md
index 4f268283..9bde25f9 100644
--- a/docs/01-jwt.md
+++ b/docs/01-jwt.md
@@ -392,8 +392,8 @@ func ExampleJWT_ParseWithKeySet() {
     // all of the public keys
     {
       privset := jwk.NewSet()
-      privset.Add(realKey)
-      privset.Add(bogusKey)
+      privset.AddKey(realKey)
+      privset.AddKey(bogusKey)
       v, err := jwk.PublicSetOf(privset)
       if err != nil {
         fmt.Printf("failed to create public JWKS: %s\n", err)
@@ -610,7 +610,7 @@ func ExampleJWT_ParseWithJKU() {
       fmt.Printf("failed to create public key: %s\n", err)
       return
     }
-    set.Add(pubkey)
+    set.AddKey(pubkey)
   }
 
   srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/docs/02-jws.md b/docs/02-jws.md
index 7407e83b..bd3d29d2 100644
--- a/docs/02-jws.md
+++ b/docs/02-jws.md
@@ -370,14 +370,14 @@ func ExampleJWS_VerifyWithJWKSet() {
   set := jwk.NewSet()
   // Add some bogus keys
   k1, _ := jwk.FromRaw([]byte("abracadavra"))
-  set.Add(k1)
+  set.AddKey(k1)
   k2, _ := jwk.FromRaw([]byte("opensasame"))
-  set.Add(k2)
-  // Add the real thing
+  set.AddKey(k2)
+  // AddKey the real thing
   pubkey, _ := jwk.PublicRawKeyOf(privkey)
   k3, _ := jwk.FromRaw(pubkey)
   k3.Set(jwk.AlgorithmKey, jwa.RS256)
-  set.Add(k3)
+  set.AddKey(k3)
 
   // Up to this point, you probably will replace with a simple jwk.Fetch()
 
diff --git a/docs/03-jwe.md b/docs/03-jwe.md
index bea35d3f..71ab01e2 100644
--- a/docs/03-jwe.md
+++ b/docs/03-jwe.md
@@ -416,13 +416,13 @@ func ExampleJWE_VerifyWithJWKSet() {
   set := jwk.NewSet()
   // Add some bogus keys
   k1, _ := jwk.FromRaw([]byte("abracadavra"))
-  set.Add(k1)
+  set.AddKey(k1)
   k2, _ := jwk.FromRaw([]byte("opensasame"))
-  set.Add(k2)
+  set.AddKey(k2)
   // Add the real thing
   k3, _ := jwk.FromRaw(privkey)
   k3.Set(jwk.AlgorithmKey, jwa.RSA_OAEP)
-  set.Add(k3)
+  set.AddKey(k3)
 
   // Up to this point, you probably will replace with a simple jwk.Fetch()
 
diff --git a/docs/04-jwk.md b/docs/04-jwk.md
index 6578bb7e..98377655 100644
--- a/docs/04-jwk.md
+++ b/docs/04-jwk.md
@@ -524,7 +524,7 @@ source: [examples/jwk_from_raw_example_test.go](https://github.com/lestrrat-go/j
 
 To parse keys stored in a remote location pointed by a HTTP(s) URL, use [`jwk.Fetch()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Fetch)
 
-If you are going to be using this key repeatedly in a long running process, consider using [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) described elsewhere in this document.
+If you are going to be using this key repeatedly in a long running process, consider using [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) or [`jwk.CachedSet`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#CachedSet) described elsewhere in this document.
 
 <!-- INCLUDE(examples/jwk_fetch_example_test.go) -->
 ```go
@@ -590,7 +590,7 @@ Normally, you should be able to simply fetch the JWK using [`jwk.Fetch()`](https
 but keys are usually routinely expired and rotated due to security reasons.
 In such cases you would need to refetch the JWK periodically, which is a pain.
 
-`github.com/lestrrat-go/jwx/v2/jwk` provides the [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) tool to do this for you.
+`github.com/lestrrat-go/jwx/v2/jwk` provides the [`jwk.Cache`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#Cache) and [`jwk.CachedSet`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jwk#CachedSet) to do this for you.
 
 <!-- INCLUDE(examples/jwk_cache_example_test.go) -->
 ```go
@@ -669,6 +669,52 @@ MAIN:
 source: [examples/jwk_cache_example_test.go](https://github.com/lestrrat-go/jwx/blob/v2/examples/jwk_cache_example_test.go)
 <!-- END INCLUDE -->
 
+<!-- INCLUDE(examples/jwk_cached_set_example_test.go) -->
+```go
+package examples_test
+
+import (
+  "context"
+  "fmt"
+  "time"
+
+  "github.com/lestrrat-go/jwx/v2/jwk"
+  "github.com/lestrrat-go/jwx/v2/jws"
+)
+
+func ExampleJWK_CachedSet() {
+  ctx, cancel := context.WithCancel(context.Background())
+  defer cancel()
+
+  const googleCerts = `https://www.googleapis.com/oauth2/v3/certs`
+
+  // The first steps are the same as examples/jwk_cache_example_test.go
+  c := jwk.NewCache(ctx)
+  c.Register(googleCerts, jwk.WithMinRefreshInterval(15*time.Minute))
+  _, err := c.Refresh(ctx, googleCerts)
+  if err != nil {
+    fmt.Printf("failed to refresh google JWKS: %s\n", err)
+    return
+  }
+
+  cached := jwk.NewCachedSet(c, googleCerts)
+
+  // cached fulfills the jwk.Set interface.
+  var _ jwk.Set = cached
+
+  // That means you can pass it to things like jws.WithKeySet,
+  // allowing you to pretend as if you are using the result of
+  //
+  //   jwk.Fetch(ctx, googleCerts)
+  //
+  // But you are instead using a cached (and periodically refreshed)
+  // for each operation.
+  _ = jws.WithKeySet(cached)
+}
+```
+source: [examples/jwk_cached_set_example_test.go](https://github.com/lestrrat-go/jwx/blob/v2/examples/jwk_cached_set_example_test.go)
+<!-- END INCLUDE -->
+
 ## Using Whitelists
 
 If you are fetching JWK Sets from a possibly untrusted source such as the URL in the`"jku"` field of a JWS message,
diff --git a/jwe/key_provider.go b/jwe/key_provider.go
index f3467f72..5302c392 100644
--- a/jwe/key_provider.go
+++ b/jwe/key_provider.go
@@ -143,7 +143,7 @@ func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, r Recipient
 	}
 
 	for i := 0; i < kp.set.Len(); i++ {
-		key, _ := kp.set.Get(i)
+		key, _ := kp.set.Key(i)
 		if err := kp.selectKey(sink, key, r, msg); err != nil {
 			continue
 		}
diff --git a/jwk/cache.go b/jwk/cache.go
index 974589e0..1c9efae2 100644
--- a/jwk/cache.go
+++ b/jwk/cache.go
@@ -8,6 +8,8 @@ import (
 	"time"
 
 	"github.com/lestrrat-go/httprc"
+	"github.com/lestrrat-go/iter/arrayiter"
+	"github.com/lestrrat-go/iter/mapiter"
 )
 
 type Transformer = httprc.Transformer
@@ -213,3 +215,134 @@ func (c *Cache) Unregister(u string) error {
 func (c *Cache) Snapshot() *httprc.Snapshot {
 	return c.cache.Snapshot()
 }
+
+// CachedSet is a thin shim over jwk.Cache that allows the user to cloack
+// jwk.Cache as if it's a `jwk.Set`. Behind the scenes, the `jwk.Set` is
+// retrieved from the `jwk.Cache` for every operation.
+//
+// Since `jwk.CachedSet` always deals with a cached version of the `jwk.Set`,
+// all operations that mutate the object (such as AddKey(), RemoveKey(), et. al)
+// are no-ops and return an error.
+//
+// Note that since this is a utility shim over `jwk.Cache`, you _will_ lose
+// the ability to control the finer details (such as controlling how long to
+// wait for in case of a fetch failure using `context.Context`)
+type CachedSet struct {
+	cache *Cache
+	url   string
+}
+
+var _ Set = &CachedSet{}
+
+func NewCachedSet(cache *Cache, url string) Set {
+	return &CachedSet{
+		cache: cache,
+		url:   url,
+	}
+}
+
+func (cs *CachedSet) cached() (Set, error) {
+	return cs.cache.Get(context.Background(), cs.url)
+}
+
+// Add is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
+func (*CachedSet) AddKey(_ Key) error {
+	return fmt.Errorf(`(jwk.Cachedset).AddKey: jwk.CachedSet is immutable`)
+}
+
+// Clear is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
+func (*CachedSet) Clear() error {
+	return fmt.Errorf(`(jwk.CachedSet).Clear: jwk.CachedSet is immutable`)
+}
+
+// Set is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
+func (*CachedSet) Set(_ string, _ interface{}) error {
+	return fmt.Errorf(`(jwk.CachedSet).Set: jwk.CachedSet is immutable`)
+}
+
+// Remove is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
+func (*CachedSet) Remove(_ string) error {
+	// TODO: Remove() should be renamed to Remove(string) error
+	return fmt.Errorf(`(jwk.CachedSet).Remove: jwk.CachedSet is immutable`)
+}
+
+// RemoveKey is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
+func (*CachedSet) RemoveKey(_ Key) error {
+	return fmt.Errorf(`(jwk.CachedSet).RemoveKey: jwk.CachedSet is immutable`)
+}
+
+func (cs *CachedSet) Clone() (Set, error) {
+	set, err := cs.cached()
+	if err != nil {
+		return nil, fmt.Errorf(`failed to get cached jwk.Set: %w`, err)
+	}
+
+	return set.Clone()
+}
+
+// Get returns the value of non-Key field stored in the jwk.Set
+func (cs *CachedSet) Get(name string) (interface{}, bool) {
+	set, err := cs.cached()
+	if err != nil {
+		return nil, false
+	}
+
+	return set.Get(name)
+}
+
+// Key returns the Key at the specified index
+func (cs *CachedSet) Key(idx int) (Key, bool) {
+	set, err := cs.cached()
+	if err != nil {
+		return nil, false
+	}
+
+	return set.Key(idx)
+}
+
+func (cs *CachedSet) Index(key Key) int {
+	set, err := cs.cached()
+	if err != nil {
+		return -1
+	}
+
+	return set.Index(key)
+}
+
+func (cs *CachedSet) Keys(ctx context.Context) KeyIterator {
+	//nolint:contextcheck
+	set, err := cs.cached()
+	if err != nil {
+		return arrayiter.New(nil)
+	}
+
+	return set.Keys(ctx)
+}
+
+func (cs *CachedSet) Iterate(ctx context.Context) HeaderIterator {
+	//nolint:contextcheck
+	set, err := cs.cached()
+	if err != nil {
+		return mapiter.New(nil)
+	}
+
+	return set.Iterate(ctx)
+}
+
+func (cs *CachedSet) Len() int {
+	set, err := cs.cached()
+	if err != nil {
+		return -1
+	}
+
+	return set.Len()
+}
+
+func (cs *CachedSet) LookupKeyID(kid string) (Key, bool) {
+	set, err := cs.cached()
+	if err != nil {
+		return nil, false
+	}
+
+	return set.LookupKeyID(kid)
+}
diff --git a/jwk/interface.go b/jwk/interface.go
index 3ca5487d..a8a3763f 100644
--- a/jwk/interface.go
+++ b/jwk/interface.go
@@ -49,30 +49,26 @@ const (
 // are stored in _both_ the resulting `jwk.Set` object and the `jwk.Key` object .
 //
 type Set interface {
-	// Add adds the specified key. If the key already exists in the set, it is
-	// not added.
-	// This method will be renamed to `AddKey(Key)` in a future major release.
-	Add(Key) bool
+	// AddKey adds the specified key. If the key already exists in the set, it is
+	// not added (but no error is returned).
+	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.
-	Clear()
+	Clear() error
 
 	// Get returns the key at index `idx`. If the index is out of range,
 	// then the second return value is false.
-	// This method will be renamed to `Key(int)` in a future major release.
-	Get(int) (Key, bool)
+	Key(int) (Key, bool)
 
-	// Field returns the value of a private field in the key set.
+	// Get returns the value of a private field in the key set.
 	//
 	// For the purposes of a key set, any field other than the "keys" field is
 	// considered to be a private field. In other words, you cannot use this
 	// method to directly access the list of keys in the set
-	//
-	// This method will be renamed to `Get(string)` in a future major release.
-	Field(string) (interface{}, bool)
+	Get(string) (interface{}, bool)
 
 	// Set sets the value of a single field.
 	//
@@ -81,8 +77,10 @@ type Set interface {
 	// specify, and there is no way of knowing what type they could be.
 	Set(string, interface{}) error
 
-	// Remove removes the field associated with the specified key.
-	// There is no way to remove the `kty` (key type). You will ALWAYS be left with one field in a jwk.Key.
+	// RemoveKey removes the specified non-key field from the set.
+	// Keys may not be removed using this method.
+	Remove(string) error
+
 	// Index returns the index where the given key exists, -1 otherwise
 	Index(Key) int
 
@@ -95,11 +93,14 @@ type Set interface {
 	// need all of them, use `Iterate()`
 	LookupKeyID(string) (Key, bool)
 
-	// Remove removes the key from the set.
-	Remove(Key) bool
+	// RemoveKey removes the key from the set.
+	RemoveKey(Key) error
+
+	// Keys creates an iterator to iterate through all keys in the set.
+	Keys(context.Context) KeyIterator
 
-	// Iterate creates an iterator to iterate through all keys in the set.
-	Iterate(context.Context) KeyIterator
+	// Iterate creates an iterator to iterate through all fields other than the keys
+	Iterate(context.Context) HeaderIterator
 
 	// Clone create a new set with identical keys. Keys themselves are not cloned.
 	Clone() (Set, error)
diff --git a/jwk/jwk.go b/jwk/jwk.go
index d53dc458..75a6329d 100644
--- a/jwk/jwk.go
+++ b/jwk/jwk.go
@@ -135,7 +135,7 @@ func PublicSetOf(v Set) (Set, error) {
 
 	n := v.Len()
 	for i := 0; i < n; i++ {
-		k, ok := v.Get(i)
+		k, ok := v.Key(i)
 		if !ok {
 			return nil, fmt.Errorf(`key not found`)
 		}
@@ -143,7 +143,9 @@ func PublicSetOf(v Set) (Set, error) {
 		if err != nil {
 			return nil, fmt.Errorf(`failed to get public key of %T: %w`, k, err)
 		}
-		newSet.Add(pubKey)
+		if err := newSet.AddKey(pubKey); err != nil {
+			return nil, fmt.Errorf(`failed to add key to public key set: %w`, err)
+		}
 	}
 
 	return newSet, nil
@@ -525,7 +527,9 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
 			if err != nil {
 				return nil, fmt.Errorf(`failed to create jwk.Key from %T: %w`, raw, err)
 			}
-			s.Add(key)
+			if err := s.AddKey(key); err != nil {
+				return nil, fmt.Errorf(`failed to add jwk.Key to set: %w`, err)
+			}
 			src = bytes.TrimSpace(rest)
 		}
 		return s, nil
@@ -641,7 +645,9 @@ func Pem(v interface{}) ([]byte, error) {
 	switch v := v.(type) {
 	case Key:
 		set = NewSet()
-		set.Add(v)
+		if err := set.AddKey(v); err != nil {
+			return nil, fmt.Errorf(`failed to add key to set: %w`, err)
+		}
 	case Set:
 		set = v
 	default:
@@ -650,7 +656,7 @@ func Pem(v interface{}) ([]byte, error) {
 
 	var ret []byte
 	for i := 0; i < set.Len(); i++ {
-		key, _ := set.Get(i)
+		key, _ := set.Key(i)
 		typ, buf, err := asnEncode(key)
 		if err != nil {
 			return nil, fmt.Errorf(`failed to encode content for key #%d: %w`, i, err)
diff --git a/jwk/set.go b/jwk/set.go
index 6fd8df38..6a261566 100644
--- a/jwk/set.go
+++ b/jwk/set.go
@@ -7,6 +7,7 @@ import (
 	"sort"
 
 	"github.com/lestrrat-go/iter/arrayiter"
+	"github.com/lestrrat-go/iter/mapiter"
 	"github.com/lestrrat-go/jwx/v2/internal/json"
 	"github.com/lestrrat-go/jwx/v2/internal/pool"
 )
@@ -37,7 +38,7 @@ func (s *set) Set(n string, v interface{}) error {
 	return nil
 }
 
-func (s *set) Field(n string) (interface{}, bool) {
+func (s *set) Get(n string) (interface{}, bool) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
@@ -45,7 +46,7 @@ func (s *set) Field(n string) (interface{}, bool) {
 	return v, ok
 }
 
-func (s *set) Get(idx int) (Key, bool) {
+func (s *set) Key(idx int) (Key, bool) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
@@ -79,18 +80,26 @@ func (s *set) Index(key Key) int {
 	return s.indexNL(key)
 }
 
-func (s *set) Add(key Key) bool {
+func (s *set) AddKey(key Key) error {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
 	if i := s.indexNL(key); i > -1 {
-		return false
+		return fmt.Errorf(`(jwk.Set).AddKey: key already exists`)
 	}
 	s.keys = append(s.keys, key)
-	return true
+	return nil
+}
+
+func (s *set) Remove(name string) error {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	delete(s.privateParams, name)
+	return nil
 }
 
-func (s *set) Remove(key Key) bool {
+func (s *set) RemoveKey(key Key) error {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
@@ -104,20 +113,22 @@ func (s *set) Remove(key Key) bool {
 			default:
 				s.keys = append(s.keys[:i], s.keys[i+1:]...)
 			}
-			return true
+			return nil
 		}
 	}
-	return false
+	return fmt.Errorf(`(jwk.Set).RemoveKey: specified key does not exist in set`)
 }
 
-func (s *set) Clear() {
+func (s *set) Clear() error {
 	s.mu.Lock()
 	defer s.mu.Unlock()
 
 	s.keys = nil
+	s.privateParams = make(map[string]interface{})
+	return nil
 }
 
-func (s *set) Iterate(ctx context.Context) KeyIterator {
+func (s *set) Keys(ctx context.Context) KeyIterator {
 	ch := make(chan *KeyPair, s.Len())
 	go iterate(ctx, s.keys, ch)
 	return arrayiter.New(ch)
@@ -264,7 +275,7 @@ func (s *set) LookupKeyID(kid string) (Key, bool) {
 
 	n := s.Len()
 	for i := 0; i < n; i++ {
-		key, ok := s.Get(i)
+		key, ok := s.Key(i)
 		if !ok {
 			return nil, false
 		}
@@ -300,3 +311,31 @@ func (s *set) Clone() (Set, error) {
 	}
 	return s2, nil
 }
+
+func (s *set) makePairs() []*HeaderPair {
+	pairs := make([]*HeaderPair, 0, len(s.privateParams))
+	for k, v := range s.privateParams {
+		pairs = append(pairs, &HeaderPair{Key: k, Value: v})
+	}
+	sort.Slice(pairs, func(i, j int) bool {
+		//nolint:forcetypeassert
+		return pairs[i].Key.(string) < pairs[j].Key.(string)
+	})
+	return pairs
+}
+
+func (s *set) Iterate(ctx context.Context) HeaderIterator {
+	pairs := s.makePairs()
+	ch := make(chan *HeaderPair, len(pairs))
+	go func(ctx context.Context, ch chan *HeaderPair, pairs []*HeaderPair) {
+		defer close(ch)
+		for _, pair := range pairs {
+			select {
+			case <-ctx.Done():
+				return
+			case ch <- pair:
+			}
+		}
+	}(ctx, ch, pairs)
+	return mapiter.New(ch)
+}
diff --git a/jws/key_provider.go b/jws/key_provider.go
index e77af40e..02fcded8 100644
--- a/jws/key_provider.go
+++ b/jws/key_provider.go
@@ -165,7 +165,7 @@ func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, sig *Signat
 
 			// if we got here, then useDefault == true AND there is exactly
 			// one key in the set.
-			key, _ = kp.set.Get(0)
+			key, _ = kp.set.Key(0)
 		} else {
 			// Otherwise we better be able to look up the key, baby.
 			v, ok := kp.set.LookupKeyID(wantedKid)
@@ -179,7 +179,7 @@ func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, sig *Signat
 	}
 
 	for i := 0; i < kp.set.Len(); i++ {
-		key, _ := kp.set.Get(i)
+		key, _ := kp.set.Key(i)
 		if err := kp.selectKey(sink, key, sig, msg); err != nil {
 			continue
 		}
diff --git a/tools/autodoc.pl b/tools/autodoc.pl
index 46bd14c6..b2764ade 100644
--- a/tools/autodoc.pl
+++ b/tools/autodoc.pl
@@ -5,6 +5,12 @@ use File::Temp;
 # Accept a list of filenames, and process them
 # if any of them has a diff, commit it
 
+# Use GITHUB_REF, but if the ref is develop/v\d, then use v\d
+my $link_ref = $ENV{GITHUB_REF};
+if ($link_ref =~ /^develop\/(v\d+)$/) {
+    $link_ref = $1;
+}
+
 my @files = @ARGV;
 my @has_diff;
 for my $filename (@files) {
@@ -36,7 +42,8 @@ for my $filename (@files) {
         $content =~ s{^(\t+)}{"  " x length($1)}gsme;
         $output->print($content);
         $output->print("```\n");
-        $output->print("source: [$include_filename](https://github.com/lestrrat-go/jwx/blob/$ENV{GITHUB_REF}/$include_filename)\n");
+
+        $output->print("source: [$include_filename](https://github.com/lestrrat-go/jwx/blob/$link_ref/$include_filename)\n");
     
         # now we need to skip copying until the end of INCLUDE
         $skip_until_end = 1;
@@ -63,7 +70,9 @@ if (!$ENV{AUTODOC_DRYRUN}) {
         system("git", "remote", "set-url", "origin", "https://github-actions:$ENV{GITHUB_TOKEN}\@github.com/$ENV{GITHUB_REPOSITORY}") == 0 or die $!;
         system("git", "config", "--global", "user.name", "$ENV{GITHUB_ACTOR}") == 0 or die $!;
         system("git", "config", "--global", "user.email", "$ENV{GITHUB_ACTOR}\@users.noreply.github.com") == 0 or die $!;
+        system("git", "switch", "-c", "autodoc-pr-$ENV{GITHUB_HEAD_REF}") == 0 or die $!;
         system("git", "commit", "-F", $commit_message_file->filename, @files) == 0 or die $!;
-        system("git", "push", "origin", "HEAD:$ENV{GITHUB_REF}") == 0 or die $!;
+        system("git", "push", "origin", "HEAD:autodoc-pr-$ENV{GITHUB_HEAD_REF}") == 0 or die $!;
+        system("gh", "pr", "create", "--fill") == 0 or die $!;
     }
 }
diff --git a/tools/cmd/genjwe/go.mod b/tools/cmd/genjwe/go.mod
index c43c3ece..5b1226bd 100644
--- a/tools/cmd/genjwe/go.mod
+++ b/tools/cmd/genjwe/go.mod
@@ -6,4 +6,5 @@ require (
 	github.com/goccy/go-yaml v1.9.4
 	github.com/lestrrat-go/codegen v1.0.4-0.20220329032654-7b542c9fb49b
 	github.com/stretchr/testify v1.7.1 // indirect
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
 )
diff --git a/tools/cmd/genjwe/go.sum b/tools/cmd/genjwe/go.sum
index 2fabb0a6..952c1ee2 100644
--- a/tools/cmd/genjwe/go.sum
+++ b/tools/cmd/genjwe/go.sum
@@ -47,8 +47,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/tools/cmd/genjwk/go.mod b/tools/cmd/genjwk/go.mod
index 8bbad412..79da8af7 100644
--- a/tools/cmd/genjwk/go.mod
+++ b/tools/cmd/genjwk/go.mod
@@ -6,4 +6,5 @@ require (
 	github.com/goccy/go-yaml v1.9.4
 	github.com/lestrrat-go/codegen v1.0.4-0.20220329032654-7b542c9fb49b
 	github.com/stretchr/testify v1.7.1 // indirect
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
 )
diff --git a/tools/cmd/genjwk/go.sum b/tools/cmd/genjwk/go.sum
index 2fabb0a6..952c1ee2 100644
--- a/tools/cmd/genjwk/go.sum
+++ b/tools/cmd/genjwk/go.sum
@@ -47,8 +47,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/tools/cmd/genjws/go.mod b/tools/cmd/genjws/go.mod
index 9dbf8afa..71e3fd50 100644
--- a/tools/cmd/genjws/go.mod
+++ b/tools/cmd/genjws/go.mod
@@ -6,4 +6,5 @@ require (
 	github.com/goccy/go-yaml v1.9.4
 	github.com/lestrrat-go/codegen v1.0.4-0.20220329032654-7b542c9fb49b
 	github.com/stretchr/testify v1.7.1 // indirect
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
 )
diff --git a/tools/cmd/genjws/go.sum b/tools/cmd/genjws/go.sum
index 2fabb0a6..952c1ee2 100644
--- a/tools/cmd/genjws/go.sum
+++ b/tools/cmd/genjws/go.sum
@@ -47,8 +47,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/tools/cmd/genjwt/go.mod b/tools/cmd/genjwt/go.mod
index cd0f642e..2cf74ff1 100644
--- a/tools/cmd/genjwt/go.mod
+++ b/tools/cmd/genjwt/go.mod
@@ -3,8 +3,9 @@ module github.com/lestrrat-go/jwx/v2/jwt/internal/cmd/gentoken
 go 1.16
 
 require (
-	github.com/goccy/go-json v0.7.10
+	github.com/goccy/go-json v0.9.6
 	github.com/goccy/go-yaml v1.9.4
 	github.com/lestrrat-go/codegen v1.0.4-0.20220329032654-7b542c9fb49b
 	github.com/stretchr/testify v1.7.1 // indirect
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
 )
diff --git a/tools/cmd/genjwt/go.sum b/tools/cmd/genjwt/go.sum
index 3a577c0f..81720884 100644
--- a/tools/cmd/genjwt/go.sum
+++ b/tools/cmd/genjwt/go.sum
@@ -9,8 +9,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
 github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
-github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec=
-github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.9.6 h1:5/4CtRQdtsX0sal8fdVhTaiMN01Ri8BExZZ8iRmHQ6E=
+github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/goccy/go-yaml v1.9.4 h1:S0GCYjwHKVI6IHqio7QWNKNThUl6NLzFd/g8Z65Axw8=
 github.com/goccy/go-yaml v1.9.4/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
 github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
@@ -49,8 +49,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/tools/cmd/genoptions/go.mod b/tools/cmd/genoptions/go.mod
index 4ffca4ab..38bbdb0a 100644
--- a/tools/cmd/genoptions/go.mod
+++ b/tools/cmd/genoptions/go.mod
@@ -15,7 +15,7 @@ require (
 	github.com/mattn/go-isatty v0.0.12 // indirect
 	github.com/stretchr/testify v1.7.1 // indirect
 	golang.org/x/mod v0.3.0 // indirect
-	golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
+	golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
 	golang.org/x/tools v0.0.0-20200918232735-d647fc253266 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 )
diff --git a/tools/cmd/genoptions/go.sum b/tools/cmd/genoptions/go.sum
index b9f5a4bf..31c7707b 100644
--- a/tools/cmd/genoptions/go.sum
+++ b/tools/cmd/genoptions/go.sum
@@ -47,8 +47,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
+golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=


Test patch:
diff --git a/bench/performance/jwt_benchmark_test.go b/bench/performance/jwt_benchmark_test.go
index b7c655f3..6635aeff 100644
--- a/bench/performance/jwt_benchmark_test.go
+++ b/bench/performance/jwt_benchmark_test.go
@@ -146,14 +146,14 @@ func BenchmarkJWT(b *testing.B) {
 					Name:      "jwt.ParseString",
 					SkipShort: true,
 					Test: func(b *testing.B) error {
-						_, err := jwt.ParseString(jsonString)
+						_, err := jwt.ParseString(jsonString, jwt.WithVerify(false), jwt.WithValidate(false))
 						return err
 					},
 				},
 				{
 					Name: "jwt.Parse",
 					Test: func(b *testing.B) error {
-						_, err := jwt.Parse(jsonBuf)
+						_, err := jwt.Parse(jsonBuf, jwt.WithVerify(false), jwt.WithValidate(false))
 						return err
 					},
 				},
@@ -165,7 +165,7 @@ func BenchmarkJWT(b *testing.B) {
 						return err
 					},
 					Test: func(b *testing.B) error {
-						_, err := jwt.ParseReader(jsonReader)
+						_, err := jwt.ParseReader(jsonReader, jwt.WithVerify(false), jwt.WithValidate(false))
 						return err
 					},
 				},
diff --git a/examples/jwe_decrypt_with_keyset_example_test.go b/examples/jwe_decrypt_with_keyset_example_test.go
index be8f8cd3..aa298e48 100644
--- a/examples/jwe_decrypt_with_keyset_example_test.go
+++ b/examples/jwe_decrypt_with_keyset_example_test.go
@@ -27,13 +27,13 @@ func ExampleJWE_VerifyWithJWKSet() {
 	set := jwk.NewSet()
 	// Add some bogus keys
 	k1, _ := jwk.FromRaw([]byte("abracadavra"))
-	set.Add(k1)
+	set.AddKey(k1)
 	k2, _ := jwk.FromRaw([]byte("opensasame"))
-	set.Add(k2)
+	set.AddKey(k2)
 	// Add the real thing
 	k3, _ := jwk.FromRaw(privkey)
 	k3.Set(jwk.AlgorithmKey, jwa.RSA_OAEP)
-	set.Add(k3)
+	set.AddKey(k3)
 
 	// Up to this point, you probably will replace with a simple jwk.Fetch()
 
diff --git a/examples/jwk_cached_set_example_test.go b/examples/jwk_cached_set_example_test.go
new file mode 100644
index 00000000..6dc0cd5e
--- /dev/null
+++ b/examples/jwk_cached_set_example_test.go
@@ -0,0 +1,40 @@
+package examples_test
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/lestrrat-go/jwx/v2/jwk"
+	"github.com/lestrrat-go/jwx/v2/jws"
+)
+
+func ExampleJWK_CachedSet() {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	const googleCerts = `https://www.googleapis.com/oauth2/v3/certs`
+
+	// The first steps are the same as examples/jwk_cache_example_test.go
+	c := jwk.NewCache(ctx)
+	c.Register(googleCerts, jwk.WithMinRefreshInterval(15*time.Minute))
+	_, err := c.Refresh(ctx, googleCerts)
+	if err != nil {
+		fmt.Printf("failed to refresh google JWKS: %s\n", err)
+		return
+	}
+
+	cached := jwk.NewCachedSet(c, googleCerts)
+
+	// cached fulfills the jwk.Set interface.
+	var _ jwk.Set = cached
+
+	// That means you can pass it to things like jws.WithKeySet,
+	// allowing you to pretend as if you are using the result of
+	//
+	//   jwk.Fetch(ctx, googleCerts)
+	//
+	// But you are instead using a cached (and periodically refreshed)
+	// for each operation.
+	_ = jws.WithKeySet(cached)
+}
diff --git a/examples/jws_verify_with_keyset_example_test.go b/examples/jws_verify_with_keyset_example_test.go
index 29fc3891..841dc77e 100644
--- a/examples/jws_verify_with_keyset_example_test.go
+++ b/examples/jws_verify_with_keyset_example_test.go
@@ -28,14 +28,14 @@ func ExampleJWS_VerifyWithJWKSet() {
 	set := jwk.NewSet()
 	// Add some bogus keys
 	k1, _ := jwk.FromRaw([]byte("abracadavra"))
-	set.Add(k1)
+	set.AddKey(k1)
 	k2, _ := jwk.FromRaw([]byte("opensasame"))
-	set.Add(k2)
-	// Add the real thing
+	set.AddKey(k2)
+	// AddKey the real thing
 	pubkey, _ := jwk.PublicRawKeyOf(privkey)
 	k3, _ := jwk.FromRaw(pubkey)
 	k3.Set(jwk.AlgorithmKey, jwa.RS256)
-	set.Add(k3)
+	set.AddKey(k3)
 
 	// Up to this point, you probably will replace with a simple jwk.Fetch()
 
diff --git a/examples/jwt_example_test.go b/examples/jwt_example_test.go
index 9a00a3f7..cc0229ef 100644
--- a/examples/jwt_example_test.go
+++ b/examples/jwt_example_test.go
@@ -73,7 +73,7 @@ func ExampleJWT_ParseWithJWKS() {
 
 			// This JWKS can *only* have 1 key.
 			keyset = jwk.NewSet()
-			keyset.Add(pubKey)
+			keyset.AddKey(pubKey)
 		}
 
 		{
diff --git a/examples/jwt_parse_with_jku_example_test.go b/examples/jwt_parse_with_jku_example_test.go
index d82bc12e..df573d80 100644
--- a/examples/jwt_parse_with_jku_example_test.go
+++ b/examples/jwt_parse_with_jku_example_test.go
@@ -45,7 +45,7 @@ func ExampleJWT_ParseWithJKU() {
 			fmt.Printf("failed to create public key: %s\n", err)
 			return
 		}
-		set.Add(pubkey)
+		set.AddKey(pubkey)
 	}
 
 	srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
diff --git a/examples/jwt_parse_with_keyset_example_test.go b/examples/jwt_parse_with_keyset_example_test.go
index 0df6dbc0..9c880872 100644
--- a/examples/jwt_parse_with_keyset_example_test.go
+++ b/examples/jwt_parse_with_keyset_example_test.go
@@ -51,8 +51,8 @@ func ExampleJWT_ParseWithKeySet() {
 		// all of the public keys
 		{
 			privset := jwk.NewSet()
-			privset.Add(realKey)
-			privset.Add(bogusKey)
+			privset.AddKey(realKey)
+			privset.AddKey(bogusKey)
 			v, err := jwk.PublicSetOf(privset)
 			if err != nil {
 				fmt.Printf("failed to create public JWKS: %s\n", err)
diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go
index 468c1c36..bfd86ab5 100644
--- a/jwe/jwe_test.go
+++ b/jwe/jwe_test.go
@@ -268,7 +268,7 @@ func TestParse_RSAES_OAEP_AES_GCM(t *testing.T) {
 				// Keys are not going to be selected without an algorithm
 				_ = pkJwk.Set(jwe.AlgorithmKey, jwa.RSA_OAEP)
 				set := jwk.NewSet()
-				set.Add(pkJwk)
+				set.AddKey(pkJwk)
 
 				var used interface{}
 				plaintext, err = jwe.Decrypt(encrypted, jwe.WithKeySet(set, jwe.WithRequireKid(false)), jwe.WithKeyUsed(&used))
diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go
index 69414910..3e54afd0 100644
--- a/jwk/jwk_test.go
+++ b/jwk/jwk_test.go
@@ -313,7 +313,7 @@ func TestParse(t *testing.T) {
 			ctx, cancel := context.WithCancel(context.Background())
 			defer cancel()
 
-			for iter := set.Iterate(ctx); iter.Next(ctx); {
+			for iter := set.Keys(ctx); iter.Next(ctx); {
 				pair := iter.Pair()
 				if !assert.True(t, reflect.TypeOf(pair.Value).AssignableTo(expected), "key should be a %s", expected) {
 					return
@@ -331,7 +331,7 @@ func TestParse(t *testing.T) {
 				return
 			}
 
-			for iter := set.Iterate(context.TODO()); iter.Next(context.TODO()); {
+			for iter := set.Keys(context.TODO()); iter.Next(context.TODO()); {
 				pair := iter.Pair()
 				key := pair.Value.(jwk.Key)
 
@@ -651,7 +651,7 @@ func TestRoundtrip(t *testing.T) {
 		if !assert.NoError(t, err, `tc.generate should succeed`) {
 			return
 		}
-		if !assert.True(t, ks1.Add(key), `ks1.Add should succeed`) {
+		if !assert.NoError(t, ks1.AddKey(key), `ks1.Add should succeed`) {
 			return
 		}
 	}
@@ -928,7 +928,7 @@ func TestPublicKeyOf(t *testing.T) {
 				Key:           jwkKey,
 				PublicKeyType: key.PublicKeyType,
 			})
-			set.Add(jwkKey)
+			set.AddKey(jwkKey)
 			count++
 		}
 
@@ -938,7 +938,7 @@ func TestPublicKeyOf(t *testing.T) {
 		}
 
 		for i, key := range setKeys {
-			setKey, ok := newSet.Get(i)
+			setKey, ok := newSet.Key(i)
 			if !assert.True(t, ok, `element %d should be present`, i) {
 				return
 			}
@@ -1523,7 +1523,7 @@ func TestTypedFields(t *testing.T) {
 	t.Run("Set", func(t *testing.T) {
 		s := jwk.NewSet()
 		for _, key := range keys {
-			s.Add(key)
+			s.AddKey(key)
 		}
 
 		serialized, err := json.Marshal(s)
@@ -1542,7 +1542,7 @@ func TestTypedFields(t *testing.T) {
 					return
 				}
 
-				for iter := got.Iterate(ctx); iter.Next(ctx); {
+				for iter := got.Keys(ctx); iter.Next(ctx); {
 					pair := iter.Pair()
 					key, _ := pair.Value.(jwk.Key)
 					v, ok := key.Get("typed-field")
@@ -1576,7 +1576,7 @@ func TestGH412(t *testing.T) {
 
 		kid := "key-" + strconv.Itoa(i)
 		k.Set(jwk.KeyIDKey, kid)
-		base.Add(k)
+		base.AddKey(k)
 		kids[kid] = struct{}{}
 	}
 
@@ -1593,12 +1593,12 @@ func TestGH412(t *testing.T) {
 				return
 			}
 
-			k, ok := set.Get(idx)
+			k, ok := set.Key(idx)
 			if !assert.True(t, ok, `set.Get should succeed`) {
 				return
 			}
 
-			if !assert.True(t, set.Remove(k), `set.Remove should succeed`) {
+			if !assert.NoError(t, set.RemoveKey(k), `set.Remove should succeed`) {
 				return
 			}
 			t.Logf("deleted key %s", k.KeyID())
@@ -1616,7 +1616,7 @@ func TestGH412(t *testing.T) {
 			}
 
 			ctx := context.Background()
-			for iter := set.Iterate(ctx); iter.Next(ctx); {
+			for iter := set.Keys(ctx); iter.Next(ctx); {
 				pair := iter.Pair()
 				key := pair.Value.(jwk.Key)
 				if !assert.NotEqual(t, k.KeyID(), key.KeyID(), `key id should not match`) {
@@ -1641,7 +1641,7 @@ func TestGH491(t *testing.T) {
 	}
 
 	// there should be 2 keys , get the first key
-	k, _ := keys.Get(0)
+	k, _ := keys.Key(0)
 	ops := k.KeyOps()
 	if !assert.Equal(t, jwk.KeyOperationList{jwk.KeyOpDeriveKey}, ops, `k.KeyOps should match`) {
 		return
@@ -1677,17 +1677,17 @@ func TestSetWithPrivateParams(t *testing.T) {
 				return
 			}
 
-			v, ok := set.Field(`renewal_kid`)
-			if !assert.True(t, ok, `set.Field("renewal_kid") should return ok = true`) {
+			v, ok := set.Get(`renewal_kid`)
+			if !assert.True(t, ok, `set.Get("renewal_kid") should return ok = true`) {
 				return
 			}
 
-			if !assert.Equal(t, `foo`, v, `set.Field("renewal_kid") should return "foo"`) {
+			if !assert.Equal(t, `foo`, v, `set.Get("renewal_kid") should return "foo"`) {
 				return
 			}
 
-			key, ok := set.Get(0)
-			if !assert.True(t, ok, `set.Get(0) should return ok = true`) {
+			key, ok := set.Key(0)
+			if !assert.True(t, ok, `set.Key(0) should return ok = true`) {
 				return
 			}
 
@@ -1737,12 +1737,12 @@ func TestSetWithPrivateParams(t *testing.T) {
 				return
 			}
 
-			v, ok := set.Field(`renewal_kid`)
-			if !assert.True(t, ok, `set.Field("renewal_kid") should return ok = true`) {
+			v, ok := set.Get(`renewal_kid`)
+			if !assert.True(t, ok, `set.Get("renewal_kid") should return ok = true`) {
 				return
 			}
 
-			if !assert.Equal(t, `foo`, v, `set.Field("renewal_kid") should return "foo"`) {
+			if !assert.Equal(t, `foo`, v, `set.Get("renewal_kid") should return "foo"`) {
 				return
 			}
 		}
@@ -1768,7 +1768,7 @@ func TestSetWithPrivateParams(t *testing.T) {
 			return
 		}
 
-		v, ok := set.Field(`renewal_kid`)
+		v, ok := set.Get(`renewal_kid`)
 		if !assert.True(t, ok, `set.Get("renewal_kid") should succeed`) {
 			return
 		}
@@ -1811,9 +1811,9 @@ func TestFetch(t *testing.T) {
 		return
 	}
 	set := jwk.NewSet()
-	set.Add(k1)
-	set.Add(k2)
-	set.Add(k3)
+	set.AddKey(k1)
+	set.AddKey(k2)
+	set.AddKey(k3)
 
 	expected, err := json.MarshalIndent(set, "", "  ")
 	if !assert.NoError(t, err, `json.MarshalIndent should succeed`) {
diff --git a/jwk/refresh_test.go b/jwk/refresh_test.go
index fbf7ee41..66f17584 100644
--- a/jwk/refresh_test.go
+++ b/jwk/refresh_test.go
@@ -10,7 +10,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/lestrrat-go/iter/arrayiter"
 	"github.com/lestrrat-go/jwx/v2/internal/json"
 	"github.com/lestrrat-go/jwx/v2/internal/jwxtest"
 	"github.com/lestrrat-go/jwx/v2/jwk"
@@ -18,10 +17,10 @@ import (
 )
 
 //nolint:revive,golint
-func checkAccessCount(t *testing.T, ctx context.Context, src arrayiter.Source, expected ...int) bool {
+func checkAccessCount(t *testing.T, ctx context.Context, src jwk.Set, expected ...int) bool {
 	t.Helper()
 
-	iter := src.Iterate(ctx)
+	iter := src.Keys(ctx)
 	iter.Next(ctx)
 
 	key := iter.Pair().Value.(jwk.Key)
@@ -51,6 +50,75 @@ func checkAccessCount(t *testing.T, ctx context.Context, src arrayiter.Source, e
 func TestCache(t *testing.T) {
 	t.Parallel()
 
+	t.Run("CachedSet", func(t *testing.T) {
+		const numKeys = 3
+		t.Parallel()
+		ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+		defer cancel()
+
+		set := jwk.NewSet()
+		for i := 0; i < numKeys; i++ {
+			key, err := jwxtest.GenerateRsaJwk()
+			if !assert.NoError(t, err, `jwxtest.GenerateRsaJwk should succeed`) {
+				return
+			}
+			if !assert.NoError(t, set.AddKey(key), `set.AddKey should succeed`) {
+				return
+			}
+		}
+
+		srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			hdrs := w.Header()
+			hdrs.Set(`Content-Type`, `application/json`)
+			hdrs.Set(`Cache-Control`, `max-age=5`)
+
+			json.NewEncoder(w).Encode(set)
+		}))
+		defer srv.Close()
+
+		af := jwk.NewCache(ctx, jwk.WithRefreshWindow(time.Second))
+		if !assert.NoError(t, af.Register(srv.URL), `af.Register should succeed`) {
+			return
+		}
+
+		cached := jwk.NewCachedSet(af, srv.URL)
+		if !assert.Error(t, cached.Set("bogus", nil), `cached.Set should be an error`) {
+			return
+		}
+		if !assert.Error(t, cached.Remove("bogus"), `cached.Remove should be an error`) {
+			return
+		}
+		if !assert.Error(t, cached.AddKey(nil), `cached.AddKey should be an error`) {
+			return
+		}
+		if !assert.Error(t, cached.RemoveKey(nil), `cached.RemoveKey should be an error`) {
+			return
+		}
+		if !assert.Equal(t, set.Len(), cached.Len(), `value of Len() should be the same`) {
+			return
+		}
+
+		iter := set.Keys(ctx)
+		citer := cached.Keys(ctx)
+		for i := 0; i < numKeys; i++ {
+			k, err := set.Key(i)
+			ck, cerr := cached.Key(i)
+			if !assert.Equal(t, k, ck, `key %d should match`, i) {
+				return
+			}
+			if !assert.Equal(t, err, cerr, `error %d should match`, i) {
+				return
+			}
+
+			if !assert.Equal(t, iter.Next(ctx), citer.Next(ctx), `iter.Next should match`) {
+				return
+			}
+
+			if !assert.Equal(t, iter.Pair(), citer.Pair(), `iter.Pair should match`) {
+				return
+			}
+		}
+	})
 	t.Run("Specify explicit refresh interval", func(t *testing.T) {
 		t.Parallel()
 		ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
@@ -328,7 +396,7 @@ func TestErrorSink(t *testing.T) {
 		return
 	}
 	set := jwk.NewSet()
-	set.Add(k)
+	_ = set.AddKey(k)
 	testcases := []struct {
 		Name    string
 		Options func() []jwk.RegisterOption
@@ -420,7 +488,7 @@ func TestPostFetch(t *testing.T) {
 		if !assert.NoError(t, err, `jwk.FromRaw should succeed`) {
 			return
 		}
-		set.Add(key)
+		_ = set.AddKey(key)
 	}
 
 	testcases := []struct {
@@ -435,7 +503,7 @@ func TestPostFetch(t *testing.T) {
 			Name: "With PostFetch",
 			Options: []jwk.RegisterOption{jwk.WithPostFetcher(jwk.PostFetchFunc(func(_ string, set jwk.Set) (jwk.Set, error) {
 				for i := 0; i < set.Len(); i++ {
-					key, _ := set.Get(i)
+					key, _ := set.Key(i)
 					key.Set(jwk.KeyIDKey, fmt.Sprintf(`key-%d`, i))
 				}
 				return set, nil
@@ -467,7 +535,7 @@ func TestPostFetch(t *testing.T) {
 			}
 
 			for i := 0; i < set.Len(); i++ {
-				key, _ := set.Get(i)
+				key, _ := set.Key(i)
 				if tc.ExpectKid {
 					if !assert.NotEmpty(t, key.KeyID(), `key.KeyID should not be empty`) {
 						return
diff --git a/jwk/set_test.go b/jwk/set_test.go
index 14352745..dc2d1705 100644
--- a/jwk/set_test.go
+++ b/jwk/set_test.go
@@ -24,7 +24,7 @@ func TestSet(t *testing.T) {
 		if !assert.NoError(t, err, `key generation should succeed`) {
 			return
 		}
-		if !assert.True(t, set.Add(k), `set.Add should succeed`) {
+		if !assert.NoError(t, set.AddKey(k), `set.AddKey should succeed`) {
 			return
 		}
 		keys = append(keys, k)
@@ -41,7 +41,7 @@ func TestSet(t *testing.T) {
 	}
 
 	for _, k := range keys {
-		if !assert.True(t, set.Remove(k), `set.Remove should succeed`) {
+		if !assert.NoError(t, set.RemoveKey(k), `set.RemoveKey should succeed`) {
 			return
 		}
 	}
@@ -55,7 +55,7 @@ func TestSet(t *testing.T) {
 		if !assert.NoError(t, err, `key generation should succeed`) {
 			return
 		}
-		if !assert.True(t, set.Add(k), `set.Add should succeed`) {
+		if !assert.NoError(t, set.AddKey(k), `set.Add should succeed`) {
 			return
 		}
 	}
diff --git a/jws/jws_test.go b/jws/jws_test.go
index b51571c2..302e6994 100644
--- a/jws/jws_test.go
+++ b/jws/jws_test.go
@@ -1072,12 +1072,12 @@ func TestVerifySet(t *testing.T) {
 	makeSet := func(privkey jwk.Key) jwk.Set {
 		set := jwk.NewSet()
 		k1, _ := jwk.FromRaw([]byte("abracadavra"))
-		set.Add(k1)
+		set.AddKey(k1)
 		k2, _ := jwk.FromRaw([]byte("opensasame"))
-		set.Add(k2)
+		set.AddKey(k2)
 		pubkey, _ := jwk.PublicKeyOf(privkey)
 		pubkey.Set(jwk.AlgorithmKey, jwa.RS256)
-		set.Add(pubkey)
+		set.AddKey(pubkey)
 		return set
 	}
 
@@ -1460,7 +1460,7 @@ func TestJKU(t *testing.T) {
 		return
 	}
 	set := jwk.NewSet()
-	set.Add(pubkey)
+	set.AddKey(pubkey)
 	srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusOK)
 		json.NewEncoder(w).Encode(set)
@@ -1598,7 +1598,7 @@ func TestJKU(t *testing.T) {
 			if !assert.Equal(t, pubkey.KeyID(), key.KeyID(), `key ID should be populated`) {
 				return
 			}
-			set.Add(pubkey)
+			set.AddKey(pubkey)
 		}
 		srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			w.WriteHeader(http.StatusOK)
diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go
index b9ff3c32..c02d8f9c 100644
--- a/jwt/jwt_test.go
+++ b/jwt/jwt_test.go
@@ -280,30 +280,30 @@ func TestJWTParseVerify(t *testing.T) {
 							switch i {
 							case 0:
 								name = "Lone key"
-								set.Add(pubkey)
+								set.AddKey(pubkey)
 							case 1:
 								name = "Two keys, correct one at the end"
-								set.Add(dummyKey)
-								set.Add(pubkey)
+								set.AddKey(dummyKey)
+								set.AddKey(pubkey)
 							case 2:
 								name = "Two keys, correct one at the beginning"
-								set.Add(pubkey)
-								set.Add(dummyKey)
+								set.AddKey(pubkey)
+								set.AddKey(dummyKey)
 							case 3:
 								name = "Three keys, correct one at the end"
-								set.Add(dummyKey)
-								set.Add(dummyKey)
-								set.Add(pubkey)
+								set.AddKey(dummyKey)
+								set.AddKey(dummyKey)
+								set.AddKey(pubkey)
 							case 4:
 								name = "Three keys, correct one at the middle"
-								set.Add(dummyKey)
-								set.Add(pubkey)
-								set.Add(dummyKey)
+								set.AddKey(dummyKey)
+								set.AddKey(pubkey)
+								set.AddKey(dummyKey)
 							case 5:
 								name = "Three keys, correct one at the beginning"
-								set.Add(pubkey)
-								set.Add(dummyKey)
-								set.Add(dummyKey)
+								set.AddKey(pubkey)
+								set.AddKey(dummyKey)
+								set.AddKey(dummyKey)
 							}
 
 							t.Run(name, func(t *testing.T) {
@@ -356,7 +356,7 @@ func TestJWTParseVerify(t *testing.T) {
 			pubkey.Set(jwk.AlgorithmKey, jwa.HS256)
 			pubkey.Set(jwk.KeyIDKey, kid)
 			set := jwk.NewSet()
-			set.Add(pubkey)
+			set.AddKey(pubkey)
 
 			_, err = jwt.Parse(signed, jwt.WithKeySet(set, jws.WithInferAlgorithmFromKey(true), jws.WithUseDefault(true)))
 			if !assert.Error(t, err, `jwt.Parse should fail`) {
@@ -377,7 +377,7 @@ func TestJWTParseVerify(t *testing.T) {
 				t.Fatal("Failed to sign JWT")
 			}
 			set := jwk.NewSet()
-			set.Add(pubkey)
+			set.AddKey(pubkey)
 			t2, err := jwt.Parse(signedNoKid, jwt.WithKeySet(set, jws.WithUseDefault(true)))
 			if !assert.NoError(t, err, `jwt.Parse with key set should succeed`) {
 				return
@@ -404,8 +404,8 @@ func TestJWTParseVerify(t *testing.T) {
 				t.Fatal("Failed to sign JWT")
 			}
 			set := jwk.NewSet()
-			set.Add(pubkey1)
-			set.Add(pubkey2)
+			set.AddKey(pubkey1)
+			set.AddKey(pubkey2)
 			_, err = jwt.Parse(signedNoKid, jwt.WithKeySet(set, jws.WithUseDefault(true)))
 			if !assert.Error(t, err, `jwt.Parse should fail`) {
 				return
@@ -449,7 +449,7 @@ func TestJWTParseVerify(t *testing.T) {
 			pubkey.Set(jwk.KeyIDKey, kid)
 
 			set := jwk.NewSet()
-			set.Add(pubkey)
+			set.AddKey(pubkey)
 			_, err = jwt.Parse(signedButNot, jwt.WithKeySet(set))
 			// This should fail
 			if !assert.Error(t, err, `jwt.Parse with key set + alg=none should fail`) {
@@ -855,7 +855,7 @@ func TestParseRequest(t *testing.T) {
 			},
 			Parse: func(req *http.Request) (jwt.Token, error) {
 				set := jwk.NewSet()
-				set.Add(pubkey)
+				set.AddKey(pubkey)
 				return jwt.ParseRequest(req, jwt.WithKeySet(set))
 			},
 		},
@@ -1059,7 +1059,7 @@ func TestGH375(t *testing.T) {
 	verifyKey.Set(jwk.AlgorithmKey, jwa.RS256) // != jwa.RS512
 
 	ks := jwk.NewSet()
-	ks.Add(verifyKey)
+	ks.AddKey(verifyKey)
 
 	_, err = jwt.Parse(signed, jwt.WithKeySet(ks))
 	if !assert.Error(t, err, `jwt.Parse should fail`) {
@@ -1355,7 +1355,7 @@ func TestVerifyAuto(t *testing.T) {
 		return
 	}
 	set := jwk.NewSet()
-	set.Add(pubkey)
+	set.AddKey(pubkey)
 	backoffCount := 0
 	srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		switch r.URL.Query().Get(`type`) {