Repository: lestrrat-go/jwx
Pull request #636 (2022-03-10 05:56:03)

JWE docs and a bunch of fixes

codecov-commenter: # [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/636?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) Report
> Merging [#636](https://codecov.io/gh/lestrrat-go/jwx/pull/636?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (a7c5e2a) into [v2](https://codecov.io/gh/lestrrat-go/jwx/commit/fcd83068da0f88d7a87a95b33698b934c075db1e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (fcd8306) will **decrease** coverage by `0.12%`.
> The diff coverage is `56.09%`.

[![Impacted file tree graph](https://codecov.io/gh/lestrrat-go/jwx/pull/636/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/636?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)

```diff
@@            Coverage Diff             @@
##               v2     #636      +/-   ##
==========================================
- Coverage   72.27%   72.15%   -0.13%     
==========================================
  Files          88       87       -1     
  Lines        9768     9794      +26     
==========================================
+ Hits         7060     7067       +7     
- Misses       1901     1922      +21     
+ Partials      807      805       -2     
```


| [Impacted Files](https://codecov.io/gh/lestrrat-go/jwx/pull/636?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | Coverage Δ | |
|---|---|---|
| [internal/json/json.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvanNvbi9qc29uLmdv) | `66.66% <0.00%> (-6.67%)` | :arrow_down: |
| [jwe/options.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL29wdGlvbnMuZ28=) | `55.17% <0.00%> (ø)` | |
| [jwt/options.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-and0L29wdGlvbnMuZ28=) | `82.08% <ø> (-0.27%)` | :arrow_down: |
| [jwe/jwe.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL2p3ZS5nbw==) | `56.62% <41.66%> (+0.28%)` | :arrow_up: |
| [jwe/message.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL21lc3NhZ2UuZ28=) | `56.64% <62.29%> (-1.11%)` | :arrow_down: |
| [jwe/options\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL29wdGlvbnNfZ2VuLmdv) | `69.23% <100.00%> (ø)` | |
| [jws/options.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andzL29wdGlvbnMuZ28=) | `87.23% <100.00%> (+0.27%)` | :arrow_up: |
| [internal/json/stdlib.go](https://codecov.io/gh/lestrrat-go/jwx/pull/636/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvanNvbi9zdGRsaWIuZ28=) | | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/636?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/636?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Last update [020be00...a7c5e2a](https://codecov.io/gh/lestrrat-go/jwx/pull/636?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.v2 b/Changes.v2
index 7a20412f..cf13e023 100644
--- a/Changes.v2
+++ b/Changes.v2
@@ -24,6 +24,11 @@ Changes
     // example, but can be anything that fulfills jws.KeyProvider)
     jwt.Parse(serialized, jwt.WithKeyProvider(jws.KeyProviderFunc(...)))
 
+  tl;dr for JWK users:
+
+    // jwk.New() was confusing. Renamed to fit the actual implementation
+    key, err := jwk.FromRaw(rawKey)
+
   * Module now requires go 1.17
 
   * Use of github.com/pkg/errors is no more. If you were relying on bevaior
@@ -129,20 +134,22 @@ Changes
     verify the payload with.
 
   * [jws] jws.WithKeySet() has been added to specify a JWKS to be used for
-    verification.
+    verification. By default `kid` AND `alg` must match between the signature
+    and the key.
 
     The option can take further suboptions:
 
       jws.Parse(serialized,
         jws.WithKeySet(set,
-          // require `kid` to match
-          jws.WithRequireKid(true),
+          // by default `kid` is required. set false to disable.
+          jws.WithRequireKid(false),
           // optionally skip matching kid if there's exactly one key in set
           jws.WithUseDefault(true),
           // infer algorithm name from key type
           jws.WithInferAlgorithm(true),
         ),
       )
+
   * [jws] `jws.VerifuAuto` has been removed in favor of using
     `jws.WithVerifyAuto` option with `jws.Verify()`
 
diff --git a/docs/02-jws.md b/docs/02-jws.md
index b57d8247..1551eae0 100644
--- a/docs/02-jws.md
+++ b/docs/02-jws.md
@@ -3,13 +3,14 @@
 In this document we describe how to work with JWS using [`github.com/lestrrat-go/jwx/v2/jws`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws)
 
 * [Parsing](#parsing)
-  * [Parse a JWS encoded buffer into a jws.Message](#parse-a-jws-encoded-buffer-into-a-jwsmessage)
-  * [Parse a JWS encoded message stored in a file](#parse-a-jws-encoded-message-stored-in-a-file)
+  * [Parse a JWS message stored in memory](#parse-a-jws-message-stored-in-memory)
+  * [Parse a JWS message stored in a file](#parse-a-jws-message-stored-in-a-file)
 * [Signing](#signing)
   * [Generating a JWS message in compact serialization format](#generating-a-jws-message-in-compact-serialization-format)
   * [Generating a JWS message in JSON serialization format](#generating-a-jws-message-in-json-serialization-format)
   * [Generating a JWS message with detached payload](#generating-a-jws-message-with-detached-payload)
   * [Using cloud KMS services](#using-cloud-kms-services)
+  * [Including arbitrary headers](#including-arbitrary-headers)
 * [Verifying](#verifying)
   * [Verification using a single key](#verification-using-a-single-key)
   * [Verification using a JWKS](#verification-using-a-jwks)
@@ -29,9 +30,9 @@ verification. It is only provided such that it can be inspected -- there is no w
 to sign or verify using a parsed `jws.Message`. To do this, you would need to use
 `jws.Sign()` or `jws.Message()`.
 
-## Parse a JWS encoded buffer into a jws.Message
+## Parse a JWS message stored in memory
 
-You can parse a JWS buffer into a [`jws.Message`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws#Message) object. In this mode, there is no verification performed.
+You can parse a JWS message in memory stored as `[]byte` into a [`jws.Message`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws#Message) object. In this mode, there is no verification performed.
 
 <!-- INCLUDE(examples/jws_parse_example_test.go) -->
 ```go
@@ -72,7 +73,7 @@ func ExampleJWS_Parse() {
 source: [examples/jws_parse_example_test.go](https://github.com/lestrrat-go/jwx/blob/v2/examples/jws_parse_example_test.go)
 <!-- END INCLUDE -->
 
-## Parse a JWS encoded message stored in a file
+## Parse a JWS message stored in a file
 
 To parse a JWS stored in a file, use [`jws.ReadFile()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws#ReadFile). [`jws.ReadFile()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws#ReadFile) accepts the same options as [`jws.Parse()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws#Parse).
 
@@ -161,7 +162,7 @@ source: [examples/jws_sign_example_test.go](https://github.com/lestrrat-go/jwx/b
 
 Generally the only time you need to use a JSON serialization format is when you have to generate multiple signatures for a given payload using multiple signing algorithms and keys.
 
-When this need arises, use the [`jws.Sign()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws#Sign) function with the `jws.WithJSON()` option and multiple `jwt.WithKey()` options:
+When this need arises, use the [`jws.Sign()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v2/jws#Sign) function with the `jws.WithJSON()` option and multiple `jws.WithKey()` options:
 
 <!-- INCLUDE(examples/jws_sign_json_example_test.go) -->
 ```go
@@ -245,18 +246,16 @@ func ExampleJWS_SignDetachedPayload() {
 source: [examples/jws_sign_detached_payload_example_test.go](https://github.com/lestrrat-go/jwx/blob/v2/examples/jws_sign_detached_payload_example_test.go)
 <!-- END INCLUDE -->
 
-## Including Arbitrary Headers to Compact Serialization
+## Including arbitrary headers
 
 By default, only some header fields are included in the result from `jws.Sign()`.
-If you want to include more headers fields in the resulting JWS, you will have to provide them via the `jws.WithHeaders()` option
+If you want to include more header fields in the resulting JWS, you will have to provide them via the `jws.WithProtectedHeaders()` option.
 
-```go
-hdrs := jws.NewHeaders()
-hdrs.Set(`arbitrary-key`, `value`)
-signed, _ := jws.Sign(payload, jws.WithKey(alg, key, jws.WithProtected(hdrs)))
-```
+While `jws.WithPublicHeaders()` exists to keep API symmetric and complete, for most
+cases you only want to use `jws.WithProtectedHeaders()`
 
-Even if you need to pass in custom headers, normally you should only need to set the protected headers.
+<!-- INCLUDE(examples/jws_sign_with_headers_example_test.go) -->
+<!-- END INCLUDE -->
 
 ## Using cloud KMS services
 
@@ -310,12 +309,11 @@ source: [examples/jws_verify_with_key_example_test.go](https://github.com/lestrr
 
 ## Verification using a JWKS
 
-To verify a payload using JWKS, by default you will need your payload and JWKS to have matching `alg` field.
+To verify a payload using JWKS, by default you will need your payload and JWKS to have matching `kid` and `alg` fields.
 
 The `alg` field's requirement is the same for using a single key. See "[Why don't you automatically infer the algorithm for `jws.Verify`?](99-faq.md#why-dont-you-automatically-infer-the-algorithm-for-jwsverify-)"
 
-Note that unlike in JWT, the `kid` is not required by default, although you _can_ make it so
-by passing `jws.WithRequireKid(true)`.
+The `kid` field by default must match between the JWS signature and the key in JWKS. This can be explictly disabled by specifying `jws.WithRequireKid(false)` suboption when using the `jws.WithKeySet()` option (i.e.: `jws.WithKeySet(keyset, jws.WithRequireKid(false))`)
 
 For more discussion on why/how `alg`/`kid` values work, please read the [relevant section in the JWT documentation](01-jwt.md#parse-and-verify-a-jwt-with-a-key-set-matching-kid)
 
diff --git a/docs/README.md b/docs/README.md
index 39285a48..295252ea 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,7 +7,7 @@ If you would rather see code, try the [examples directory](../examples)
 * [Anatomy of JOSE](./00-anatomy.md)
 * [Working with JWT](./01-jwt.md)
 * [Working with JWS](./02-jws.md)
-* Working with JWE (TODO)
+* [Working with JWE](./03-jwe.md)
 * [Working with JWK](./04-jwk.md)
 * [Global Settings](./20-global-settings.md)
 * [Integrating With Frameworks](./21-frameworks.md)
diff --git a/internal/json/json.go b/internal/json/json.go
index bd6f36d5..466f8e79 100644
--- a/internal/json/json.go
+++ b/internal/json/json.go
@@ -3,6 +3,7 @@ package json
 import (
 	"bytes"
 	"fmt"
+	"os"
 	"sync"
 	"sync/atomic"
 
@@ -103,3 +104,9 @@ func NewDecodeCtx(r *Registry) DecodeCtx {
 func (dc *decodeCtx) Registry() *Registry {
 	return dc.registry
 }
+
+func Dump(v interface{}) {
+	enc := NewEncoder(os.Stdout)
+	enc.SetIndent("", "  ")
+	_ = enc.Encode(v)
+}
diff --git a/jwe/interface.go b/jwe/interface.go
index f11014f4..99ecb40a 100644
--- a/jwe/interface.go
+++ b/jwe/interface.go
@@ -28,6 +28,8 @@ type stdRecipient struct {
 	// At least one of the "header", "protected", and "unprotected" members
 	// MUST be present so that "alg" and "enc" Header Parameter values are
 	// conveyed for each recipient computation.
+	//
+	// JWX note: see Message.unprotectedHeaders
 	headers Headers
 
 	// encrypted_key
@@ -73,6 +75,12 @@ type Message struct {
 	//    value is non-empty; otherwise, it MUST be absent.  This value is
 	//    represented as an unencoded JSON object, rather than as a string.
 	//    These Header Parameter values are not integrity protected.
+	//
+	// JWX note: This field is NOT mutually exclusive with per-recipient
+	// headers within the implmentation because... it's too much work.
+	// It is _never_ populated (we don't provide a way to do this) upon encryption.
+	// When decrypting, if present its values are always merged with
+	// per-recipient header.
 	unprotectedHeaders Headers
 
 	// iv
diff --git a/jwe/jwe.go b/jwe/jwe.go
index c467962a..e80a123e 100644
--- a/jwe/jwe.go
+++ b/jwe/jwe.go
@@ -158,6 +158,9 @@ func (b *recipientBuilder) Build(cek []byte, calg jwa.ContentEncryptionAlgorithm
 	}
 
 	r := NewRecipient()
+	if hdrs := b.headers; hdrs != nil {
+		_ = r.SetHeaders(hdrs)
+	}
 
 	if err := r.Headers().Set(AlgorithmKey, b.alg); err != nil {
 		return nil, nil, fmt.Errorf(`failed to set header: %w`, err)
@@ -269,8 +272,13 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
 	}
 
 	// We need to have at least one builder
-	if len(builders) == 0 {
+	switch l := len(builders); {
+	case l == 0:
 		return nil, fmt.Errorf(`jwe.Encrypt: missing key encryption builders: use jwe.WithKey() to specify one`)
+	case l > 1:
+		if format == fmtCompact {
+			return nil, fmt.Errorf(`jwe.Encrypt: cannot use compact serialization when multiple recipients exist (check the number of WithKey() argument, or use WithJSON())`)
+		}
 	}
 
 	if useRawCEK {
@@ -348,13 +356,6 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
 
 	msg := NewMessage()
 
-	decodedAad, err := base64.Decode(aad)
-	if err != nil {
-		return nil, fmt.Errorf(`failed to decode base64: %w`, err)
-	}
-	if err := msg.Set(AuthenticatedDataKey, decodedAad); err != nil {
-		return nil, fmt.Errorf(`failed to set %s: %w`, AuthenticatedDataKey, err)
-	}
 	if err := msg.Set(CipherTextKey, ciphertext); err != nil {
 		return nil, fmt.Errorf(`failed to set %s: %w`, CipherTextKey, err)
 	}
@@ -498,6 +499,8 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
 }
 
 func (dctx *decryptCtx) try(ctx context.Context, recipient Recipient) ([]byte, error) {
+	var tried int
+	var lastError error
 	for i, kp := range dctx.keyProviders {
 		var sink algKeySink
 		if err := kp.FetchKeys(ctx, &sink, recipient, dctx.msg); err != nil {
@@ -505,6 +508,7 @@ func (dctx *decryptCtx) try(ctx context.Context, recipient Recipient) ([]byte, e
 		}
 
 		for _, pair := range sink.list {
+			tried++
 			// alg is converted here because pair.alg is of type jwa.KeyAlgorithm.
 			// this may seem ugly, but we're trying to avoid declaring separate
 			// structs for `alg jwa.KeyAlgorithm` and `alg jwa.SignatureAlgorithm`
@@ -514,13 +518,14 @@ func (dctx *decryptCtx) try(ctx context.Context, recipient Recipient) ([]byte, e
 
 			decrypted, err := dctx.decryptKey(ctx, alg, key, recipient)
 			if err != nil {
+				lastError = err
 				continue
 			}
 
 			return decrypted, nil
 		}
 	}
-	return nil, fmt.Errorf(`jwe.Encrypt: failed to match any of the keys with recipient`)
+	return nil, fmt.Errorf(`jwe.Decrypt: tried %d keys, but failed to match any of the keys with recipient (last error = %s)`, tried, lastError)
 }
 
 func (dctx *decryptCtx) decryptKey(ctx context.Context, alg jwa.KeyEncryptionAlgorithm, key interface{}, recipient Recipient) ([]byte, error) {
@@ -531,6 +536,7 @@ func (dctx *decryptCtx) decryptKey(ctx context.Context, alg jwa.KeyEncryptionAlg
 		}
 		key = raw
 	}
+
 	dec := newDecrypter(alg, dctx.msg.protectedHeaders.ContentEncryption(), key).
 		AuthenticatedData(dctx.aad).
 		ComputedAuthenticatedData(dctx.computedAad).
diff --git a/jwe/message.go b/jwe/message.go
index c9b86f6b..243ad59a 100644
--- a/jwe/message.go
+++ b/jwe/message.go
@@ -3,6 +3,9 @@ package jwe
 import (
 	"context"
 	"fmt"
+	"log"
+	"sort"
+	"strings"
 
 	"github.com/lestrrat-go/jwx/v2/internal/base64"
 	"github.com/lestrrat-go/jwx/v2/internal/json"
@@ -189,90 +192,121 @@ type messageMarshalProxy struct {
 	EncryptedKey string          `json:"encrypted_key,omitempty"`
 }
 
+type jsonKV struct {
+	Key   string
+	Value string
+}
+
 func (m *Message) MarshalJSON() ([]byte, error) {
 	// This is slightly convoluted, but we need to encode the
 	// protected headers, so we do it by hand
 	buf := pool.GetBytesBuffer()
 	defer pool.ReleaseBytesBuffer(buf)
 	enc := json.NewEncoder(buf)
-	fmt.Fprintf(buf, `{`)
 
-	var wrote bool
-	if aad := m.AuthenticatedData(); len(aad) > 0 {
-		wrote = true
-		fmt.Fprintf(buf, `%#v:`, AuthenticatedDataKey)
-		if err := enc.Encode(base64.EncodeToString(aad)); err != nil {
-			return nil, fmt.Errorf(`failed to encode %s field: %w`, AuthenticatedDataKey, err)
-		}
-	}
+	var fields []jsonKV
+
 	if cipherText := m.CipherText(); len(cipherText) > 0 {
-		if wrote {
-			fmt.Fprintf(buf, `,`)
-		}
-		wrote = true
-		fmt.Fprintf(buf, `%#v:`, CipherTextKey)
+		buf.Reset()
 		if err := enc.Encode(base64.EncodeToString(cipherText)); err != nil {
 			return nil, fmt.Errorf(`failed to encode %s field: %w`, CipherTextKey, err)
 		}
+		fields = append(fields, jsonKV{
+			Key:   CipherTextKey,
+			Value: strings.TrimSpace(buf.String()),
+		})
 	}
 
 	if iv := m.InitializationVector(); len(iv) > 0 {
-		if wrote {
-			fmt.Fprintf(buf, `,`)
-		}
-		wrote = true
-		fmt.Fprintf(buf, `%#v:`, InitializationVectorKey)
+		buf.Reset()
 		if err := enc.Encode(base64.EncodeToString(iv)); err != nil {
 			return nil, fmt.Errorf(`failed to encode %s field: %w`, InitializationVectorKey, err)
 		}
+		fields = append(fields, jsonKV{
+			Key:   InitializationVectorKey,
+			Value: strings.TrimSpace(buf.String()),
+		})
 	}
 
+	var encodedProtectedHeaders []byte
 	if h := m.ProtectedHeaders(); h != nil {
-		encodedHeaders, err := h.Encode()
+		v, err := h.Encode()
 		if err != nil {
 			return nil, fmt.Errorf(`failed to encode protected headers: %w`, err)
 		}
 
-		if len(encodedHeaders) > 2 {
-			if wrote {
-				fmt.Fprintf(buf, `,`)
-			}
-			wrote = true
-			fmt.Fprintf(buf, `%#v:%#v`, ProtectedHeadersKey, string(encodedHeaders))
+		encodedProtectedHeaders = v
+		if len(encodedProtectedHeaders) <= 2 { // '{}'
+			encodedProtectedHeaders = nil
+		} else {
+			fields = append(fields, jsonKV{
+				Key:   ProtectedHeadersKey,
+				Value: fmt.Sprintf("%q", encodedProtectedHeaders),
+			})
 		}
 	}
 
-	if recipients := m.Recipients(); len(recipients) > 0 {
-		if wrote {
-			fmt.Fprintf(buf, `,`)
+	if aad := m.AuthenticatedData(); len(aad) > 0 {
+		aad = base64.Encode(aad)
+		if encodedProtectedHeaders != nil {
+			tmp := append(encodedProtectedHeaders, '.')
+			aad = append(tmp, aad...)
+		}
+
+		buf.Reset()
+		if err := enc.Encode(aad); err != nil {
+			return nil, fmt.Errorf(`failed to encode %s field: %w`, AuthenticatedDataKey, err)
 		}
+		fields = append(fields, jsonKV{
+			Key:   AuthenticatedDataKey,
+			Value: strings.TrimSpace(buf.String()),
+		})
+	}
+
+	if recipients := m.Recipients(); len(recipients) > 0 {
 		if len(recipients) == 1 { // Use flattened format
-			fmt.Fprintf(buf, `%#v:`, HeadersKey)
-			if err := enc.Encode(recipients[0].Headers()); err != nil {
-				return nil, fmt.Errorf(`failed to encode %s field: %w`, HeadersKey, err)
+			if hdrs := recipients[0].Headers(); hdrs != nil {
+				buf.Reset()
+				if err := enc.Encode(hdrs); err != nil {
+					return nil, fmt.Errorf(`failed to encode %s field: %w`, HeadersKey, err)
+				}
+				fields = append(fields, jsonKV{
+					Key:   HeadersKey,
+					Value: strings.TrimSpace(buf.String()),
+				})
 			}
+
 			if ek := recipients[0].EncryptedKey(); len(ek) > 0 {
-				fmt.Fprintf(buf, `,%#v:`, EncryptedKeyKey)
+				buf.Reset()
 				if err := enc.Encode(base64.EncodeToString(ek)); err != nil {
 					return nil, fmt.Errorf(`failed to encode %s field: %w`, EncryptedKeyKey, err)
 				}
+				fields = append(fields, jsonKV{
+					Key:   EncryptedKeyKey,
+					Value: strings.TrimSpace(buf.String()),
+				})
 			}
 		} else {
-			fmt.Fprintf(buf, `%#v:`, RecipientsKey)
+			buf.Reset()
 			if err := enc.Encode(recipients); err != nil {
 				return nil, fmt.Errorf(`failed to encode %s field: %w`, RecipientsKey, err)
 			}
+			fields = append(fields, jsonKV{
+				Key:   RecipientsKey,
+				Value: strings.TrimSpace(buf.String()),
+			})
 		}
 	}
 
 	if tag := m.Tag(); len(tag) > 0 {
-		if wrote {
-			fmt.Fprintf(buf, `,`)
-		}
-		fmt.Fprintf(buf, `%#v:`, TagKey)
+		buf.Reset()
 		if err := enc.Encode(base64.EncodeToString(tag)); err != nil {
 			return nil, fmt.Errorf(`failed to encode %s field: %w`, TagKey, err)
 		}
+		fields = append(fields, jsonKV{
+			Key:   TagKey,
+			Value: strings.TrimSpace(buf.String()),
+		})
 	}
 
 	if h := m.UnprotectedHeaders(); h != nil {
@@ -282,10 +316,26 @@ func (m *Message) MarshalJSON() ([]byte, error) {
 		}
 
 		if len(unprotected) > 2 {
-			fmt.Fprintf(buf, `,%#v:%#v`, UnprotectedHeadersKey, string(unprotected))
+			fields = append(fields, jsonKV{
+				Key:   UnprotectedHeadersKey,
+				Value: fmt.Sprintf("%q", unprotected),
+			})
+		}
+	}
+
+	sort.Slice(fields, func(i, j int) bool {
+		return fields[i].Key < fields[j].Key
+	})
+	buf.Reset()
+	fmt.Fprintf(buf, `{`)
+	for i, kv := range fields {
+		if i > 0 {
+			fmt.Fprintf(buf, `,`)
 		}
+		fmt.Fprintf(buf, `%q:%s`, kv.Key, kv.Value)
 	}
 	fmt.Fprintf(buf, `}`)
+	log.Printf("%s", buf.String())
 
 	ret := make([]byte, buf.Len())
 	copy(ret, buf.Bytes())
diff --git a/jwe/options.go b/jwe/options.go
index ce2972b2..25a9f273 100644
--- a/jwe/options.go
+++ b/jwe/options.go
@@ -10,6 +10,8 @@ import (
 
 // Specify contents of the protected header. Some fields such as
 // "enc" and "zip" will be overwritten when encryption is performed.
+//
+// There is no equivalent for unprotected headers in this implementation
 func WithProtectedHeaders(h Headers) EncryptOption {
 	cloned, _ := h.Clone(context.Background())
 	return &encryptOption{option.New(identProtectedHeaders{}, cloned)}
@@ -32,8 +34,10 @@ type withKeySuboption struct {
 
 func (*withKeySuboption) withKeySuboption() {}
 
-func WithRecipientHeaders(hdr Headers) WithKeySuboption {
-	return &withKeySuboption{option.New(identRecipientHeaders{}, hdr)}
+// WithPerRecipientHeaders is used to pass header values for each recipient.
+// Note that these headers are by definition _unprotected_.
+func WithPerRecipientHeaders(hdr Headers) WithKeySuboption {
+	return &withKeySuboption{option.New(identPerRecipientHeaders{}, hdr)}
 }
 
 // WithKey is used to pass a static algorithm/key pair to either `jwe.Encrypt()` or `jwe.Decrypt()`.
@@ -42,13 +46,16 @@ func WithRecipientHeaders(hdr Headers) WithKeySuboption {
 // It is of type `jwa.KeyAlgorithm` but in reality you can only pass `jwa.SignatureAlgorithm`
 // types. It is this way so that the value in `(jwk.Key).Algorithm()` can be directly
 // passed to the option. If you specify other algorithm types such as `jwa.ContentEncryptionAlgorithm`,
-// then you will get an error when `jws.Sign()` or `jws.Verify()` is executed.
+// then you will get an error when `jwe.Encrypt()` or `jwe.Decrypt()` is executed.
+//
+// Unlike `jwe.WithKeySet()`, the `kid` field does not need to match for the key
+// to be tried.
 func WithKey(alg jwa.KeyAlgorithm, key interface{}, options ...WithKeySuboption) EncryptDecryptOption {
 	var hdr Headers
 	for _, option := range options {
 		//nolint:forcetypeassert
 		switch option.Ident() {
-		case identRecipientHeaders{}:
+		case identPerRecipientHeaders{}:
 			hdr = option.Value().(Headers)
 		}
 	}
@@ -61,7 +68,7 @@ func WithKey(alg jwa.KeyAlgorithm, key interface{}, options ...WithKeySuboption)
 }
 
 func WithKeySet(set jwk.Set, options ...WithKeySetSuboption) DecryptOption {
-	var requireKid bool
+	requireKid := true
 	for _, option := range options {
 		//nolint:forcetypeassert
 		switch option.Ident() {
diff --git a/jwe/options.yaml b/jwe/options.yaml
index c59a50e1..ea552f4e 100644
--- a/jwe/options.yaml
+++ b/jwe/options.yaml
@@ -30,7 +30,7 @@ options:
     skip_option: true
   - ident: ProtectedHeaders
     skip_option: true
-  - ident: RecipientHeaders
+  - ident: PerRecipientHeaders
     skip_option: true
   - ident: KeyProvider
     interface: DecryptOption
diff --git a/jwe/options_gen.go b/jwe/options_gen.go
index 454e317a..9d724b2b 100644
--- a/jwe/options_gen.go
+++ b/jwe/options_gen.go
@@ -90,9 +90,9 @@ type identKey struct{}
 type identKeyProvider struct{}
 type identMergeProtectedHeaders struct{}
 type identMessage struct{}
+type identPerRecipientHeaders struct{}
 type identPretty struct{}
 type identProtectedHeaders struct{}
-type identRecipientHeaders struct{}
 type identRequireKid struct{}
 type identSerialization struct{}
 
@@ -120,6 +120,10 @@ func (identMessage) String() string {
 	return "WithMessage"
 }
 
+func (identPerRecipientHeaders) String() string {
+	return "WithPerRecipientHeaders"
+}
+
 func (identPretty) String() string {
 	return "WithPretty"
 }
@@ -128,10 +132,6 @@ func (identProtectedHeaders) String() string {
 	return "WithProtectedHeaders"
 }
 
-func (identRecipientHeaders) String() string {
-	return "WithRecipientHeaders"
-}
-
 func (identRequireKid) String() string {
 	return "WithRequireKid"
 }
diff --git a/jws/options.go b/jws/options.go
index 017dbdfb..dff9f308 100644
--- a/jws/options.go
+++ b/jws/options.go
@@ -111,7 +111,8 @@ func WithKey(alg jwa.KeyAlgorithm, key interface{}, options ...WithKeySuboption)
 }
 
 func WithKeySet(set jwk.Set, options ...WithKeySetSuboption) VerifyOption {
-	var requireKid, useDefault, inferAlgorithm bool
+	requireKid := true
+	var useDefault, inferAlgorithm bool
 	for _, option := range options {
 		//nolint:forcetypeassert
 		switch option.Ident() {
diff --git a/jwt/options.go b/jwt/options.go
index 7e8f6449..59617d7d 100644
--- a/jwt/options.go
+++ b/jwt/options.go
@@ -136,10 +136,9 @@ type withKeySet struct {
 // WithKeySet forces the Parse method to verify the JWT message
 // using one of the keys in the given key set.
 //
-// Unlike the equivalent option in `jws` (`jws.WithKeySet`), the `jwt`
-// version by default requires that key IDs (`kid`) in the JWS message
-// and the JWK in the given `jwk.Set` match in order for the verification
-// to proceed.
+// Key IDs (`kid`) in the JWS message and the JWK in the given `jwk.Set`
+// must match in order for the key to be a candidate to be used for
+// verification.
 //
 // This is for security reasons. If you must disable it, you can do so by
 // specifying `jws.WithRequireKid(false)` in the suboptions. But we don't
@@ -159,7 +158,6 @@ type withKeySet struct {
 // If you have only one key in the set, and are sure you want to
 // use that key, you can use the `jwt.WithDefaultKey` option.
 func WithKeySet(set jwk.Set, options ...interface{}) ParseOption {
-	options = append(append([]interface{}(nil), jws.WithRequireKid(true)), options...)
 	return &parseOption{option.New(identKeySet{}, &withKeySet{
 		set:     set,
 		options: options,


Test patch:
diff --git a/examples/jwe_decrypt_with_key_example_test.go b/examples/jwe_decrypt_with_key_example_test.go
new file mode 100644
index 00000000..7db6231b
--- /dev/null
+++ b/examples/jwe_decrypt_with_key_example_test.go
@@ -0,0 +1,33 @@
+package examples_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwe"
+)
+
+func ExampleJWE_VerifyWithKey() {
+	privkey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		fmt.Printf("failed to create private key: %s\n", err)
+		return
+	}
+	const payload = "Lorem ipsum"
+	encrypted, err := jwe.Encrypt([]byte(payload), jwe.WithKey(jwa.RSA_OAEP, privkey.PublicKey))
+	if err != nil {
+		fmt.Printf("failed to sign payload: %s\n", err)
+		return
+	}
+
+	decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, privkey))
+	if err != nil {
+		fmt.Printf("failed to sign payload: %s\n", err)
+		return
+	}
+	fmt.Printf("%s\n", decrypted)
+	// OUTPUT:
+	// Lorem ipsum
+}
diff --git a/examples/jwe_decrypt_with_keyset_example_test.go b/examples/jwe_decrypt_with_keyset_example_test.go
new file mode 100644
index 00000000..be8f8cd3
--- /dev/null
+++ b/examples/jwe_decrypt_with_keyset_example_test.go
@@ -0,0 +1,45 @@
+package examples_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwe"
+	"github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+func ExampleJWE_VerifyWithJWKSet() {
+	privkey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		fmt.Printf("failed to create private key: %s\n", err)
+		return
+	}
+	const payload = "Lorem ipsum"
+	encrypted, err := jwe.Encrypt([]byte(payload), jwe.WithKey(jwa.RSA_OAEP, privkey.PublicKey))
+	if err != nil {
+		fmt.Printf("failed to sign payload: %s\n", err)
+		return
+	}
+
+	// Create a JWK Set
+	set := jwk.NewSet()
+	// Add some bogus keys
+	k1, _ := jwk.FromRaw([]byte("abracadavra"))
+	set.Add(k1)
+	k2, _ := jwk.FromRaw([]byte("opensasame"))
+	set.Add(k2)
+	// Add the real thing
+	k3, _ := jwk.FromRaw(privkey)
+	k3.Set(jwk.AlgorithmKey, jwa.RSA_OAEP)
+	set.Add(k3)
+
+	// Up to this point, you probably will replace with a simple jwk.Fetch()
+
+	if _, err := jwe.Decrypt(encrypted, jwe.WithKeySet(set, jwe.WithRequireKid(false))); err != nil {
+		fmt.Printf("Failed to decrypt using jwk.Set: %s", err)
+	}
+
+	// OUTPUT:
+}
diff --git a/examples/jwe_encrypt_json_example_test.go b/examples/jwe_encrypt_json_example_test.go
new file mode 100644
index 00000000..3c348a58
--- /dev/null
+++ b/examples/jwe_encrypt_json_example_test.go
@@ -0,0 +1,97 @@
+package examples_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwe"
+	"github.com/lestrrat-go/jwx/v2/jwk"
+)
+
+func ExampleJWE_EncryptJSON() {
+	rawprivkey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		fmt.Printf("failed to create raw private key: %s\n", err)
+		return
+	}
+	privkey, err := jwk.FromRaw(rawprivkey)
+	if err != nil {
+		fmt.Printf("failed to create private key: %s\n", err)
+		return
+	}
+
+	pubkey, err := privkey.PublicKey()
+	if err != nil {
+		fmt.Printf("failed to create public key:%s\n", err)
+		return
+	}
+
+	const payload = `Lorem ipsum`
+	encrypted, err := jwe.Encrypt([]byte(payload), jwe.WithJSON(), jwe.WithKey(jwa.RSA_OAEP, pubkey))
+	if err != nil {
+		fmt.Printf("failed to encrypt payload: %s\n", err)
+		return
+	}
+
+	decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, privkey))
+	if err != nil {
+		fmt.Printf("failed to decrypt payload: %s\n", err)
+		return
+	}
+	fmt.Printf("%s\n", decrypted)
+	// OUTPUT:
+	// Lorem ipsum
+}
+
+//func ExampleJWE_EncryptJSON() {
+//	var privkeys []jwk.Key
+//	var pubkeys []jwk.Key
+//
+//	for i := 0; i < 3; i++ {
+//		rawprivkey, err := rsa.GenerateKey(rand.Reader, 2048)
+//		if err != nil {
+//			fmt.Printf("failed to create raw private key: %s\n", err)
+//			return
+//		}
+//		privkey, err := jwk.FromRaw(rawprivkey)
+//		if err != nil {
+//			fmt.Printf("failed to create private key: %s\n", err)
+//			return
+//		}
+//		privkeys = append(privkeys, privkey)
+//
+//		pubkey, err := privkey.PublicKey()
+//		if err != nil {
+//			fmt.Printf("failed to create public key:%s\n", err)
+//			return
+//		}
+//		pubkeys = append(pubkeys, pubkey)
+//	}
+//
+//	options := []jwe.EncryptOption{jwe.WithJSON()}
+//	for _, key := range pubkeys {
+//		options = append(options, jwe.WithKey(jwa.RSA_OAEP, key))
+//		break
+//	}
+//
+//	const payload = `Lorem ipsum`
+//	encrypted, err := jwe.Encrypt([]byte(payload), options...)
+//	if err != nil {
+//		fmt.Printf("failed to encrypt payload: %s\n", err)
+//		return
+//	}
+//
+//	for i, key := range privkeys {
+//		log.Printf("trying key %d", i)
+//		decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, key))
+//		if err != nil {
+//			fmt.Printf("failed to decrypt payload: %s\n", err)
+//			return
+//		}
+//		fmt.Printf("%s\n", decrypted)
+//		break
+//	}
+//	// OUTPUT:
+//}
diff --git a/examples/jwe_encrypt_with_headers_example_test.go b/examples/jwe_encrypt_with_headers_example_test.go
new file mode 100644
index 00000000..bbddb2e0
--- /dev/null
+++ b/examples/jwe_encrypt_with_headers_example_test.go
@@ -0,0 +1,50 @@
+package examples_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+	"os"
+
+	"github.com/lestrrat-go/jwx/v2/internal/json"
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwe"
+)
+
+func ExampleJWE_SignWithHeaders() {
+	privkey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		fmt.Printf("failed to create private key: %s\n", err)
+		return
+	}
+	const payload = "Lorem ipsum"
+
+	hdrs := jwe.NewHeaders()
+	hdrs.Set(`x-example`, true)
+	encrypted, err := jwe.Encrypt([]byte(payload), jwe.WithKey(jwa.RSA_OAEP, privkey.PublicKey, jwe.WithPerRecipientHeaders(hdrs)))
+	if err != nil {
+		fmt.Printf("failed to encrypt payload: %s\n", err)
+		return
+	}
+
+	msg, err := jwe.Parse(encrypted)
+	if err != nil {
+		fmt.Printf("failed to parse message: %s\n", err)
+		return
+	}
+
+	// NOTE: This is a bit tricky. Even though we specified a per-recipient
+	// header when executing jwe.Encrypt, the headers end up being in the
+	// global protected headers section. This is... by the books. JWE
+	// in Compact serialization asks us to shove the per-recipient
+	// headers in the protected header section, because there is nowhere
+	// else to store this information.
+	//
+	// If this were a full JWE JSON message, you might have to juggle
+	// between the global protected headers, global unprotected headers,
+	// and per-recipient unprotected headers
+	json.NewEncoder(os.Stdout).Encode(msg.ProtectedHeaders())
+
+	// OUTPUT:
+	// {"alg":"RSA-OAEP","enc":"A256GCM","x-example":true}
+}
diff --git a/examples/jwk_auto_refresh_example_test.go b/examples/jwk_auto_refresh_example_test.go
index 28c8bbfe..e9c78647 100644
--- a/examples/jwk_auto_refresh_example_test.go
+++ b/examples/jwk_auto_refresh_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"context"
diff --git a/examples/jwk_example_test.go b/examples/jwk_example_test.go
index ab5e7dc0..409075da 100644
--- a/examples/jwk_example_test.go
+++ b/examples/jwk_example_test.go
@@ -7,7 +7,6 @@ import (
 	"crypto/elliptic"
 	"fmt"
 	"log"
-	"time"
 
 	"github.com/lestrrat-go/jwx/v2/internal/json"
 	"github.com/lestrrat-go/jwx/v2/jwk"
@@ -108,51 +107,3 @@ func ExampleJWK_MarshalJSON() {
 	//   "y": "7fVF7b6J_6_g6Wu9RuJw8geWxEi5ja9Gp2TSdELm5u2E-M7IF-bsxqcdOj3n1n7N"
 	// }
 }
-
-func ExampleJWK_AutoRefresh() {
-	ctx, cancel := context.WithCancel(context.Background())
-	defer cancel()
-
-	const googleCerts = `https://www.googleapis.com/oauth2/v3/certs`
-	ar := jwk.NewAutoRefresh(ctx)
-
-	// Tell *jwk.AutoRefresh that we only want to refresh this JWKS
-	// when it needs to (based on Cache-Control or Expires header from
-	// the HTTP response). If the calculated minimum refresh interval is less
-	// than 15 minutes, don't go refreshing any earlier than 15 minutes.
-	ar.Configure(googleCerts, jwk.WithMinRefreshInterval(15*time.Minute))
-
-	// Refresh the JWKS once before getting into the main loop.
-	// This allows you to check if the JWKS is available before we start
-	// a long-running program
-	_, err := ar.Refresh(ctx, googleCerts)
-	if err != nil {
-		fmt.Printf("failed to refresh google JWKS: %s\n", err)
-		return
-	}
-
-	// Pretend that this is your program's main loop
-MAIN:
-	for {
-		select {
-		case <-ctx.Done():
-			break MAIN
-		default:
-		}
-		keyset, err := ar.Fetch(ctx, googleCerts)
-		if err != nil {
-			fmt.Printf("failed to fetch google JWKS: %s\n", err)
-			return
-		}
-		_ = keyset
-
-		// Do interesting stuff with the keyset... but here, we just
-		// sleep for a bit
-		time.Sleep(time.Second)
-
-		// Because we're a dummy program, we just cancel the loop now.
-		// If this were a real program, you prosumably loop forever
-		cancel()
-	}
-	// OUTPUT:
-}
diff --git a/examples/jwk_fetch_example_test.go b/examples/jwk_fetch_example_test.go
index dcd53eb2..9d9df740 100644
--- a/examples/jwk_fetch_example_test.go
+++ b/examples/jwk_fetch_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"context"
diff --git a/examples/jwk_from_raw_example_test.go b/examples/jwk_from_raw_example_test.go
index b90d528c..fe7179b2 100644
--- a/examples/jwk_from_raw_example_test.go
+++ b/examples/jwk_from_raw_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"crypto/ecdsa"
diff --git a/examples/jwk_parse_jwks_example_test.go b/examples/jwk_parse_jwks_example_test.go
index df666089..368d56f2 100644
--- a/examples/jwk_parse_jwks_example_test.go
+++ b/examples/jwk_parse_jwks_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"encoding/json"
diff --git a/examples/jwk_parse_key_example_test.go b/examples/jwk_parse_key_example_test.go
index ec63939e..dcabec16 100644
--- a/examples/jwk_parse_key_example_test.go
+++ b/examples/jwk_parse_key_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"encoding/json"
diff --git a/examples/jwk_parse_with_pem_example_test.go b/examples/jwk_parse_with_pem_example_test.go
index 489a6903..689a5268 100644
--- a/examples/jwk_parse_with_pem_example_test.go
+++ b/examples/jwk_parse_with_pem_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"fmt"
diff --git a/examples/jwk_readfile_with_pem_example_test.go b/examples/jwk_readfile_with_pem_example_test.go
index 1cdcc857..6061e30a 100644
--- a/examples/jwk_readfile_with_pem_example_test.go
+++ b/examples/jwk_readfile_with_pem_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"fmt"
diff --git a/examples/jws_sign_with_headers_example_test.go b/examples/jws_sign_with_headers_example_test.go
new file mode 100644
index 00000000..dec71236
--- /dev/null
+++ b/examples/jws_sign_with_headers_example_test.go
@@ -0,0 +1,28 @@
+package examples_test
+
+import (
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v2/jwa"
+	"github.com/lestrrat-go/jwx/v2/jwk"
+	"github.com/lestrrat-go/jwx/v2/jws"
+)
+
+func ExampleJWS_SignWithHeaders() {
+	key, err := jwk.FromRaw([]byte(`abracadavra`))
+	if err != nil {
+		fmt.Printf("failed to create key: %s\n", err)
+		return
+	}
+
+	hdrs := jws.NewHeaders()
+	hdrs.Set(`x-example`, true)
+	buf, err := jws.Sign([]byte("Lorem ipsum"), jws.WithKey(jwa.HS256, key, jws.WithProtectedHeaders(hdrs)))
+	if err != nil {
+		fmt.Printf("failed to sign payload: %s\n", err)
+		return
+	}
+	fmt.Printf("%s\n", buf)
+	// OUTPUT:
+	// eyJhbGciOiJIUzI1NiIsIngtZXhhbXBsZSI6dHJ1ZX0.TG9yZW0gaXBzdW0.G1_mZLeYsCNCpglWcdofgoU9HExBGEMW08qzvouAzBo
+}
diff --git a/examples/jws_verify_with_keyset_example_test.go b/examples/jws_verify_with_keyset_example_test.go
index c1cdd61b..29fc3891 100644
--- a/examples/jws_verify_with_keyset_example_test.go
+++ b/examples/jws_verify_with_keyset_example_test.go
@@ -1,4 +1,4 @@
-package examples
+package examples_test
 
 import (
 	"crypto/rand"
@@ -40,8 +40,8 @@ func ExampleJWS_VerifyWithJWKSet() {
 	// Up to this point, you probably will replace with a simple jwk.Fetch()
 
 	// Now verify using the set.
-	if _, err := jws.Verify(signed, jws.WithKeySet(set)); err != nil {
-		fmt.Printf("Failed to verify using jwk.Set!: %s", err)
+	if _, err := jws.Verify(signed, jws.WithKeySet(set, jws.WithRequireKid(false))); err != nil {
+		fmt.Printf("Failed to verify using jwk.Set: %s", err)
 	}
 
 	// OUTPUT:
diff --git a/jwe/options_gen_test.go b/jwe/options_gen_test.go
index 652161f6..2d608e43 100644
--- a/jwe/options_gen_test.go
+++ b/jwe/options_gen_test.go
@@ -15,9 +15,9 @@ func TestOptionIdent(t *testing.T) {
 	require.Equal(t, "WithKeyProvider", identKeyProvider{}.String())
 	require.Equal(t, "WithMergeProtectedHeaders", identMergeProtectedHeaders{}.String())
 	require.Equal(t, "WithMessage", identMessage{}.String())
+	require.Equal(t, "WithPerRecipientHeaders", identPerRecipientHeaders{}.String())
 	require.Equal(t, "WithPretty", identPretty{}.String())
 	require.Equal(t, "WithProtectedHeaders", identProtectedHeaders{}.String())
-	require.Equal(t, "WithRecipientHeaders", identRecipientHeaders{}.String())
 	require.Equal(t, "WithRequireKid", identRequireKid{}.String())
 	require.Equal(t, "WithCompact", identSerialization{}.String())
 }
diff --git a/jws/jws_test.go b/jws/jws_test.go
index a81ac956..e8739079 100644
--- a/jws/jws_test.go
+++ b/jws/jws_test.go
@@ -1111,7 +1111,7 @@ func TestVerifySet(t *testing.T) {
 				}
 
 				var used jwk.Key
-				verified, err := jws.Verify(signed, jws.WithKeySet(set), jws.WithKeyUsed(&used))
+				verified, err := jws.Verify(signed, jws.WithKeySet(set, jws.WithRequireKid(false)), jws.WithKeyUsed(&used))
 				if !assert.NoError(t, err, `jws.Verify should succeed`) {
 					return
 				}