Repository: lestrrat-go/jwx
Pull request #581 (2022-02-25 07:52:31)

Rework JWS/JWT Sign() method signatures
refs #579


Patch:
diff --git a/Changes.v2 b/Changes.v2
index 056e58e7..ed4e59a3 100644
--- a/Changes.v2
+++ b/Changes.v2
@@ -12,6 +12,17 @@ Changes
     https://github.com/lestrrat-go/jwx/blob/main/docs/99-faq.md#why-is-jwkkeyalgorithm-and-jwakeyalgorithm-so-confusing
     for more details
 
+  * [jws] `jws.Sign()` can now generate JWS messages in either compact or JSON
+    forms. By default, the compact form is used. JSON format can be
+    enabled by using the `jws.WithJSON` option.
+
+  * [jws] `jws.Sign()` can now accept multiple keys by passing multiple
+    `jws.WithKey()` options. This can be used with `jws.WithJSON` to
+    create JWS messages with multiple signatures.
+
+  * [jws] `jws.WithCompact` and `jws.WithJSON` options have been added
+    to control the signatur serialization format.
+
   * [jwk] jwk.SetFetcher and jwk.SetFetchFunc has been added.
     They represent something that can fetch a jwk.Set
 
@@ -36,7 +47,7 @@ Changes
   * [jws] jws.WithKeySet() has been added to specify a JWKS to be used for
     verification.
 
-    The option can take further options:
+    The option can take further suboptions:
 
       jws.Parse(serialized,
         jws.WithKeySet(set,
@@ -48,8 +59,9 @@ Changes
           jws.WithInferAlgorithm(true),
         ),
       )
-
-  * [jws] jws.WithVerifyAuto() has been added to enable verification
+  * [jws] `jws.VerifuAuto` has been removed in favor of using
+    `jws.WithVerifyAuto` option with `jws.Verify()`
+  * [jws] `jws.WithVerifyAuto` has been added to enable verification
     using `jku`.
 
     The first argument must be a jwk.SetFetcher object, but can be
@@ -84,3 +96,12 @@ Changes
 
     Also, this really... is not safe. You are using an unverified payload
     to verify the JWS.
+
+  * [jws] Remove `jws.WithPayloadSigner()`. This should be completely repleceable
+    using `jws.WithKey()`
+
+  * [jwt] `jwt.Sign()` has been changed so that it works more like the new `jws.Sign()`
+  * [jwt] `jwt.WithVerify()` has been renamed to `jwt.WithKey()`. The option can
+    be used for both signing and parsing.
+  * [jwt] `jwt.WithHeaders` and `jwt.WithJwsHeaders` have been removed.
+    You should be able to use the new `jwt.WithKey` option to pass headers
diff --git a/docs/01-jwt.md b/docs/01-jwt.md
index af1fdc64..7dbbdb72 100644
--- a/docs/01-jwt.md
+++ b/docs/01-jwt.md
@@ -191,8 +191,7 @@ use `jws.VerifyAuto()` instead of `jws.Verify()`:
 ```go
 token, _ := jwt.Parse(
   src,
-  jwt.WithVerifyAuto(true),
-  jwt.WithFetchWhitelist(...),
+  jwt.WithVerifyAuto(nil, jwt.WithFetchWhitelist(...)),
 )
 ```
 
@@ -264,11 +263,9 @@ The `jwt` package provides a convenience function `jwt.Sign()` to serialize a to
 token := jwt.New()
 token.Set(jwt.IssuerKey, `github.com/lestrrat-go/jwx`)
 
-serialized, err := jws.Sign(token, algorithm, key)
+serialized, err := jwt.Sign(token, jwt.WithKey(algorithm, key))
 ```
 
-There are some options available in the `jwt` package to modify the JWS behavior such as `jwt.WithJwsHeaders()` to specify extra protected headers to the JWS message, `jwt.WithVerifyAuto()` to invoke verification via `jku`, etc. Please read their documentation to understand how they work.
-
 If you need even further customization, consider using the `jws` package directly.
 
 ## Serialize using JWE and JWS
diff --git a/docs/02-jws.md b/docs/02-jws.md
index 349eb22f..3318456f 100644
--- a/docs/02-jws.md
+++ b/docs/02-jws.md
@@ -23,7 +23,7 @@ If you want to get the payload in the JWS message after it has been verified, us
 
 ```go
 var encoded = []byte{...}
-payload, _ := jws.Verify(encoded, alg, key)
+payload, _ := jws.Verify(encoded, jws.WithKey(alg, key))
 ```
 
 You must provide the algorithm and the public key to use for verification.
@@ -57,7 +57,7 @@ message, _ := jws.ReadFile(`message.jws`)
 To parse a JWS with detached payload, use the `jws.WithDetachedPayload()` option:
 
 ```go
-signed, _ := jws.Verify(signed, alg, key, jws.WithDetachedPayload(payload))
+signed, _ := jws.Verify(signed, jws.WithKey(alg, key), jws.WithDetachedPayload(payload))
 ```
 
 # Signing
@@ -67,23 +67,28 @@ signed, _ := jws.Verify(signed, alg, key, jws.WithDetachedPayload(payload))
 In most cases this is all you really need.
 
 ```go
-signed, _ := jws.Sign(payload, alg, key)
+signed, _ := jws.Sign(payload, jws.WithKey(alg, key))
 ```
 
 To sign a JWT, use [`jwt.Sign()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwt#Sign)
+Notice that in this case the `WithKey()` options is that of the `jwt` package.
 
 ```go
-signed, _ := jwt.Sign(token, alg, key)
+signed, _ := jwt.Sign(token, jwt.WithKey(alg, key))
 ```
 
 ## Generating a JWS message in JSON serialization format
 
 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.SignMulti()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jws#SignMulti) method.
+
+When this need arises, use the [`jws.Sign()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jws#Sign) function with the `jws.WithJSON()` option and multiple `jwt.WithKey()` options:
 
 ```go
-signer, _ := jws.NewSigner(alg)
-signed, _ := jws.SignMulti(payload, jws.WithSigner(signer, key, pubHeaders, protHeaders)
+signed, _ := jws.Sign(payload,
+  jws.WithJSON(),
+  jws.WithKey(alg1, key1),
+  jws.WithKey(alg2, key2),
+)
 ```
 
 ## Generating a JWS message with detached payload
@@ -91,7 +96,7 @@ signed, _ := jws.SignMulti(payload, jws.WithSigner(signer, key, pubHeaders, prot
 Use the `jws.WithDetachedPayload()` option to sign a detached payload:
 
 ```go
-signed, _ := jws.Sign(nil, alg, key, jws.WithDetachedPayload(payload))
+signed, _ := jws.Sign(nil, jws.WithKey(alg, key), jws.WithDetachedPayload(payload))
 ```
 
 ## Including Arbitrary Headers to Compact Serialization
@@ -102,9 +107,11 @@ If you want to include more headers fields in the resulting JWS, you will have t
 ```go
 hdrs := jws.NewHeaders()
 hdrs.Set(`arbitrary-key`, `value`)
-signed, _ := jws.Sign(payload, alg, key, jws.WithHEaders(hdrs))
+signed, _ := jws.Sign(payload, jws.WithKey(alg, key, jws.WithProtected(hdrs)))
 ```
 
+Even if you need to pass in custom headers, normally you should only need to set the protected headers.
+
 ## Using cloud KMS services
 
 If you want to use cloud KMSes such as AWS KMS to sign and verify payloads, look for an object that implements
@@ -120,7 +127,7 @@ Simply use `jws.Verify()`. It will automatically do the right thing whether it's
 form or JSON form.
 
 ```go
-payload, _ := jws.Verify(data, alg, key)
+payload, _ := jws.Verify(data, jws.WithKey(alg, key))
 ```
 
 The `alg` must be explicitly specified. See "[Why don't you automatically infer the algorithm for `jws.Verify`?](99-faq.md#why-dont-you-automatically-infer-the-algorithm-for-jwsverify-)"
@@ -128,7 +135,7 @@ The `alg` must be explicitly specified. See "[Why don't you automatically infer
 To verify a JWS message with detached payload, use the `jws.WithDetachedPayload()` option:
 
 ```go
-_, err := jws.Verify(data, alg, key, jws.WithDetachedPayload(payload))
+_, err := jws.Verify(data, jws.WithKey(alg, key), jws.WithDetachedPayload(payload))
 ```
 
 ## Verification using `jku`
diff --git a/jws/jws.go b/jws/jws.go
index 4e2f7423..7369b256 100644
--- a/jws/jws.go
+++ b/jws/jws.go
@@ -108,76 +108,80 @@ var muSigner = &sync.Mutex{}
 // If you want to use a detached payload, use `jws.WithDetachedPayload()` as
 // one of the options. When you use this option, you must always set the
 // first parameter (`payload`) to `nil`, or the function will return an error
-func Sign(payload []byte, alg jwa.KeyAlgorithm, key interface{}, options ...SignOption) ([]byte, error) {
-	salg, ok := alg.(jwa.SignatureAlgorithm)
-	if !ok {
-		return nil, errors.Errorf(`expected alg to be jwa.SignatureAlgorithm, but got %T`, alg)
-	}
-
-	var hdrs Headers
-	var detached bool
-	for _, o := range options {
-		//nolint:forcetypeassert
-		switch o.Ident() {
-		case identHeaders{}:
-			hdrs = o.Value().(Headers)
-		case identDetachedPayload{}:
-			detached = true
-			if payload != nil {
-				return nil, errors.New(`jws.Sign: payload must be nil when jws.WithDetachedPayload() is specified`)
-			}
-			payload = o.Value().([]byte)
-		}
-	}
 
+func makeSigner(alg jwa.SignatureAlgorithm, key interface{}, public, protected Headers) (*payloadSigner, error) {
 	muSigner.Lock()
-	signer, ok := signers[salg]
+	signer, ok := signers[alg]
 	if !ok {
-		v, err := NewSigner(salg)
+		v, err := NewSigner(alg)
 		if err != nil {
 			muSigner.Unlock()
-			return nil, errors.Wrap(err, `failed to create signer`)
+			return nil, fmt.Errorf(`failed to create payload signer: %w`, err)
 		}
-		signers[salg] = v
+		signers[alg] = v
 		signer = v
 	}
 	muSigner.Unlock()
 
-	// XXX This is cheating. Ideally `detached` should be passed as a parameter
-	// but since this is an exported method, we can't change this without bumping
-	// major versions.... But we don't want to do that now, so we will cheat by
-	// making it part of the object
-	sig := &Signature{
-		protected: hdrs,
-		detached:  detached,
-	}
-	_, signature, err := sig.Sign(payload, signer, key)
-	if err != nil {
-		return nil, errors.Wrap(err, `failed sign payload`)
-	}
-
-	return signature, nil
+	return &payloadSigner{
+		signer:    signer,
+		key:       key,
+		public:    public,
+		protected: protected,
+	}, nil
 }
 
-// SignMulti accepts multiple signers via the options parameter,
-// and creates a JWS in JSON serialization format that contains
-// signatures from applying aforementioned signers.
-//
-// Use `jws.WithSigner(...)` to specify values how to generate
-// each signature in the `"signatures": [ ... ]` field.
-func SignMulti(payload []byte, options ...Option) ([]byte, error) {
+const (
+	fmtInvalid = iota
+	fmtCompact
+	fmtJSON
+	fmtMax
+)
+
+// silence linters
+var _ = fmtInvalid
+var _ = fmtMax
+
+func Sign(payload []byte, options ...SignOption) ([]byte, error) {
+	format := fmtCompact
 	var signers []*payloadSigner
+	var detached bool
 	for _, o := range options {
+		//nolint:forcetypeassert
 		switch o.Ident() {
-		case identPayloadSigner{}:
-			signers = append(signers, o.Value().(*payloadSigner))
+		case identSerialization{}:
+			format = o.Value().(int)
+		case identKey{}:
+			data := o.Value().(*withKey)
+
+			alg, ok := data.alg.(jwa.SignatureAlgorithm)
+			if !ok {
+				return nil, fmt.Errorf(`jws.Sign: expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, o.Value())
+			}
+			signer, err := makeSigner(alg, data.key, data.public, data.protected)
+			if err != nil {
+				return nil, fmt.Errorf(`jws.Sign: failed to create signer: %w`, err)
+			}
+			signers = append(signers, signer)
+		case identDetachedPayload{}:
+			detached = true
+			if payload != nil {
+				return nil, errors.New(`jws.Sign: payload must be nil when jws.WithDetachedPayload() is specified`)
+			}
+			payload = o.Value().([]byte)
 		}
 	}
 
-	if len(signers) == 0 {
-		return nil, errors.New(`no signers provided`)
+	lsigner := len(signers)
+	if lsigner == 0 {
+		return nil, fmt.Errorf(`jws.Sign: no signers available. Specify an alogirthm and akey using jws.WithKey()`)
+	}
+	if format == fmtCompact && lsigner != 1 {
+		return nil, fmt.Errorf(`jws.Sign: cannot have multiple specified for compact serialization`)
 	}
 
+	// Create a Message object with all the bits and bobs, and we'll
+	// serialize it in the end
 	var result Message
 
 	result.payload = payload
@@ -203,6 +207,8 @@ func SignMulti(payload []byte, options ...Option) ([]byte, error) {
 		sig := &Signature{
 			headers:   signer.PublicHeader(),
 			protected: protected,
+			// cheat. FIXXXXXXMEEEEEE
+			detached: detached,
 		}
 		_, _, err := sig.Sign(payload, signer.signer, signer.key)
 		if err != nil {
@@ -212,7 +218,47 @@ func SignMulti(payload []byte, options ...Option) ([]byte, error) {
 		result.signatures = append(result.signatures, sig)
 	}
 
-	return json.Marshal(result)
+	switch format {
+	case fmtJSON:
+		return json.Marshal(result)
+	case fmtCompact:
+		// Take the only signature object, and convert it into a Compact
+		// serialization format
+		s := result.signatures[0]
+		// XXX check if this is correct
+		hdrs := s.ProtectedHeaders()
+
+		hdrbuf, err := json.Marshal(hdrs)
+		if err != nil {
+			return nil, fmt.Errorf(`jws.Sign: failed to marshal headers: %w`, err)
+		}
+
+		buf := pool.GetBytesBuffer()
+		defer pool.ReleaseBytesBuffer(buf)
+
+		buf.WriteString(base64.EncodeToString(hdrbuf))
+		buf.WriteByte('.')
+
+		if !detached {
+			if getB64Value(hdrs) {
+				encoded := base64.EncodeToString(payload)
+				buf.WriteString(encoded)
+			} else {
+				if bytes.Contains(payload, []byte{'.'}) {
+					return nil, fmt.Errorf(`jws.Sign: payload must not contain a "."`)
+				}
+				buf.Write(payload)
+			}
+		}
+
+		buf.WriteByte('.')
+		buf.WriteString(base64.EncodeToString(s.signature))
+		ret := make([]byte, buf.Len())
+		copy(ret, buf.Bytes())
+		return ret, nil
+	default:
+		return nil, fmt.Errorf(`jws.Sign: invalid serialization format`)
+	}
 }
 
 var allowNoneWhitelist = jwk.WhitelistFunc(func(string) bool {
@@ -243,6 +289,16 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
 			dst = option.Value().(*Message)
 		case identDetachedPayload{}:
 			detachedPayload = option.Value().([]byte)
+		case identKey{}:
+			pair := option.Value().(*withKey)
+			alg, ok := pair.alg.(jwa.SignatureAlgorithm)
+			if !ok {
+				return nil, fmt.Errorf(`WithKey() option must be specified using jwa.SignatureAlgorithm (got %T)`, pair.alg)
+			}
+			keyProviders = append(keyProviders, &staticKeyProvider{
+				alg: alg,
+				key: pair.key,
+			})
 		case identKeyProvider{}:
 			keyProviders = append(keyProviders, option.Value().(KeyProvider))
 		case identKeyUsed{}:
@@ -309,7 +365,12 @@ func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
 			}
 
 			for _, pair := range sink.list {
-				alg, key := pair.alg, pair.key
+				// 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`
+				//nolint:forcetypeassert
+				alg := pair.alg.(jwa.SignatureAlgorithm)
+				key := pair.key
 				verifier, err := NewVerifier(alg)
 				if err != nil {
 					return nil, fmt.Errorf(`failed to create verifier for algorithm %q: %w`, alg, err)
diff --git a/jws/key_provider.go b/jws/key_provider.go
index 305fd1c3..4c4ba5e2 100644
--- a/jws/key_provider.go
+++ b/jws/key_provider.go
@@ -19,7 +19,7 @@ type KeySink interface {
 }
 
 type algKeyPair struct {
-	alg jwa.SignatureAlgorithm
+	alg jwa.KeyAlgorithm
 	key interface{}
 }
 
diff --git a/jws/option.go b/jws/option.go
index 0d29c867..a15153d0 100644
--- a/jws/option.go
+++ b/jws/option.go
@@ -10,19 +10,23 @@ import (
 
 type Option = option.Interface
 
-type identPayloadSigner struct{}
+type identContext struct{}
 type identDetachedPayload struct{}
-type identHeaders struct{}
 type identMessage struct{}
+type identHeaders struct{}
+type identKey struct{}
+type identKeyUsed struct{}
+type identKeyProvider struct{}
+type identSerialization struct{}
 
-func WithSigner(signer Signer, key interface{}, public, protected Headers) Option {
-	return option.New(identPayloadSigner{}, &payloadSigner{
-		signer:    signer,
-		key:       key,
-		protected: protected,
-		public:    public,
-	})
-}
+// WithKey options
+type identSignProtected struct{}
+type identSignPublic struct{}
+
+// WithKeySet options
+type identRequireKid struct{}
+type identUseDefault struct{}
+type identInferAlgorithm struct{}
 
 type SignOption interface {
 	Option
@@ -82,21 +86,102 @@ func WithDetachedPayload(v []byte) SignVerifyOption {
 	return &signVerifyOption{option.New(identDetachedPayload{}, v)}
 }
 
-type identKeyProvider struct{}
-type identRequireKid struct{}
-type identUseDefault struct{}
-type identInferAlgorithm struct{}
-type identKeyUsed struct{}
-type identContext struct{}
+// WithCompact specifies that the result of `jws.Sign()` is serialized in
+// compact format.
+//
+// By default `jws.Sign()` will opt to use compact format, so you usually
+// do not need to specify this option other than to be explicit about it
+func WithCompact() SignOption {
+	return &signOption{option.New(identSerialization{}, fmtCompact)}
+}
 
-func WithKey(alg jwa.SignatureAlgorithm, key interface{}) VerifyOption {
-	return WithKeyProvider(&staticKeyProvider{
-		alg: alg,
-		key: key,
-	})
+// WithJSON specifies that the result of `jws.Sign()` is serialized in
+// JSON format.
+//
+// If you pass multiple keys to `jws.Sign()`, it will fail unless
+// you also pass this option.
+func WithJSON() SignOption {
+	return &signOption{option.New(identSerialization{}, fmtJSON)}
+}
+
+// WithKeyOption describes option types that can be passed to the `jws.WithKey()`
+// option.
+type WithKeyOption interface {
+	Option
+	withKeyOption()
 }
 
-// WithKeySetOption is an option passed to the WithKeySet() option (recursion!)
+type withKeyOption struct {
+	Option
+}
+
+func (*withKeyOption) withKeyOption() {}
+
+// WithProtected is used with `jws.WithKey()` option when used with `jws.Sign()`
+// to specify a protected header to be attached to the JWS signature.
+//
+// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
+func WithProtected(hdr Headers) WithKeyOption {
+	return &withKeyOption{option.New(identSignProtected{}, hdr)}
+}
+
+// WithPublic is used with `jws.WithKey()` option when used with `jws.Sign()`
+// to specify a public header to be attached to the JWS signature.
+//
+// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
+//
+// `jws.Sign()` will result in an error if `jws.WithPublic()` is used
+// and the serialization format is compact serialization.
+func WithPublic(hdr Headers) WithKeyOption {
+	return &withKeyOption{option.New(identSignPublic{}, hdr)}
+}
+
+type withKey struct {
+	alg       jwa.KeyAlgorithm
+	key       interface{}
+	protected Headers
+	public    Headers
+}
+
+// This exist as escape hatches to modify the header values after the fact
+func (w *withKey) Protected(v Headers) Headers {
+	if w.protected == nil && v != nil {
+		w.protected = v
+	}
+	return w.protected
+}
+
+// WithKey is used to pass algorithm/key pair to either `jws.Sign()` or `jws.Verify()`.
+//
+// When used with `jws.Sign()`, additional properties `jws.WithProtected()` and
+// `jws.WithPublic()` to specify JWS headers that should be used whe signing().
+// These options are ignored whe the `jws.WithKey()` option is used with `jws.Verify()`.
+func WithKey(alg jwa.KeyAlgorithm, key interface{}, options ...WithKeyOption) SignVerifyOption {
+	// Implementation note: this option is shared between Sign() and
+	// Verify(). As such we don't create a KeyProvider here because
+	// if used in Sign() we would be doing something else.
+	var protected, public Headers
+	for _, option := range options {
+		//nolint:forcetypeassert
+		switch option.Ident() {
+		case identSignProtected{}:
+			protected = option.Value().(Headers)
+		case identSignPublic{}:
+			public = option.Value().(Headers)
+		}
+	}
+
+	return &signVerifyOption{
+		option.New(identKey{}, &withKey{
+			alg:       alg,
+			key:       key,
+			protected: protected,
+			public:    public,
+		}),
+	}
+}
+
+// WithKeySetOption is a suboption passed to the WithKeySet() option
 type WithKeySetOption interface {
 	Option
 	withKeySetOption()
@@ -108,6 +193,9 @@ type withKeySetOption struct {
 
 func (*withKeySetOption) withKeySetOption() {}
 
+// WithrequiredKid specifies whether the keys in the jwk.Set should
+// only be matched if the target JWS message's Key ID and the Key ID
+// in the given key matches.
 func WithRequireKid(v bool) WithKeySetOption {
 	return &withKeySetOption{option.New(identRequireKid{}, v)}
 }
@@ -161,6 +249,16 @@ func WithKeyProvider(kp KeyProvider) VerifyOption {
 	return &verifyOption{option.New(identKeyProvider{}, kp)}
 }
 
+// WithKeyUsed allows you to specify the `jws.Verify()` function to
+// return the key used for verification. This may be useful when
+// you specify multiple key sources or if you pass a `jwk.Set`
+// and you want to know which key was successful at verifying the
+// signature.
+//
+// `v` must be a pointer to an empty `interface{}`. Do not use
+// `jwk.Key` here unless you are 100% sure that all keys that you
+// have provided are instances of `jwk.Key` (remember that the
+// jwx API allows users to specify a raw key such as *rsa.PublicKey)
 func WithKeyUsed(v interface{}) VerifyOption {
 	return &verifyOption{option.New(identKeyUsed{}, v)}
 }
diff --git a/jwt/jwt.go b/jwt/jwt.go
index 170a4907..ee1dfea3 100644
--- a/jwt/jwt.go
+++ b/jwt/jwt.go
@@ -14,7 +14,6 @@ import (
 	"github.com/lestrrat-go/jwx/internal/json"
 	"github.com/lestrrat-go/jwx/jwe"
 
-	"github.com/lestrrat-go/jwx/jwa"
 	"github.com/lestrrat-go/jwx/jws"
 	"github.com/pkg/errors"
 )
@@ -332,12 +331,8 @@ OUTER:
 // The protected header will also automatically have the `typ` field set
 // to the literal value `JWT`, unless you provide a custom value for it
 // by jwt.WithHeaders option.
-func Sign(t Token, alg jwa.KeyAlgorithm, key interface{}, options ...SignOption) ([]byte, error) {
-	salg, ok := alg.(jwa.SignatureAlgorithm)
-	if !ok {
-		return nil, errors.Errorf(`jwt.Sign received %T for alg. Expected jwa.SignatureAlgorithm`, alg)
-	}
-	return NewSerializer().Sign(salg, key, options...).Serialize(t)
+func Sign(t Token, options ...SignOption) ([]byte, error) {
+	return NewSerializer().Sign(options...).Serialize(t)
 }
 
 // Equal compares two JWT tokens. Do not use `reflect.Equal` or the like
diff --git a/jwt/options.go b/jwt/options.go
index ae89a70e..199258b7 100644
--- a/jwt/options.go
+++ b/jwt/options.go
@@ -63,15 +63,20 @@ type SignOption interface {
 	signOption()
 }
 
-type signOption struct {
-	Option
+// SignParseOption describes an Option that can be passed to both
+// `jwt.Sign()` and `jwt.Parse()`
+type SignParseOption interface {
+	SignOption
+	ParseOption
 }
 
-func newSignOption(n interface{}, v interface{}) SignOption {
-	return &signOption{option.New(n, v)}
+type signParseOption struct {
+	Option
 }
 
-func (*signOption) signOption() {}
+func (*signParseOption) readFileOption() {}
+func (*signParseOption) parseOption()    {}
+func (*signParseOption) signOption()     {}
 
 // EncryptOption describes an Option that can be passed to Encrypt() or
 // (jwt.Serializer).Encrypt
@@ -116,8 +121,8 @@ type identDefault struct{}
 type identFlattenAudience struct{}
 type identInferAlgorithmFromKey struct{}
 type identJweHeaders struct{}
-type identJwsHeaders struct{}
 type identKey struct{}
+type identKeyProvider struct{}
 type identKeySet struct{}
 type identPedantic struct{}
 type identValidator struct{}
@@ -129,11 +134,12 @@ type identVerifyAuto struct{}
 type identHeaderKey struct{}
 type identFormKey struct{}
 
-// WithVerify forces the Parse method to verify the JWT message
-// using the given key. XXX Should have been named something like
-// WithVerificationKey
-func WithKey(alg jwa.SignatureAlgorithm, key interface{}) ParseOption {
-	return newParseOption(identKey{}, jws.WithKey(alg, key))
+// WithKey forces the Parse method to verify the JWT message
+// using the given key.
+//
+// This is a utility wrapper around `jws.WithKey()`
+func WithKey(alg jwa.KeyAlgorithm, key interface{}, options ...jws.WithKeyOption) SignParseOption {
+	return &signParseOption{option.New(identKey{}, jws.WithKey(alg, key, options...))}
 }
 
 // WithKeySet forces the Parse method to verify the JWT message
@@ -176,22 +182,6 @@ func WithToken(t Token) ParseOption {
 	return newParseOption(identToken{}, t)
 }
 
-// WithHeaders is passed to `jwt.Sign()` function, to allow specifying arbitrary
-// header values to be included in the header section of the jws message
-//
-// This option will be deprecated in the next major version. Use
-// jwt.WithJwsHeaders() instead.
-func WithHeaders(hdrs jws.Headers) SignOption {
-	return WithJwsHeaders(hdrs)
-}
-
-// WithJwsHeaders is passed to `jwt.Sign()` function or
-// "jwt.Serializer".Sign() method, to allow specifying arbitrary
-// header values to be included in the header section of the JWE message
-func WithJwsHeaders(hdrs jws.Headers) SignOption {
-	return newSignOption(identJwsHeaders{}, hdrs)
-}
-
 // WithJweHeaders is passed to "jwt.Serializer".Encrypt() method to allow
 // specifying arbitrary header values to be included in the protected header
 // of the JWE message
@@ -436,23 +426,32 @@ func WithContext(ctx context.Context) ValidateOption {
 	return newValidateOption(identContext{}, ctx)
 }
 
-// WithVerifyAuto specifies that the JWS verification should be performed
-// using `jws.VerifyAuto()`, which in turn attempts to verify the message
-// using values that are stored within the JWS message.
+// WithVerifyAuto specifies that the JWS verification should be attempted
+// by using the data available in the JWS message. Currently only verification
+// method available is to use the keys available in the JWKS URL pointed
+// in the `jku` field.
+//
+// The first argument should either be `nil`, or your custom jwk.SetFetcher
+// object, which tells how the JWKS should be fetched. Leaving it to
+// `nil` is equivalent to specifying that `jwk.Fetch` should be used.
+//
+// You can further pass options to customize the fetching behavior.
 //
-// Only passing this option to `jwt.Parse()` will not result in a successful
-// verification. Please make sure to carefully read the documentation in
-// `jws.VerifyAuto()`, and provide the necessary Whitelist object via
-// `jwt.WithFetchWhitelist()`
+// One notable difference in the option available via the `jwt`
+// package and the `jws.Verify()` or `jwk.Fetch()` functions is that
+// by default all fetching is disabled unless you explicitly whitelist urls.
+// Therefore, when you use this option you WILL have to specify at least
+// the `jwk.WithFetchWhitelist()` suboption: as:
 //
-// You might also consider using a backoff policy by using `jwt.WithFetchBackoff()`
-// to control the number of requests being made.
+//   jwt.Parse(data, jwt.WithVerifyAuto(nil, jwk.WithFetchWhitelist(...)))
+//
+// See the list of available options that you can pass to `jwk.Fetch()`
+// in the `jwk` package, including specifying a backoff policy by via
+// `jwt.WithFetchBackoff()`
 func WithVerifyAuto(f jwk.SetFetcher, options ...jwk.FetchOption) ParseOption {
 	return newParseOption(identVerifyAuto{}, jws.WithVerifyAuto(f, options...))
 }
 
-type identKeyProvider struct{}
-
 func WithKeyProvider(kp jws.KeyProvider) ParseOption {
 	return newParseOption(identKeyProvider{}, jws.WithKeyProvider(kp))
 }
diff --git a/jwt/serialize.go b/jwt/serialize.go
index a3665ce6..f81d5178 100644
--- a/jwt/serialize.go
+++ b/jwt/serialize.go
@@ -120,9 +120,7 @@ func setTypeOrCty(ctx SerializeCtx, hdrs genericHeader) error {
 }
 
 type jwsSerializer struct {
-	alg     jwa.SignatureAlgorithm
-	key     interface{}
-	options []SignOption
+	options []jws.SignOption
 }
 
 func (s *jwsSerializer) Serialize(ctx SerializeCtx, v interface{}) (interface{}, error) {
@@ -131,40 +129,42 @@ func (s *jwsSerializer) Serialize(ctx SerializeCtx, v interface{}) (interface{},
 		return nil, errors.New(`expected []byte as input`)
 	}
 
-	var hdrs jws.Headers
-	//nolint:forcetypeassert
 	for _, option := range s.options {
-		switch option.Ident() {
-		case identJwsHeaders{}:
-			hdrs = option.Value().(jws.Headers)
+		pc, ok := option.Value().(interface{ Protected(jws.Headers) jws.Headers })
+		if !ok {
+			continue
+		}
+		hdrs := pc.Protected(jws.NewHeaders())
+		if err := setTypeOrCty(ctx, hdrs); err != nil {
+			return nil, err // this is already wrapped
 		}
-	}
-
-	if hdrs == nil {
-		hdrs = jws.NewHeaders()
-	}
-
-	if err := setTypeOrCty(ctx, hdrs); err != nil {
-		return nil, err // this is already wrapped
-	}
 
-	// JWTs MUST NOT use b64 = false
-	// https://datatracker.ietf.org/doc/html/rfc7797#section-7
-	if v, ok := hdrs.Get("b64"); ok {
-		if bval, bok := v.(bool); bok {
-			if !bval { // b64 = false
-				return nil, errors.New(`b64 cannot be false for JWTs`)
+		// JWTs MUST NOT use b64 = false
+		// https://datatracker.ietf.org/doc/html/rfc7797#section-7
+		if v, ok := hdrs.Get("b64"); ok {
+			if bval, bok := v.(bool); bok {
+				if !bval { // b64 = false
+					return nil, errors.New(`b64 cannot be false for JWTs`)
+				}
 			}
 		}
 	}
-	return jws.Sign(payload, s.alg, s.key, jws.WithHeaders(hdrs))
+	return jws.Sign(payload, s.options...)
 }
 
-func (s *Serializer) Sign(alg jwa.SignatureAlgorithm, key interface{}, options ...SignOption) *Serializer {
+func (s *Serializer) Sign(options ...SignOption) *Serializer {
+	//nolint:prealloc
+	var soptions []jws.SignOption
+	for _, option := range options {
+		v, ok := option.Value().(jws.SignOption)
+		if !ok {
+			continue
+		}
+		soptions = append(soptions, v)
+	}
+
 	return s.Step(&jwsSerializer{
-		alg:     alg,
-		key:     key,
-		options: options,
+		options: soptions,
 	})
 }
 


Test patch:
diff --git a/bench/performance/jwt_benchmark_test.go b/bench/performance/jwt_benchmark_test.go
index d14527b4..79352c19 100644
--- a/bench/performance/jwt_benchmark_test.go
+++ b/bench/performance/jwt_benchmark_test.go
@@ -34,7 +34,7 @@ func BenchmarkJWT(b *testing.B) {
 				{
 					Name: "jwt.Sign",
 					Test: func(b *testing.B) error {
-						_, err := jwt.Sign(t1, alg, key)
+						_, err := jwt.Sign(t1, jwt.WithKey(alg, key))
 						return err
 					},
 				},
@@ -60,7 +60,7 @@ func BenchmarkJWT(b *testing.B) {
 	})
 
 	b.Run("Serialization", func(b *testing.B) {
-		signedBuf, err := jwt.Sign(t1, alg, key)
+		signedBuf, err := jwt.Sign(t1, jwt.WithKey(alg, key))
 		if err != nil {
 			b.Fatal(err)
 		}
diff --git a/examples/jws_example_test.go b/examples/jws_example_test.go
index 8f4006bb..9954abfd 100644
--- a/examples/jws_example_test.go
+++ b/examples/jws_example_test.go
@@ -23,7 +23,7 @@ func ExampleJWS_VerifyWithJWKSet() {
 		return
 	}
 	const payload = "Lorem ipsum"
-	signed, err := jws.Sign([]byte(payload), jwa.RS256, privkey)
+	signed, err := jws.Sign([]byte(payload), jws.WithKey(jwa.RS256, privkey))
 	if err != nil {
 		log.Printf("failed to sign payload: %s", err)
 		return
@@ -100,7 +100,7 @@ func ExampleJWS_ExternalSignerVerifier() {
 	}
 
 	const payload = "Lorem Ipsum"
-	signed, err := jws.Sign([]byte(payload), jwa.EdDSA, privkey)
+	signed, err := jws.Sign([]byte(payload), jws.WithKey(jwa.EdDSA, privkey))
 	if err != nil {
 		fmt.Printf(`failed to generate signed message: %s`, err)
 		return
@@ -127,7 +127,7 @@ func ExampleJWS_Sign() {
 		return
 	}
 
-	buf, err := jws.Sign([]byte("Lorem ipsum"), jwa.RS256, privkey)
+	buf, err := jws.Sign([]byte("Lorem ipsum"), jws.WithKey(jwa.RS256, privkey))
 	if err != nil {
 		log.Printf("failed to sign payload: %s", err)
 		return
diff --git a/examples/jwt_example_test.go b/examples/jwt_example_test.go
index 3c5d9fbc..d1e285a7 100644
--- a/examples/jwt_example_test.go
+++ b/examples/jwt_example_test.go
@@ -52,7 +52,7 @@ func ExampleJWT_ParseWithJWKS() {
 			token.Set(`foo`, `bar`)
 
 			// Sign the token and generate a payload
-			signed, err := jwt.Sign(token, jwa.RS256, realKey)
+			signed, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, realKey))
 			if err != nil {
 				fmt.Printf("failed to generate signed payload: %s\n", err)
 				return
@@ -128,7 +128,7 @@ func ExampleJWT_ParseWithJWKS() {
 			token.Set(`foo`, `bar`)
 
 			// Sign the token and generate a payload
-			signed, err := jwt.Sign(token, jwa.RS256, realKey)
+			signed, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, realKey))
 			if err != nil {
 				fmt.Printf("failed to generate signed payload: %s\n", err)
 				return
@@ -213,7 +213,7 @@ func ExampleJWT_Sign_WithImportJWK() {
 	}
 
 	// signed and return a jwt
-	signed, _ := jwt.Sign(t, jwa.RS256, jwkey)
+	signed, _ := jwt.Sign(t, jwt.WithKey(jwa.RS256, jwkey))
 
 	fmt.Println(string(signed[:]))
 
@@ -232,7 +232,7 @@ func ExampleJWT_Sign() {
 	{ // Create signed payload
 		token := jwt.New()
 		token.Set(`foo`, `bar`)
-		payload, err = jwt.Sign(token, jwa.RS256, privKey)
+		payload, err = jwt.Sign(token, jwt.WithKey(jwa.RS256, privKey))
 		if err != nil {
 			fmt.Printf("failed to generate signed payload: %s\n", err)
 			return
@@ -311,7 +311,7 @@ func ExampleJWT_SignToken() {
 
 	{
 		// Signing a token (using raw rsa.PrivateKey)
-		signed, err := jwt.Sign(t, jwa.RS256, key)
+		signed, err := jwt.Sign(t, jwt.WithKey(jwa.RS256, key))
 		if err != nil {
 			log.Printf("failed to sign token: %s", err)
 			return
@@ -327,7 +327,7 @@ func ExampleJWT_SignToken() {
 			return
 		}
 
-		signed, err := jwt.Sign(t, jwa.RS256, jwkKey)
+		signed, err := jwt.Sign(t, jwt.WithKey(jwa.RS256, jwkKey))
 		if err != nil {
 			log.Printf("failed to sign token: %s", err)
 			return
diff --git a/internal/jwxtest/jwxtest.go b/internal/jwxtest/jwxtest.go
index 644f698e..03a08835 100644
--- a/internal/jwxtest/jwxtest.go
+++ b/internal/jwxtest/jwxtest.go
@@ -350,7 +350,7 @@ func SignJwsFile(ctx context.Context, payload []byte, alg jwa.SignatureAlgorithm
 		return "", nil, errors.Wrapf(err, `failed to parse keyfile %s`, keyfile)
 	}
 
-	buf, err := jws.Sign(payload, alg, key)
+	buf, err := jws.Sign(payload, jws.WithKey(alg, key))
 	if err != nil {
 		return "", nil, errors.Wrap(err, `failed to sign payload`)
 	}
diff --git a/jws/jws_test.go b/jws/jws_test.go
index 4bf524d8..69e72373 100644
--- a/jws/jws_test.go
+++ b/jws/jws_test.go
@@ -244,7 +244,7 @@ func testRoundtrip(t *testing.T, payload []byte, alg jwa.SignatureAlgorithm, sig
 	for _, key := range signKeys {
 		key := key
 		t.Run(key.Name, func(t *testing.T) {
-			signed, err := jws.Sign(payload, alg, key.Key)
+			signed, err := jws.Sign(payload, jws.WithKey(alg, key.Key))
 			if !assert.NoError(t, err, "jws.Sign should succeed") {
 				return
 			}
@@ -380,16 +380,12 @@ func TestSignMulti2(t *testing.T) {
 	hmacAlgorithms := []jwa.SignatureAlgorithm{jwa.HS256, jwa.HS384, jwa.HS512}
 	var signed []byte
 	t.Run("Sign", func(t *testing.T) {
-		var options []jws.Option
+		var options = []jws.SignOption{jws.WithJSON()}
 		for _, alg := range hmacAlgorithms {
-			signer, err := jws.NewSigner(alg)
-			if !assert.NoError(t, err, `jws.NewSigner should succeed`) {
-				return
-			}
-			options = append(options, jws.WithSigner(signer, sharedkey, nil, nil))
+			options = append(options, jws.WithKey(alg, sharedkey)) // (signer, sharedkey, nil, nil))
 		}
 		var err error
-		signed, err = jws.SignMulti(payload, options...)
+		signed, err = jws.Sign(payload, options...)
 		if !assert.NoError(t, err, `jws.SignMulti should succeed`) {
 			return
 		}
@@ -519,7 +515,7 @@ func TestEncode(t *testing.T) {
 			return
 		}
 		var jwsCompact []byte
-		jwsCompact, err = jws.Sign(jwsPayload, alg, key)
+		jwsCompact, err = jws.Sign(jwsPayload, jws.WithKey(alg, key))
 		if err != nil {
 			t.Fatal("Failed to sign message")
 		}
@@ -1101,7 +1097,7 @@ func TestVerifySet(t *testing.T) {
 				}
 
 				set := makeSet(key)
-				signed, err := jws.Sign([]byte(payload), jwa.RS256, key)
+				signed, err := jws.Sign([]byte(payload), jws.WithKey(jwa.RS256, key))
 				if !assert.NoError(t, err, `jws.Sign should succeed`) {
 					return
 				}
@@ -1141,7 +1137,7 @@ func TestVerifySet(t *testing.T) {
 				key.Set(jwk.KeyIDKey, `mykey`)
 
 				set := makeSet(key)
-				signed, err := jws.Sign([]byte(payload), jwa.RS256, key)
+				signed, err := jws.Sign([]byte(payload), jws.WithKey(jwa.RS256, key))
 				if !assert.NoError(t, err, `jws.Sign should succeed`) {
 					return
 				}
@@ -1192,7 +1188,7 @@ func TestCustomField(t *testing.T) {
 	hdrs := jws.NewHeaders()
 	hdrs.Set(`x-birthday`, string(bdaybytes))
 
-	signed, err := jws.Sign([]byte(payload), jwa.RS256, privkey, jws.WithHeaders(hdrs))
+	signed, err := jws.Sign([]byte(payload), jws.WithKey(jwa.RS256, privkey, jws.WithProtected(hdrs)))
 	if !assert.NoError(t, err, `jws.Sign should succeed`) {
 		return
 	}
@@ -1241,7 +1237,7 @@ func TestWithMessage(t *testing.T) {
 	}
 
 	const text = "hello, world"
-	signed, err := jws.Sign([]byte(text), jwa.RS256, key)
+	signed, err := jws.Sign([]byte(text), jws.WithKey(jwa.RS256, key))
 	if !assert.NoError(t, err, `jws.Sign should succeed`) {
 		return
 	}
@@ -1285,7 +1281,7 @@ func TestRFC7797(t *testing.T) {
 		hdrs.Set("b64", false)
 		hdrs.Set("crit", "b64")
 
-		_, err := jws.Sign([]byte(payload), jwa.HS256, key, jws.WithHeaders(hdrs))
+		_, err := jws.Sign([]byte(payload), jws.WithKey(jwa.HS256, key, jws.WithProtected(hdrs)))
 		if !assert.Error(t, err, `jws.Sign should fail`) {
 			return
 		}
@@ -1296,7 +1292,7 @@ func TestRFC7797(t *testing.T) {
 		hdrs.Set("b64", false)
 		hdrs.Set("crit", "b64")
 
-		_, err := jws.Sign([]byte(payload), jwa.HS256, key, jws.WithHeaders(hdrs), jws.WithDetachedPayload([]byte(payload)))
+		_, err := jws.Sign([]byte(payload), jws.WithKey(jwa.HS256, key, jws.WithProtected(hdrs)), jws.WithDetachedPayload([]byte(payload)))
 		if !assert.Error(t, err, `jws.Sign should fail`) {
 			return
 		}
@@ -1326,7 +1322,7 @@ func TestRFC7797(t *testing.T) {
 				hdrs.Set("crit", "b64")
 
 				payload := tc.Payload
-				signOptions := []jws.SignOption{jws.WithHeaders(hdrs)}
+				signOptions := []jws.SignOption{jws.WithKey(jwa.HS256, key, jws.WithProtected(hdrs))}
 				var verifyOptions []jws.VerifyOption
 				verifyOptions = append(verifyOptions, jws.WithKey(jwa.HS256, key))
 				if tc.Detached {
@@ -1334,7 +1330,7 @@ func TestRFC7797(t *testing.T) {
 					verifyOptions = append(verifyOptions, jws.WithDetachedPayload(payload))
 					payload = nil
 				}
-				signed, err := jws.Sign(payload, jwa.HS256, key, signOptions...)
+				signed, err := jws.Sign(payload, signOptions...)
 				if !assert.NoError(t, err, `jws.Sign should succeed`) {
 					return
 				}
@@ -1677,7 +1673,7 @@ func TestJKU(t *testing.T) {
 					u += "?" + tc.Query
 				}
 				hdr.Set(jws.JWKSetURLKey, u)
-				signed, err := jws.Sign(payload, jwa.RS256, key, jws.WithHeaders(hdr))
+				signed, err := jws.Sign(payload, jws.WithKey(jwa.RS256, key, jws.WithProtected(hdr)))
 				if !assert.NoError(t, err, `jws.Sign should succeed`) {
 					return
 				}
@@ -1750,19 +1746,14 @@ func TestJKU(t *testing.T) {
 		defer srv.Close()
 
 		// Sign the payload using the three keys
-		var signOptions []jws.Option
+		var signOptions = []jws.SignOption{jws.WithJSON()}
 		for _, key := range keys {
-			signer, err := jws.NewSigner(jwa.RS256)
-			if !assert.NoError(t, err, `jws.NewSigner should succeed`) {
-				return
-			}
-
 			hdr := jws.NewHeaders()
 			hdr.Set(jws.JWKSetURLKey, srv.URL)
-			signOptions = append(signOptions, jws.WithSigner(signer, key, nil, hdr))
+			signOptions = append(signOptions, jws.WithKey(jwa.RS256, key, jws.WithProtected(hdr)))
 		}
 
-		signed, err := jws.SignMulti(payload, signOptions...)
+		signed, err := jws.Sign(payload, signOptions...)
 		if !assert.NoError(t, err, `jws.SignMulti should succeed`) {
 			return
 		}
diff --git a/jws/signer_test.go b/jws/signer_test.go
index 34426fa4..9e486ec0 100644
--- a/jws/signer_test.go
+++ b/jws/signer_test.go
@@ -14,14 +14,14 @@ func TestSign(t *testing.T) {
 	t.Parallel()
 	t.Run("Bad algorithm", func(t *testing.T) {
 		t.Parallel()
-		_, err := jws.Sign([]byte(nil), jwa.SignatureAlgorithm("FooBar"), nil)
+		_, err := jws.Sign([]byte(nil), jws.WithKey(jwa.SignatureAlgorithm("FooBar"), nil))
 		if !assert.Error(t, err, "Unknown algorithm should return error") {
 			return
 		}
 	})
 	t.Run("No private key", func(t *testing.T) {
 		t.Parallel()
-		_, err := jws.Sign([]byte{'a', 'b', 'c'}, jwa.RS256, nil)
+		_, err := jws.Sign([]byte{'a', 'b', 'c'}, jws.WithKey(jwa.RS256, nil))
 		if !assert.Error(t, err, "Sign with no private key should return error") {
 			return
 		}
@@ -74,24 +74,17 @@ func TestSignMulti(t *testing.T) {
 		return
 	}
 
-	s1, err := jws.NewSigner(jwa.RS256)
-	if !assert.NoError(t, err, "RSA Signer created") {
-		return
-	}
 	s1hdr := jws.NewHeaders()
 	s1hdr.Set(jws.KeyIDKey, "2010-12-29")
 
-	s2, err := jws.NewSigner(jwa.ES256)
-	if !assert.NoError(t, err, "DSA Signer created") {
-		return
-	}
 	s2hdr := jws.NewHeaders()
 	s2hdr.Set(jws.KeyIDKey, "e9bc097a-ce51-4036-9562-d2ade882db0d")
 
 	v := strings.Join([]string{`{"iss":"joe",`, ` "exp":1300819380,`, ` "http://example.com/is_root":true}`}, "\r\n")
-	m, err := jws.SignMulti([]byte(v),
-		jws.WithSigner(s1, rsakey, s1hdr, nil),
-		jws.WithSigner(s2, dsakey, s2hdr, nil),
+	m, err := jws.Sign([]byte(v),
+		jws.WithJSON(),
+		jws.WithKey(jwa.RS256, rsakey, jws.WithPublic(s1hdr)),
+		jws.WithKey(jwa.ES256, dsakey, jws.WithPublic(s2hdr)),
 	)
 	if !assert.NoError(t, err, "jws.SignMulti should succeed") {
 		return
diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go
index 7cd051a9..a03992f4 100644
--- a/jwt/jwt_test.go
+++ b/jwt/jwt_test.go
@@ -56,7 +56,7 @@ func TestJWTParse(t *testing.T) {
 		return
 	}
 	t1 := jwt.New()
-	signed, err := jwt.Sign(t1, alg, key)
+	signed, err := jwt.Sign(t1, jwt.WithKey(alg, key))
 	if !assert.NoError(t, err, `jwt.Sign should succeed`) {
 		return
 	}
@@ -250,7 +250,7 @@ func TestJWTParseVerify(t *testing.T) {
 						hdrs.Set(jws.KeyIDKey, kid)
 
 						t1 := jwt.New()
-						signed, err := jwt.Sign(t1, alg, key, jwt.WithHeaders(hdrs))
+						signed, err := jwt.Sign(t1, jwt.WithKey(alg, key, jws.WithProtected(hdrs)))
 						if !assert.NoError(t, err, "token.Sign should succeed") {
 							return
 						}
@@ -343,7 +343,7 @@ func TestJWTParseVerify(t *testing.T) {
 		hdrs := jws.NewHeaders()
 		hdrs.Set(jws.KeyIDKey, kid)
 		t1 := jwt.New()
-		signed, err := jwt.Sign(t1, alg, key, jwt.WithHeaders(hdrs))
+		signed, err := jwt.Sign(t1, jwt.WithKey(alg, key, jws.WithProtected(hdrs)))
 		if !assert.NoError(t, err, "token.Sign should succeed") {
 			return
 		}
@@ -374,7 +374,7 @@ func TestJWTParseVerify(t *testing.T) {
 
 			pubkey.Set(jwk.AlgorithmKey, alg)
 			pubkey.Set(jwk.KeyIDKey, kid)
-			signedNoKid, err := jwt.Sign(t1, alg, key)
+			signedNoKid, err := jwt.Sign(t1, jwt.WithKey(alg, key))
 			if err != nil {
 				t.Fatal("Failed to sign JWT")
 			}
@@ -401,7 +401,7 @@ func TestJWTParseVerify(t *testing.T) {
 
 			pubkey1.Set(jwk.KeyIDKey, kid)
 			pubkey2.Set(jwk.KeyIDKey, "test-jwt-parse-verify-kid-2")
-			signedNoKid, err := jwt.Sign(t1, alg, key)
+			signedNoKid, err := jwt.Sign(t1, jwt.WithKey(alg, key))
 			if err != nil {
 				t.Fatal("Failed to sign JWT")
 			}
@@ -589,7 +589,7 @@ func TestGH52(t *testing.T) {
 			defer wg.Done()
 			tok := jwt.New()
 
-			s, err := jwt.Sign(tok, jwa.ES256, priv)
+			s, err := jwt.Sign(tok, jwt.WithKey(jwa.ES256, priv))
 			if !assert.NoError(t, err) {
 				return
 			}
@@ -630,7 +630,7 @@ func TestSignErrors(t *testing.T) {
 	}
 
 	tok := jwt.New()
-	_, err = jwt.Sign(tok, jwa.SignatureAlgorithm("BOGUS"), priv)
+	_, err = jwt.Sign(tok, jwt.WithKey(jwa.SignatureAlgorithm("BOGUS"), priv))
 	if !assert.Error(t, err) {
 		return
 	}
@@ -639,7 +639,7 @@ func TestSignErrors(t *testing.T) {
 		return
 	}
 
-	_, err = jwt.Sign(tok, jwa.ES256, nil)
+	_, err = jwt.Sign(tok, jwt.WithKey(jwa.ES256, nil))
 	if !assert.Error(t, err) {
 		return
 	}
@@ -662,7 +662,7 @@ func TestSignJWK(t *testing.T) {
 	key.Set(jwk.AlgorithmKey, jwa.RS256)
 
 	tok := jwt.New()
-	signed, err := jwt.Sign(tok, key.Algorithm(), key)
+	signed, err := jwt.Sign(tok, jwt.WithKey(key.Algorithm(), key))
 	assert.Nil(t, err)
 
 	header, err := jws.ParseString(string(signed))
@@ -690,7 +690,7 @@ func TestSignTyp(t *testing.T) {
 	t.Run(`"typ" header parameter should be set to JWT by default`, func(t *testing.T) {
 		t.Parallel()
 		t1 := jwt.New()
-		signed, err := jwt.Sign(t1, jwa.RS256, key)
+		signed, err := jwt.Sign(t1, jwt.WithKey(jwa.RS256, key))
 		if !assert.NoError(t, err) {
 			return
 		}
@@ -708,7 +708,7 @@ func TestSignTyp(t *testing.T) {
 		t1 := jwt.New()
 		hdrs := jws.NewHeaders()
 		hdrs.Set(`typ`, `custom-typ`)
-		signed, err := jwt.Sign(t1, jwa.RS256, key, jwt.WithHeaders(hdrs))
+		signed, err := jwt.Sign(t1, jwt.WithKey(jwa.RS256, key, jws.WithProtected(hdrs)))
 		if !assert.NoError(t, err) {
 			return
 		}
@@ -805,7 +805,7 @@ func TestParseRequest(t *testing.T) {
 	tok.Set(jwt.IssuerKey, u)
 	tok.Set(jwt.IssuedAtKey, time.Now().Round(0))
 
-	signed, _ := jwt.Sign(tok, jwa.ES256, privkey)
+	signed, _ := jwt.Sign(tok, jwt.WithKey(jwa.ES256, privkey))
 
 	testcases := []struct {
 		Request func() *http.Request
@@ -1048,7 +1048,7 @@ func TestGH375(t *testing.T) {
 	}
 
 	signAlg := jwa.RS512
-	signed, err := jwt.Sign(token, signAlg, key)
+	signed, err := jwt.Sign(token, jwt.WithKey(signAlg, key))
 	if !assert.NoError(t, err, `jwt.Sign should succeed`) {
 		return
 	}
@@ -1125,8 +1125,8 @@ func TestJWTParseWithTypedClaim(t *testing.T) {
 		if !assert.NoError(t, token.Set("typed-claim", expected), `expected.Set should succeed`) {
 			return
 		}
-		v, err := jwt.Sign(token, jwa.RS256, key)
-		if !assert.NoError(t, err, `jws.Sign should succeed`) {
+		v, err := jwt.Sign(token, jwt.WithKey(jwa.RS256, key))
+		if !assert.NoError(t, err, `jwt.Sign should succeed`) {
 			return
 		}
 		signed = v
@@ -1287,7 +1287,7 @@ func TestNested(t *testing.T) {
 	}
 
 	serialized, err := jwt.NewSerializer().
-		Sign(jwa.RS256, key).
+		Sign(jwt.WithKey(jwa.RS256, key)).
 		Encrypt(jwa.RSA_OAEP, key.PublicKey, jwa.A256GCM, jwa.NoCompress).
 		Serialize(token)
 
@@ -1346,7 +1346,7 @@ func TestRFC7797(t *testing.T) {
 	token := jwt.New()
 	token.Set(jwt.AudienceKey, `foo`)
 
-	_, err = jwt.Sign(token, jwa.RS256, key, jwt.WithJwsHeaders(hdrs))
+	_, err = jwt.Sign(token, jwt.WithKey(jwa.RS256, key, jws.WithProtected(hdrs)))
 	if !assert.Error(t, err, `jwt.Sign should fail`) {
 		return
 	}
@@ -1362,7 +1362,7 @@ func TestGH430(t *testing.T) {
 	}
 
 	key := []byte("secret")
-	signed, err := jwt.Sign(t1, jwa.HS256, key)
+	signed, err := jwt.Sign(t1, jwt.WithKey(jwa.HS256, key))
 	if !assert.NoError(t, err, `jwt.Sign should succeed`) {
 		return
 	}
@@ -1464,7 +1464,7 @@ func TestVerifyAuto(t *testing.T) {
 	hdrs := jws.NewHeaders()
 	hdrs.Set(jws.JWKSetURLKey, srv.URL)
 
-	signed, err := jwt.Sign(tok, jwa.RS256, key, jwt.WithHeaders(hdrs))
+	signed, err := jwt.Sign(tok, jwt.WithKey(jwa.RS256, key, jws.WithProtected(hdrs)))
 	if !assert.NoError(t, err, `jwt.Sign() should succeed`) {
 		return
 	}
diff --git a/jwt/openid/openid_test.go b/jwt/openid/openid_test.go
index 81c32b94..5ccbb706 100644
--- a/jwt/openid/openid_test.go
+++ b/jwt/openid/openid_test.go
@@ -450,7 +450,7 @@ func TestOpenIDClaims(t *testing.T) {
 			if !assert.NoError(t, err, `rsa.GeneraKey should succeed`) {
 				return
 			}
-			signed, err := jwt.Sign(token, alg, key)
+			signed, err := jwt.Sign(token, jwt.WithKey(alg, key))
 			if !assert.NoError(t, err, `jwt.Sign should succeed`) {
 				return
 			}