Repository: lestrrat-go/jwx

Issue:
[question] Removal of (jwt.Token).PrivateClaims interface method
Hello,

First of all, thanks for all the work put into this lib. It's a great piece of work 💪

❓ My question is why, in https://github.com/lestrrat-go/jwx/pull/999, was the `(jwt.Token).PrivateClaims` method remove from the interface? The implementation still exists, but this change is not mentioned in the release notes/Changes-v3 file.

I do not know how to re-create this logic from the user side. Should we call `.Keys()` and manually remove the standard keys? 🤔 

Would you either have some guidelines on this, or would it be possible to add it back?

Thank you!

---

**Contribution Guidelines**

Before filing an issue, please read the contents of [CONTRIBUTING.md](https://github.com/lestrrat-go/jwx/blob/v2/.github/CONTRIBUTING.md), and follow its instructions.


Hints from issue comments:
@lestrrat: The intention is that you as the user would know which private claims you want, and therefore you should be able to filter the claims that you want, rather than removing standard claims. In your use case, do you not have the list of private claims that you want?@David-Wobrock: Another service is generating the JWT with private claims, which can evolve, and that we want to retrieve.

So, it's possible for us to list these non-standard keys explicitly, but it would be less extensible and require more manual maintenance.
Basically, having `PrivateClaims` was highly convenient for us 😄 @lestrrat: okay. I don't intend to bring back `PrivateClaims` b/c it created two routes to access arbitrary fields, making the access to this data harder to control, but I suppose I could enunerate the keys of standard claims in a list so it's easier to filter them out.@David-Wobrock: That would be great indeed 🙌 @lestrrat: Check out the new `TokenFilter` type, which you can get from `jwt.StandardClaimsFilter` in branch https://github.com/lestrrat-go/jwx/tree/gh-1375

```go
filter := jwt.StandardClaimsFilter()
stdClaimsOnly, _ := filter.Filter(token)
privateClaimsOnly, _ := filter.Reject(token)
```

Note: This is a PoC, and I'll probably need to implement equivalent constructs for other types before merging.@lestrrat: @David-Wobrock Oops, sorry, it seems like I forgot to add the crucial files to git. And they are sitting in my home computer right. I'll get them back up when I can.@lestrrat: @David-Wobrock Sorry for the wait, the PoC is now available in https://github.com/lestrrat-go/jwx/tree/gh-1375@David-Wobrock: > [@David-Wobrock](https://github.com/David-Wobrock) Sorry for the wait, the PoC is now available in https://github.com/lestrrat-go/jwx/tree/gh-1375

That looks great for our need 🙏 @lestrrat: @David-Wobrock Okay, #1377 is ready for review. Please actually try it out, and if it works for you, I'll merge it.@lestrrat: @David-Wobrock FIY this will probably get merged, but please note that if you don't provide feedback soon it's going to go in as is. And once it's merged you won't have much say about it until (possibly) a major release is done (it's going to labeled experimental, but regardless, it's going to be tougher to change once it's merged)@David-Wobrock: Hey @lestrrat 

Sorry for the delay, and thanks for your responsiveness.

I tested with the branch, and it works like a charm 👌 
I ended up with something like:
```
import "github.com/lestrrat-go/jwx/v3/jwt"

func ExtractPrivateClaims(token jwt.Token) (map[string]interface{}, error) {
	filter := jwt.StandardClaimsFilter()
	tokenWithoutStdClaims, _ := filter.Reject(token)
	claims := map[string]interface{}{}
	for _, k := range tokenWithoutStdClaims.Keys() {
		var val interface{}
		err := tokenWithoutStdClaims.Get(k, &val)
		if err != nil {
			return nil, err
		}
		claims[k] = val
	}

	return claims, nil
}
```
which seems to do the trick 👍 @lestrrat: @David-Wobrock Oh wait wait, that's not my intention. The code in #1377 should allow you to just write this:

```go
tokenWithOnlyCustomClaims, _ := jwt.StandardClaimsFilter().Filter(token)
```
 
And when I do some how merge #1385, you would be able to do

```go
mapOfCustomClaims, _ := jwx.AsMap(tokenWithOnlyCustomClaims)
```@David-Wobrock: ```
jwt.StandardClaimsFilter().Filter(token)
```
definitely only keeps the standard claims 😅 
We have to use `Reject` to remove the std claims.

Looking forward to `jwx.AsMap` 🤩 @lestrrat: sorry, was the other way around@lestrrat: @David-Wobrock v3.0.2 should have everything you need. enjoy.@David-Wobrock: That's perfect, thanks a lot @lestrrat 

Solution patch:
diff --git a/.aspect/bazelrc/convenience.bazelrc b/.aspect/bazelrc/convenience.bazelrc
index cfa8d5a1..3122d327 100644
--- a/.aspect/bazelrc/convenience.bazelrc
+++ b/.aspect/bazelrc/convenience.bazelrc
@@ -4,7 +4,7 @@ build --keep_going
 test  --keep_going
 
 # Output test errors to stderr so users don't have to `cat` or open test failure log files when test
-# fail. This makes the log noisier in exchange for reducing the time-to-feedback on test failures for
+# fail. This makes the log noiser in exchange for reducing the time-to-feedback on test failures for
 # users.
 # Docs: https://bazel.build/docs/user-manual#test-output
 test --test_output=errors
diff --git a/Changes b/Changes
index 15f2b7c7..6a7ca74b 100644
--- a/Changes
+++ b/Changes
@@ -5,6 +5,11 @@ v3 has many incompatibilities with v2. To see the full list of differences betwe
 v2 and v3, please read the Changes-v3.md file (https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes-v3.md)
 
 v3.0.2 UNRELEASED
+  * [jwt] (EXPERIMENTAL) Added token filtering functionality through the TokenFilter interface. 
+  * [jwt/openid] (EXPERIMENTAL) Added StandardClaimsFilter() for filtering standard OpenID claims.
+  * [jws] (EXPERIMENTAL) Added header filtering functionality through the HeaderFilter interface.
+  * [jwe] (EXPERIMENTAL) Added header filtering functionality through the HeaderFilter interface.
+  * [jwk] (EXPERIMENTAL) Added key filtering functionality through the KeyFilter interface.
   * [jwk] `jwk.Export` previously did not recognize third-party objects that implemented `jwk.Key`,
     as it was detecting what to do by checking if the object was one of our own unexported
     types. This caused some problems for consumers of this library that wanted to extend the
diff --git a/docs/01-jwt.md b/docs/01-jwt.md
index e5abb9b8..fc610221 100644
--- a/docs/01-jwt.md
+++ b/docs/01-jwt.md
@@ -21,7 +21,10 @@ In this document we describe how to work with JWT using `github.com/lestrrat-go/
   - [Validate for specific claim values](#validate-for-specific-claim-values)
   - [Use a custom validator](#use-a-custom-validator)
   - [Detecting error types](#detecting-error-types)
-- [Serialization](#jwt-serialization)
+- [Filtering Claims](#filtering-claims)
+  - [Filtering Using Standard Claim Names](#filtering-using-standard-claim-names)
+  - [Advanced filtering scenarios](#advanced-filtering-scenarios)
+- [Serialization](#serialization)
   - [Serialize using JWS](#serialize-using-jws)
   - [Serialize using JWE and JWS](#serialize-using-jwe-and-jws)
   - [Serialize the `aud` field as a single string](#serialize-the-aud-field-as-a-single-string)
@@ -973,7 +976,31 @@ func Example_jwt_validate_detect_error_type() {
 source: [examples/jwt_validate_detect_error_type_example_test.go](https://github.com/lestrrat-go/jwx/blob/v3/examples/jwt_validate_detect_error_type_example_test.go)
 <!-- END INCLUDE -->
 
-# JWT Serialization
+# Filtering Claims
+
+JWT tokens can contain many different types of claims - standard claims like `iss`, `aud`, `exp`, as well as custom application-specific claims. Sometimes you need to create modified versions of tokens that only contain certain claims, either for security purposes, API compatibility, or to create specialized token types.
+
+While [`jwt.Token`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwt#Token) object itself does not offer ways to directly extract out these claims, you can use the [`jwt.TokenFilter`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwt#TokenFilter) interface provides methods to filter JWT claims in a flexible way.
+
+## Filtering Using Standard Claim Names
+
+The most common way to filter claims is by either excluding or including only the standard JWT claims. 
+
+For convenience, this library provides `jwt.StandardClaimsFilter()` which filters standard JWT claims defined in RFC 7519.  You can either use `(filter).Filter(token)` to create a `jwt.Token` that contains only the standard claims, or use `(filter).Reject(token)` to create a `jwt.Token` that contains only non-standard claims.
+
+<!-- INCLUDE(examples/jwt_filter_basic_example_test.go) -->
+<!-- END INCLUDE -->
+
+For OpenID tokens, you could also use `openid.StandardClaimsFilter()`.
+
+## Advanced filtering scenarios
+
+If you want to control what gets filtered, you can create a [`jwt.TokenFilter`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwt#TokenFilter) of your own. If all you want to do is filter by claim names, you can re-use the existing `jwt.ClaimNameFilter`. If you want you can also combine multiple filters to create sophisticated filtering logic.
+
+<!-- INCLUDE(examples/jwt_filter_advanced_example_test.go) -->
+<!-- END INCLUDE -->
+
+# Serialization
 
 ## Serialize as JSON
 
diff --git a/docs/02-jws.md b/docs/02-jws.md
index c3c25f4b..55e2dbd1 100644
--- a/docs/02-jws.md
+++ b/docs/02-jws.md
@@ -20,6 +20,7 @@ In this document we describe how to work with JWS using [`github.com/lestrrat-go
 * [Using a custom signing/verification algorithm](#using-a-custom-signingverification-algorithm)
 * [Enabling ES256K](#enabling-es256k)
 * [Using a custom base64 encoder](#using-a-custom-base64-encoder)
+* [Filtering JWS headers](#filtering-jws-headers)
 
 # Parsing
 
@@ -697,3 +698,30 @@ source: [examples/jws_sign_with_custom_base64_example_test.go](https://github.co
 <!-- END INCLUDE -->
 
 You can use these option for `jwt.Sign` and `jwt.Parse` as well. See the [JWT docs for an example](./01-jwt.md#using-a-custom-base64-encoder).
+
+# Filtering JWS headers
+
+**Important:** The filtering functionality described in this section operates on JWS headers only, not on the JWS message itself, nor can you filter or modify the payload of a JWS message directly using these filters.
+
+The JWS library provides filtering capabilities that allow you to selectively include or exclude specific header fields from JWS headers. This is particularly useful when you need to:
+
+- Remove sensitive information from headers before logging or transmission
+- Extract only specific header fields for processing
+- Separate standard JWS headers from custom application-specific headers
+- Create environment-specific header configurations
+
+The filtering operates on parsed JWS messages and their headers, allowing you to create new header objects with only the fields you need.
+
+## Basic header filtering
+
+You can filter JWS headers using the [`jws.HeaderNameFilter`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jws#HeaderNameFilter):
+
+<!-- INCLUDE(examples/jws_filter_basic_example_test.go) -->
+<!-- END INCLUDE -->
+
+## Advanced header filtering
+
+For more complex filtering scenarios, including multi-signature JWS messages:
+
+<!-- INCLUDE(examples/jws_filter_advanced_example_test.go) -->
+<!-- END INCLUDE -->
diff --git a/docs/03-jwe.md b/docs/03-jwe.md
index 30b7d4f9..7cb2ba0e 100644
--- a/docs/03-jwe.md
+++ b/docs/03-jwe.md
@@ -12,6 +12,7 @@ In this document we describe how to work with JWK using `github.com/lestrrat-go/
 * [Decrypting](#decrypting)
   * [Decrypting using a single key](#decrypting-using-a-single-key)
   * [Decrypting using a JWKS](#decrypting-using-a-jwks)
+* [Filtering JWE headers](#filtering-jwe-headers)
 
 # Parsing
 
@@ -432,3 +433,30 @@ func Example_jwe_verify_with_jwk_set() {
 ```
 source: [examples/jwe_decrypt_with_keyset_example_test.go](https://github.com/lestrrat-go/jwx/blob/v3/examples/jwe_decrypt_with_keyset_example_test.go)
 <!-- END INCLUDE -->
+
+# Filtering JWE headers
+
+**Important**: Filtering operates on JWE headers only, not the JWE message or encrypted payload itself. When working with JWE messages, you may need to filter or manipulate the headers for various purposes while leaving the encrypted content intact.
+
+Header filtering is particularly useful for:
+
+- Removing sensitive information from headers before logging or transmission
+- Extract only specific header fields for processing  
+- Separate standard JWE headers from custom application-specific headers
+- Create environment-specific header configurations
+
+The filtering operates on parsed JWE messages and their headers, allowing you to create new header objects with only the fields you need.
+
+## Basic header filtering
+
+You can filter JWE headers using the [`jwe.HeaderNameFilter`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwe#HeaderNameFilter):
+
+<!-- INCLUDE(examples/jwe_filter_basic_example_test.go) -->
+<!-- END INCLUDE -->
+
+## Advanced header filtering
+
+For more complex filtering scenarios, including multi-recipient JWE messages:
+
+<!-- INCLUDE(examples/jwe_filter_advanced_example_test.go) -->
+<!-- END INCLUDE -->
diff --git a/docs/04-jwk.md b/docs/04-jwk.md
index 08c84dc1..686d7c46 100644
--- a/docs/04-jwk.md
+++ b/docs/04-jwk.md
@@ -22,6 +22,7 @@ In this document we describe how to work with JWK using `github.com/lestrrat-go/
   * [Working with key-specific methods](#working-with-key-specific-methods)
   * [Setting values to fields](#setting-values-to-fields)
   * [Converting a jwk.Key to a raw key](#converting-a-jwkkey-to-a-raw-key)
+  * [Filtering Keys with KeyFilter](#filtering-keys-with-keyfilter)
 
 ---
 
@@ -938,3 +939,22 @@ if err := jwk.ParseRawKey(src, &raw); err != nil {
   ...
 }
 ```
+
+## Filtering Keys with KeyFilter
+
+The [`jwk.KeyFilter`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwk#KeyFilter) interface provides a mechanism to selectively include or exclude specific fields when working with JWK keys. This is useful when you need to serialize keys with only certain fields, or when you want to create clean representations of keys for specific purposes.
+
+KeyFilter objects provide two methods:
+- `Filter()`: Returns a new key containing only the fields that should be included
+- `Reject()`: Returns a new key with specified fields excluded
+
+### Standard Field Filters
+
+For convenience, the library provides pre-defined filters that include standard fields for each key type:
+
+- [`jwk.RSAStandardFieldsFilter()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwk#RSAStandardFieldsFilter) - for RSA keys
+- [`jwk.ECDSAStandardFieldsFilter()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwk#ECDSAStandardFieldsFilter) - for ECDSA keys  
+- [`jwk.OKPStandardFieldsFilter()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwk#OKPStandardFieldsFilter) - for OKP keys
+- [`jwk.SymmetricStandardFieldsFilter()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwk#SymmetricStandardFieldsFilter) - for symmetric keys
+
+These functions return filters configured to include the standard fields defined in the JWK specification for each key type.
diff --git a/jwe/BUILD.bazel b/jwe/BUILD.bazel
index d1abaef8..858f80e0 100644
--- a/jwe/BUILD.bazel
+++ b/jwe/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
         "compress.go",
         "decrypt.go",
         "errors.go",
+        "filter.go",
         "headers.go",
         "headers_gen.go",
         "interface.go",
@@ -21,6 +22,7 @@ go_library(
     deps = [
         "//cert",
         "//internal/base64",
+        "//transform",
         "//internal/json",
         "//internal/keyconv",
         "//internal/pool",
@@ -40,6 +42,7 @@ go_library(
 go_test(
     name = "jwe_test",
     srcs = [
+        "filter_test.go",
         "gh402_test.go",
         "headers_test.go",
         "jwe_test.go",
diff --git a/jwe/filter.go b/jwe/filter.go
new file mode 100644
index 00000000..14941604
--- /dev/null
+++ b/jwe/filter.go
@@ -0,0 +1,36 @@
+package jwe
+
+import (
+	"github.com/lestrrat-go/jwx/v3/transform"
+)
+
+// HeaderFilter is an interface that allows users to filter JWE header fields.
+// It provides two methods: Filter and Reject; Filter returns a new header with only
+// the fields that match the filter criteria, while Reject returns a new header with
+// only the fields that DO NOT match the filter.
+//
+// EXPERIMENTAL: This API is experimental and its interface and behavior is
+// subject to change in future releases. This API is not subject to semver
+// compatibility guarantees.
+type HeaderFilter interface {
+	Filter(header Headers) (Headers, error)
+	Reject(header Headers) (Headers, error)
+}
+
+// StandardHeadersFilter returns a HeaderFilter that filters out standard JWE header fields.
+//
+// You can use this filter to create headers that either only have standard fields
+// or only custom fields.
+//
+// If you need to configure the filter more precisely, consider
+// using the HeaderNameFilter directly.
+func StandardHeadersFilter() HeaderFilter {
+	return stdHeadersFilter
+}
+
+var stdHeadersFilter = NewHeaderNameFilter(stdHeaderNames...)
+
+// NewHeaderNameFilter creates a new HeaderNameFilter with the specified field names.
+func NewHeaderNameFilter(names ...string) HeaderFilter {
+	return transform.NewNameBasedFilter[Headers](names...)
+}
diff --git a/jwe/headers_gen.go b/jwe/headers_gen.go
index 1728c690..4f8d2760 100644
--- a/jwe/headers_gen.go
+++ b/jwe/headers_gen.go
@@ -86,6 +86,9 @@ type Headers interface {
 	Keys() []string
 }
 
+// stdHeaderNames is a list of all standard header names defined in the JWE specification.
+var stdHeaderNames = []string{AgreementPartyUInfoKey, AgreementPartyVInfoKey, AlgorithmKey, CompressionKey, ContentEncryptionKey, ContentTypeKey, CriticalKey, EphemeralPublicKeyKey, JWKKey, JWKSetURLKey, KeyIDKey, TypeKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, X509URLKey}
+
 type stdHeaders struct {
 	agreementPartyUInfo    []byte
 	agreementPartyVInfo    []byte
diff --git a/jwk/BUILD.bazel b/jwk/BUILD.bazel
index 851aa603..46acd156 100644
--- a/jwk/BUILD.bazel
+++ b/jwk/BUILD.bazel
@@ -9,6 +9,7 @@ go_library(
         "ecdsa_gen.go",
         "errors.go",
         "fetch.go",
+        "filter.go",
         "interface.go",
         "interface_gen.go",
         "io.go",
@@ -34,6 +35,7 @@ go_library(
         "//cert",
         "//internal/base64",
         "//internal/ecutil",
+        "//transform",
         "//internal/json",
         "//internal/pool",
         "//jwa",
@@ -47,6 +49,7 @@ go_library(
 go_test(
     name = "jwk_test",
     srcs = [
+        "filter_test.go",
         "headers_test.go",
         "jwk_internal_test.go",
         "jwk_test.go",
diff --git a/jwk/ecdsa_gen.go b/jwk/ecdsa_gen.go
index 339d2aed..f202a393 100644
--- a/jwk/ecdsa_gen.go
+++ b/jwk/ecdsa_gen.go
@@ -151,6 +151,8 @@ func (h *ecdsaPublicKey) Has(name string) bool {
 	h.mu.RLock()
 	defer h.mu.RUnlock()
 	switch name {
+	case KeyTypeKey:
+		return true
 	case AlgorithmKey:
 		return h.algorithm != nil
 	case ECDSACrvKey:
@@ -835,6 +837,8 @@ func (h *ecdsaPrivateKey) Has(name string) bool {
 	h.mu.RLock()
 	defer h.mu.RUnlock()
 	switch name {
+	case KeyTypeKey:
+		return true
 	case AlgorithmKey:
 		return h.algorithm != nil
 	case ECDSACrvKey:
@@ -1414,3 +1418,14 @@ func (h *ecdsaPrivateKey) Keys() []string {
 	}
 	return keys
 }
+
+var ecdsaStandardFields KeyFilter
+
+func init() {
+	ecdsaStandardFields = NewFieldNameFilter(KeyTypeKey, KeyUsageKey, KeyOpsKey, AlgorithmKey, KeyIDKey, X509URLKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, ECDSACrvKey, ECDSAXKey, ECDSAYKey, ECDSADKey)
+}
+
+// ECDSAStandardFieldsFilter returns a KeyFilter that filters out standard ECDSA fields.
+func ECDSAStandardFieldsFilter() KeyFilter {
+	return ecdsaStandardFields
+}
diff --git a/jwk/filter.go b/jwk/filter.go
new file mode 100644
index 00000000..e73b0757
--- /dev/null
+++ b/jwk/filter.go
@@ -0,0 +1,28 @@
+package jwk
+
+import (
+	"github.com/lestrrat-go/jwx/v3/transform"
+)
+
+// KeyFilter is an interface that allows users to filter JWK key fields.
+// It provides two methods: Filter and Reject; Filter returns a new key with only
+// the fields that match the filter criteria, while Reject returns a new key with
+// only the fields that DO NOT match the filter.
+//
+// EXPERIMENTAL: This API is experimental and its interface and behavior is
+// subject to change in future releases. This API is not subject to semver
+// compatibility guarantees.
+type KeyFilter interface {
+	Filter(key Key) (Key, error)
+	Reject(key Key) (Key, error)
+}
+
+// NewFieldNameFilter creates a new FieldNameFilter with the specified field names.
+//
+// Note that because some JWK fields are associated with the type instead of
+// stored as data, this filter will not be able to remove them. An example would
+// be the `kty` field: it's associated with the underlying JWK key type, and will
+// always be present even if you attempt to remove it.
+func NewFieldNameFilter(names ...string) KeyFilter {
+	return transform.NewNameBasedFilter[Key](names...)
+}
diff --git a/jwk/interface_gen.go b/jwk/interface_gen.go
index 85e6e8db..4f064754 100644
--- a/jwk/interface_gen.go
+++ b/jwk/interface_gen.go
@@ -23,7 +23,7 @@ const (
 
 // Key defines the minimal interface for each of the
 // key types. Their use and implementation differ significantly
-// between each key types, so you should use type assertions
+// between each key type, so you should use type assertions
 // to perform more specific tasks with each key
 type Key interface {
 
diff --git a/jwk/okp_gen.go b/jwk/okp_gen.go
index 0c90cbce..138fa6c7 100644
--- a/jwk/okp_gen.go
+++ b/jwk/okp_gen.go
@@ -141,6 +141,8 @@ func (h *okpPublicKey) Has(name string) bool {
 	h.mu.RLock()
 	defer h.mu.RUnlock()
 	switch name {
+	case KeyTypeKey:
+		return true
 	case AlgorithmKey:
 		return h.algorithm != nil
 	case OKPCrvKey:
@@ -783,6 +785,8 @@ func (h *okpPrivateKey) Has(name string) bool {
 	h.mu.RLock()
 	defer h.mu.RUnlock()
 	switch name {
+	case KeyTypeKey:
+		return true
 	case AlgorithmKey:
 		return h.algorithm != nil
 	case OKPCrvKey:
@@ -1329,3 +1333,14 @@ func (h *okpPrivateKey) Keys() []string {
 	}
 	return keys
 }
+
+var okpStandardFields KeyFilter
+
+func init() {
+	okpStandardFields = NewFieldNameFilter(KeyTypeKey, KeyUsageKey, KeyOpsKey, AlgorithmKey, KeyIDKey, X509URLKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, OKPCrvKey, OKPXKey, OKPDKey)
+}
+
+// OKPStandardFieldsFilter returns a KeyFilter that filters out standard OKP fields.
+func OKPStandardFieldsFilter() KeyFilter {
+	return okpStandardFields
+}
diff --git a/jwk/rsa_gen.go b/jwk/rsa_gen.go
index ed582f0d..852f3648 100644
--- a/jwk/rsa_gen.go
+++ b/jwk/rsa_gen.go
@@ -146,6 +146,8 @@ func (h *rsaPublicKey) Has(name string) bool {
 	h.mu.RLock()
 	defer h.mu.RUnlock()
 	switch name {
+	case KeyTypeKey:
+		return true
 	case AlgorithmKey:
 		return h.algorithm != nil
 	case RSAEKey:
@@ -831,6 +833,8 @@ func (h *rsaPrivateKey) Has(name string) bool {
 	h.mu.RLock()
 	defer h.mu.RUnlock()
 	switch name {
+	case KeyTypeKey:
+		return true
 	case AlgorithmKey:
 		return h.algorithm != nil
 	case RSADKey:
@@ -1525,3 +1529,14 @@ func (h *rsaPrivateKey) Keys() []string {
 	}
 	return keys
 }
+
+var rsaStandardFields KeyFilter
+
+func init() {
+	rsaStandardFields = NewFieldNameFilter(KeyTypeKey, KeyUsageKey, KeyOpsKey, AlgorithmKey, KeyIDKey, X509URLKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, RSAEKey, RSANKey, RSADKey, RSADPKey, RSADQKey, RSAPKey, RSAQKey, RSAQIKey)
+}
+
+// RSAStandardFieldsFilter returns a KeyFilter that filters out standard RSA fields.
+func RSAStandardFieldsFilter() KeyFilter {
+	return rsaStandardFields
+}
diff --git a/jwk/symmetric_gen.go b/jwk/symmetric_gen.go
index ffa2b13a..696bcd15 100644
--- a/jwk/symmetric_gen.go
+++ b/jwk/symmetric_gen.go
@@ -126,6 +126,8 @@ func (h *symmetricKey) Has(name string) bool {
 	h.mu.RLock()
 	defer h.mu.RUnlock()
 	switch name {
+	case KeyTypeKey:
+		return true
 	case AlgorithmKey:
 		return h.algorithm != nil
 	case KeyIDKey:
@@ -604,3 +606,14 @@ func (h *symmetricKey) Keys() []string {
 	}
 	return keys
 }
+
+var symmetricStandardFields KeyFilter
+
+func init() {
+	symmetricStandardFields = NewFieldNameFilter(KeyTypeKey, KeyUsageKey, KeyOpsKey, AlgorithmKey, KeyIDKey, X509URLKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, SymmetricOctetsKey)
+}
+
+// SymmetricStandardFieldsFilter returns a KeyFilter that filters out standard Symmetric fields.
+func SymmetricStandardFieldsFilter() KeyFilter {
+	return symmetricStandardFields
+}
diff --git a/jws/BUILD.bazel b/jws/BUILD.bazel
index 882df1b3..dd219414 100644
--- a/jws/BUILD.bazel
+++ b/jws/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
         "ecdsa.go",
         "eddsa.go",
         "errors.go",
+        "filter.go",
         "headers.go",
         "headers_gen.go",
         "hmac.go",
@@ -26,6 +27,7 @@ go_library(
         "//cert",
         "//internal/base64",
         "//internal/ecutil",
+        "//transform",
         "//internal/json",
         "//internal/keyconv",
         "//internal/pool",
@@ -39,6 +41,8 @@ go_library(
 go_test(
     name = "jws_test",
     srcs = [
+        "es256k_test.go",
+        "filter_test.go",
         "headers_test.go",
         "jws_test.go",
         "message_test.go",
diff --git a/jws/filter.go b/jws/filter.go
new file mode 100644
index 00000000..9351ab87
--- /dev/null
+++ b/jws/filter.go
@@ -0,0 +1,36 @@
+package jws
+
+import (
+	"github.com/lestrrat-go/jwx/v3/transform"
+)
+
+// HeaderFilter is an interface that allows users to filter JWS header fields.
+// It provides two methods: Filter and Reject; Filter returns a new header with only
+// the fields that match the filter criteria, while Reject returns a new header with
+// only the fields that DO NOT match the filter.
+//
+// EXPERIMENTAL: This API is experimental and its interface and behavior is
+// subject to change in future releases. This API is not subject to semver
+// compatibility guarantees.
+type HeaderFilter interface {
+	Filter(header Headers) (Headers, error)
+	Reject(header Headers) (Headers, error)
+}
+
+// StandardHeadersFilter returns a HeaderFilter that filters out standard JWS header fields.
+//
+// You can use this filter to create headers that either only have standard fields
+// or only custom fields.
+//
+// If you need to configure the filter more precisely, consider
+// using the HeaderNameFilter directly.
+func StandardHeadersFilter() HeaderFilter {
+	return stdHeadersFilter
+}
+
+var stdHeadersFilter = NewHeaderNameFilter(stdHeaderNames...)
+
+// NewHeaderNameFilter creates a new HeaderNameFilter with the specified field names.
+func NewHeaderNameFilter(names ...string) HeaderFilter {
+	return transform.NewNameBasedFilter[Headers](names...)
+}
diff --git a/jws/headers.go b/jws/headers.go
index e566da63..92f6bd11 100644
--- a/jws/headers.go
+++ b/jws/headers.go
@@ -41,3 +41,12 @@ func mergeHeaders(h1, h2 Headers) (Headers, error) {
 func (h *stdHeaders) Merge(h2 Headers) (Headers, error) {
 	return mergeHeaders(h, h2)
 }
+
+// Clone creates a deep copy of the header
+func (h *stdHeaders) Clone() (Headers, error) {
+	dst := NewHeaders()
+	if err := h.Copy(dst); err != nil {
+		return nil, fmt.Errorf(`failed to copy header: %w`, err)
+	}
+	return dst, nil
+}
diff --git a/jws/headers_gen.go b/jws/headers_gen.go
index 486e539e..7475aa45 100644
--- a/jws/headers_gen.go
+++ b/jws/headers_gen.go
@@ -52,6 +52,7 @@ type Headers interface {
 	X509URL() (string, bool)
 	Copy(Headers) error
 	Merge(Headers) (Headers, error)
+	Clone() (Headers, error)
 	// Get is used to extract the value of any field, including non-standard fields, out of the header.
 	//
 	// The first argument is the name of the field. The second argument is a pointer
@@ -69,6 +70,9 @@ type Headers interface {
 	Keys() []string
 }
 
+// stdHeaderNames is a list of all standard header names defined in the JWS specification.
+var stdHeaderNames = []string{AlgorithmKey, ContentTypeKey, CriticalKey, JWKKey, JWKSetURLKey, KeyIDKey, TypeKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, X509URLKey}
+
 type stdHeaders struct {
 	algorithm              *jwa.SignatureAlgorithm // https://tools.ietf.org/html/rfc7515#section-4.1.1
 	contentType            *string                 // https://tools.ietf.org/html/rfc7515#section-4.1.10
diff --git a/jwt/BUILD.bazel b/jwt/BUILD.bazel
index a90d65dd..7facdb17 100644
--- a/jwt/BUILD.bazel
+++ b/jwt/BUILD.bazel
@@ -5,6 +5,7 @@ go_library(
     srcs = [
         "builder_gen.go",
         "errors.go",
+        "filter.go",
         "http.go",
         "interface.go",
         "io.go",
@@ -22,6 +23,7 @@ go_library(
     deps = [
         "//:jwx",
         "//internal/base64",
+        "//transform",
         "//internal/json",
         "//internal/pool",
         "//jwa",
diff --git a/jwt/filter.go b/jwt/filter.go
new file mode 100644
index 00000000..31471dbd
--- /dev/null
+++ b/jwt/filter.go
@@ -0,0 +1,34 @@
+package jwt
+
+import (
+	"github.com/lestrrat-go/jwx/v3/transform"
+)
+
+// TokenFilter is an interface that allows users to filter JWT claims.
+// It provides two methods: Filter and Reject; Filter returns a new token with only
+// the claims that match the filter criteria, while Reject returns a new token with
+// only the claims that DO NOT match the filter.
+//
+// EXPERIMENTAL: This API is experimental and its interface and behavior is
+// subject to change in future releases. This API is not subject to semver
+// compatibility guarantees.
+type TokenFilter interface {
+	Filter(token Token) (Token, error)
+	Reject(token Token) (Token, error)
+}
+
+// StandardClaimsFilter returns a TokenFilter that filters out standard JWT claims.
+//
+// You can use this filter to create tokens that either only has standard claims
+// or only custom claims. If you need to configure the filter more precisely, consider
+// using the ClaimNameFilter directly.
+func StandardClaimsFilter() TokenFilter {
+	return stdClaimsFilter
+}
+
+var stdClaimsFilter = NewClaimNameFilter(stdClaimNames...)
+
+// NewClaimNameFilter creates a new ClaimNameFilter with the specified claim names.
+func NewClaimNameFilter(names ...string) TokenFilter {
+	return transform.NewNameBasedFilter[Token](names...)
+}
diff --git a/jwt/openid/BUILD.bazel b/jwt/openid/BUILD.bazel
index 51cb1dd9..3cf1060c 100644
--- a/jwt/openid/BUILD.bazel
+++ b/jwt/openid/BUILD.bazel
@@ -6,6 +6,7 @@ go_library(
         "address.go",
         "birthdate.go",
         "builder_gen.go",
+        "filter.go",
         "interface.go",
         "openid.go",
         "token_gen.go",
diff --git a/jwt/openid/filter.go b/jwt/openid/filter.go
new file mode 100644
index 00000000..caf868b2
--- /dev/null
+++ b/jwt/openid/filter.go
@@ -0,0 +1,16 @@
+package openid
+
+import (
+	"github.com/lestrrat-go/jwx/v3/jwt"
+)
+
+// StandardClaimsFilter returns a TokenFilter that filters out standard OpenID claims.
+//
+// You can use this filter to create tokens that either only has standard OpenID claims
+// or only custom claims. If you need to configure the filter more precisely, consider
+// using the jwt.ClaimNameFilter directly.
+func StandardClaimsFilter() jwt.TokenFilter {
+	return stdClaimsFilter
+}
+
+var stdClaimsFilter = jwt.NewClaimNameFilter(stdClaimNames...)
diff --git a/jwt/openid/token_gen.go b/jwt/openid/token_gen.go
index cbfa5224..efa426d1 100644
--- a/jwt/openid/token_gen.go
+++ b/jwt/openid/token_gen.go
@@ -45,6 +45,9 @@ const (
 	ZoneinfoKey            = "zoneinfo"
 )
 
+// stdClaimNames is a list of all standard claim names defined in the JWT specification.
+var stdClaimNames = []string{AddressKey, AudienceKey, BirthdateKey, EmailKey, EmailVerifiedKey, ExpirationKey, FamilyNameKey, GenderKey, GivenNameKey, IssuedAtKey, IssuerKey, JwtIDKey, LocaleKey, MiddleNameKey, NameKey, NicknameKey, NotBeforeKey, PhoneNumberKey, PhoneNumberVerifiedKey, PictureKey, PreferredUsernameKey, ProfileKey, SubjectKey, UpdatedAtKey, WebsiteKey, ZoneinfoKey}
+
 type Token interface {
 	// Address returns the value for "address" field of the token
 	Address() (*AddressClaim, bool)
diff --git a/jwt/token_gen.go b/jwt/token_gen.go
index 58e74327..d7e3e605 100644
--- a/jwt/token_gen.go
+++ b/jwt/token_gen.go
@@ -25,6 +25,9 @@ const (
 	SubjectKey    = "sub"
 )
 
+// stdClaimNames is a list of all standard claim names defined in the JWT specification.
+var stdClaimNames = []string{AudienceKey, ExpirationKey, IssuedAtKey, IssuerKey, JwtIDKey, NotBeforeKey, SubjectKey}
+
 // Token represents a generic JWT token.
 // which are type-aware (to an extent). Other claims may be accessed via the `Get`/`Set`
 // methods but their types are not taken into consideration at all. If you have non-standard
diff --git a/tools/cmd/genjwe/main.go b/tools/cmd/genjwe/main.go
index de11798f..3e40bf88 100644
--- a/tools/cmd/genjwe/main.go
+++ b/tools/cmd/genjwe/main.go
@@ -130,6 +130,16 @@ func generateHeaders(obj *codegen.Object) error {
 
 	o.L("}")
 
+	o.LL("// stdHeaderNames is a list of all standard header names defined in the JWE specification.")
+	o.L("var stdHeaderNames = []string{")
+	for i, f := range obj.Fields() {
+		if i > 0 {
+			o.R(",")
+		}
+		o.R("%sKey", f.Name(true))
+	}
+	o.R("}")
+
 	o.LL("type stdHeaders struct {")
 	for _, f := range obj.Fields() {
 		if c := f.Comment(); c != "" {
diff --git a/tools/cmd/genjwk/main.go b/tools/cmd/genjwk/main.go
index 9e34c44c..c1d900b6 100644
--- a/tools/cmd/genjwk/main.go
+++ b/tools/cmd/genjwk/main.go
@@ -80,11 +80,11 @@ func _main() error {
 		}
 	}
 
-	if err := generateGenericHeaders(def.StdFields); err != nil {
+	if err := generateGenericHeaders(def.StdFields, def.KeyTypes); err != nil {
 		return err
 	}
 	for _, kt := range def.KeyTypes {
-		if err := generateKeyType(kt); err != nil {
+		if err := generateKeyType(kt, def.StdFields); err != nil {
 			return fmt.Errorf(`failed to generate key type %s: %w`, kt.Prefix, err)
 		}
 	}
@@ -115,7 +115,7 @@ type Constant struct {
 	Value string
 }
 
-func generateKeyType(kt *KeyType) error {
+func generateKeyType(kt *KeyType, stdFields codegen.FieldList) error {
 	var buf bytes.Buffer
 	o := codegen.NewOutput(&buf)
 	o.L("// Code generated by tools/cmd/genjwk/main.go. DO NOT EDIT.")
@@ -154,6 +154,11 @@ func generateKeyType(kt *KeyType) error {
 		}
 	}
 
+	// Generate StandardFieldsFilter function for this key type
+	if err := generateStandardFieldsFilterWithFields(o, kt, stdFields); err != nil {
+		return fmt.Errorf(`failed to generate StandardFieldsFilter for %s: %w`, kt.Prefix, err)
+	}
+
 	if err := o.WriteFile(kt.Filename, codegen.WithFormatCode(true)); err != nil {
 		if cfe, ok := err.(codegen.CodeFormatError); ok {
 			fmt.Fprint(os.Stderr, cfe.Source())
@@ -163,6 +168,62 @@ func generateKeyType(kt *KeyType) error {
 	return nil
 }
 
+// generateStandardFieldsFilterWithFields generates a StandardFieldsFilter function with explicit field list
+func generateStandardFieldsFilterWithFields(o *codegen.Output, kt *KeyType, fields codegen.FieldList) error {
+	// Always include KeyTypeKey and standard fields
+	fieldNames := []string{"KeyTypeKey"}
+
+	// Add standard fields if provided
+	for _, f := range fields {
+		fieldNames = append(fieldNames, f.Name(true)+"Key")
+	}
+
+	// Add key-type specific fields from all objects, but avoid duplicates
+	seenFields := make(map[string]bool)
+	for _, obj := range kt.Objects {
+		for _, f := range obj.Fields() {
+			// Skip fields that are already in standard fields to avoid duplicates
+			isStandardField := false
+			for _, stdField := range fields {
+				if f.Name(true) == stdField.Name(true) {
+					isStandardField = true
+					break
+				}
+			}
+			if !isStandardField {
+				keyName := kt.Prefix + f.Name(true) + "Key"
+				if !seenFields[keyName] {
+					fieldNames = append(fieldNames, keyName)
+					seenFields[keyName] = true
+				}
+			}
+		}
+	}
+
+	// Generate the standard fields variable
+	o.LL("var %sStandardFields KeyFilter", strings.ToLower(kt.Prefix))
+
+	// Generate the init function
+	o.LL("func init() {")
+	o.L("%sStandardFields = NewFieldNameFilter(", strings.ToLower(kt.Prefix))
+	for i, name := range fieldNames {
+		if i > 0 {
+			o.R(", ")
+		}
+		o.R("%s", name)
+	}
+	o.R(")")
+	o.L("}")
+
+	// Generate the convenience function
+	o.LL("// %sStandardFieldsFilter returns a KeyFilter that filters out standard %s fields.", kt.Prefix, kt.Prefix)
+	o.L("func %sStandardFieldsFilter() KeyFilter {", kt.Prefix)
+	o.L("return %sStandardFields", strings.ToLower(kt.Prefix))
+	o.L("}")
+
+	return nil
+}
+
 func generateObject(o *codegen.Output, kt *KeyType, obj *codegen.Object) error {
 	ifName := kt.Prefix + obj.Name(true)
 	if v := obj.String(`interface`); v != "" {
@@ -264,13 +325,18 @@ func generateObject(o *codegen.Output, kt *KeyType, obj *codegen.Object) error {
 	o.L("h.mu.RLock()")
 	o.L("defer h.mu.RUnlock()")
 	o.L("switch name {")
+	o.L("case KeyTypeKey:")
+	o.L("return true") // kty is always present
 	for _, f := range obj.Fields() {
 		if f.Bool(`is_std`) {
-			o.L("case %sKey:", f.Name(true))
+			if f.Name(true) != "KeyType" { // Skip KeyType since we handled it specially above
+				o.L("case %sKey:", f.Name(true))
+				o.L("return h.%s != nil", f.Name(false))
+			}
 		} else {
 			o.L("case %s%sKey:", kt.Prefix, f.Name(true))
+			o.L("return h.%s != nil", f.Name(false))
 		}
-		o.L("return h.%s != nil", f.Name(false))
 	}
 	o.L("default:")
 	o.L("_, ok := h.privateParams[name]")
@@ -636,7 +702,7 @@ func generateObject(o *codegen.Output, kt *KeyType, obj *codegen.Object) error {
 	return nil
 }
 
-func generateGenericHeaders(fields codegen.FieldList) error {
+func generateGenericHeaders(fields codegen.FieldList, keyTypes []*KeyType) error {
 	var buf bytes.Buffer
 
 	o := codegen.NewOutput(&buf)
@@ -663,7 +729,7 @@ func generateGenericHeaders(fields codegen.FieldList) error {
 
 	o.LL("// Key defines the minimal interface for each of the")
 	o.L("// key types. Their use and implementation differ significantly")
-	o.L("// between each key types, so you should use type assertions")
+	o.L("// between each key type, so you should use type assertions")
 	o.L("// to perform more specific tasks with each key")
 	o.L("type Key interface {")
 	o.LL("// Has returns true if the specified field has a value, even if")
diff --git a/tools/cmd/genjws/main.go b/tools/cmd/genjws/main.go
index 9eafc4d5..763b3543 100644
--- a/tools/cmd/genjws/main.go
+++ b/tools/cmd/genjws/main.go
@@ -105,6 +105,7 @@ func generateHeaders(obj *codegen.Object) error {
 
 	o.L("Copy(Headers) error")
 	o.L("Merge(Headers) (Headers, error)")
+	o.L("Clone() (Headers, error)")
 
 	// These are used to access a single element by key name
 	o.L("// Get is used to extract the value of any field, including non-standard fields, out of the header.")
@@ -125,6 +126,16 @@ func generateHeaders(obj *codegen.Object) error {
 	o.L("Keys() []string")
 	o.L("}")
 
+	o.LL("// stdHeaderNames is a list of all standard header names defined in the JWS specification.")
+	o.L("var stdHeaderNames = []string{")
+	for i, f := range obj.Fields() {
+		if i > 0 {
+			o.R(",")
+		}
+		o.R("%sKey", f.Name(true))
+	}
+	o.R("}")
+
 	o.LL("type stdHeaders struct {")
 	for _, f := range obj.Fields() {
 		if c := f.Comment(); c != "" {
diff --git a/tools/cmd/genjwt/main.go b/tools/cmd/genjwt/main.go
index 744e2494..0f5c0fb4 100644
--- a/tools/cmd/genjwt/main.go
+++ b/tools/cmd/genjwt/main.go
@@ -113,6 +113,22 @@ func generateToken(obj *codegen.Object) error {
 	}
 	o.L(")") // end const
 
+	var pkgPrefix string
+	if obj.String(`package`) != `jwt` {
+		pkgPrefix = `jwt.`
+	}
+
+	// Create a stdClaimNames array
+	o.LL("// stdClaimNames is a list of all standard claim names defined in the JWT specification.")
+	o.L("var stdClaimNames = []string{")
+	for i, f := range fields {
+		if i > 0 {
+			o.R(", ")
+		}
+		o.R("%sKey", f.Name(true))
+	}
+	o.R("}")
+
 	if obj.String(`package`) == "jwt" && obj.Name(false) == "stdToken" {
 		o.LL("// Token represents a generic JWT token.")
 		o.L("// which are type-aware (to an extent). Other claims may be accessed via the `Get`/`Set`")
@@ -167,11 +183,6 @@ func generateToken(obj *codegen.Object) error {
 
 	o.L("Remove(string) error")
 
-	var pkgPrefix string
-	if obj.String(`package`) != `jwt` {
-		pkgPrefix = `jwt.`
-	}
-
 	o.LL("// Options returns the per-token options associated with this token.")
 	o.L("// The options set value will be copied when the token is cloned via `Clone()`")
 	o.L("// but it will not survive when the token goes through marshaling/unmarshaling")
diff --git a/transform/BUILD.bazel b/transform/BUILD.bazel
new file mode 100644
index 00000000..549aa15c
--- /dev/null
+++ b/transform/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "transform",
+    srcs = ["filter.go"],
+    importpath = "github.com/lestrrat-go/jwx/v3/transform",
+    visibility = ["//visibility:public"],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":transform",
+    visibility = ["//visibility:public"],
+)
diff --git a/transform/filter.go b/transform/filter.go
new file mode 100644
index 00000000..da2972db
--- /dev/null
+++ b/transform/filter.go
@@ -0,0 +1,115 @@
+package transform
+
+import "sync"
+
+// FilterLogic is an interface that defines the logic for filtering objects.
+type FilterLogic interface {
+	Apply(key string, object any) bool
+}
+
+// FilterLogicFunc is a function type that implements the FilterLogic interface.
+type FilterLogicFunc func(key string, object any) bool
+
+func (f FilterLogicFunc) Apply(key string, object any) bool {
+	return f(key, object)
+}
+
+// Filterable is an interface that must be implemented by objects that can be filtered.
+type Filterable[T any] interface {
+	// Keys returns the names of all fields in the object.
+	Keys() []string
+
+	// Clone returns a deep copy of the object.
+	Clone() (T, error)
+
+	// Remove removes a field from the object.
+	Remove(string) error
+}
+
+// Apply is a standalone function that provides type-safe filtering based on
+// specified filter logic.
+//
+// It returns a new object with only the fields that match the result of `logic.Apply`.
+func Apply[T Filterable[T]](object T, logic FilterLogic) (T, error) {
+	return filterWith(object, logic, true)
+}
+
+// Reject is a standalone function that provides type-safe filtering based on
+// specified filter logic.
+//
+// It returns a new object with only the fields that DO NOT match the result
+// of `logic.Apply`.
+func Reject[T Filterable[T]](object T, logic FilterLogic) (T, error) {
+	return filterWith(object, logic, false)
+}
+
+// filterWith is an internal function used by both Apply and Reject functions
+// to apply the filtering logic to an object. If include is true, only fields
+// matching the logic are included. If include is false, fields matching
+// the logic are excluded.
+func filterWith[T Filterable[T]](object T, logic FilterLogic, include bool) (T, error) {
+	var zero T
+
+	result, err := object.Clone()
+	if err != nil {
+		return zero, err
+	}
+
+	for _, k := range result.Keys() {
+		if ok := logic.Apply(k, object); (include && ok) || (!include && !ok) {
+			continue
+		}
+
+		if err := result.Remove(k); err != nil {
+			return zero, err
+		}
+	}
+
+	return result, nil
+}
+
+// NameBasedFilter is a filter that filters fields based on their field names.
+type NameBasedFilter[T Filterable[T]] struct {
+	names map[string]struct{}
+	mu    sync.RWMutex
+	logic FilterLogic
+}
+
+// NewNameBasedFilter creates a new NameBasedFilter with the specified field names.
+//
+// NameBasedFilter is the underlying implementation of the
+// various filters in jwe, jwk, jws, and jwt packages. You normally do not
+// need to use this directly.
+func NewNameBasedFilter[T Filterable[T]](names ...string) *NameBasedFilter[T] {
+	nameMap := make(map[string]struct{}, len(names))
+	for _, name := range names {
+		nameMap[name] = struct{}{}
+	}
+
+	nf := &NameBasedFilter[T]{
+		names: nameMap,
+	}
+
+	nf.logic = FilterLogicFunc(nf.filter)
+	return nf
+}
+
+func (nf *NameBasedFilter[T]) filter(k string, _ any) bool {
+	_, ok := nf.names[k]
+	return ok
+}
+
+// Filter returns a new object with only the fields that match the specified names.
+func (nf *NameBasedFilter[T]) Filter(object T) (T, error) {
+	nf.mu.RLock()
+	defer nf.mu.RUnlock()
+
+	return Apply(object, nf.logic)
+}
+
+// Reject returns a new object with only the fields that DO NOT match the specified names.
+func (nf *NameBasedFilter[T]) Reject(object T) (T, error) {
+	nf.mu.RLock()
+	defer nf.mu.RUnlock()
+	return Reject(object, nf.logic)
+}



Test patch:
diff --git a/examples/jwe_filter_advanced_example_test.go b/examples/jwe_filter_advanced_example_test.go
new file mode 100644
index 00000000..a4b65ad7
--- /dev/null
+++ b/examples/jwe_filter_advanced_example_test.go
@@ -0,0 +1,203 @@
+package examples_test
+
+import (
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v3/jwa"
+	"github.com/lestrrat-go/jwx/v3/jwe"
+)
+
+// Example_jwe_filter_advanced demonstrates advanced JWE HeaderFilter functionality
+// with security filtering, service integration scenarios, and header manipulation.
+func Example_jwe_filter_advanced() {
+	// Create JWE headers with comprehensive metadata including security and service information
+	protectedHeaders := jwe.NewHeaders()
+	protectedHeaders.Set(jwe.AlgorithmKey, jwa.RSA_OAEP_256())
+	protectedHeaders.Set(jwe.ContentEncryptionKey, jwa.A256GCM)
+	protectedHeaders.Set(jwe.ContentTypeKey, "application/json")
+	protectedHeaders.Set(jwe.KeyIDKey, "service-key-001")
+
+	// Security headers
+	protectedHeaders.Set("security_level", "high")
+	protectedHeaders.Set("access_control", "restricted")
+	protectedHeaders.Set("encryption_version", "v2.1")
+
+	// Service integration headers
+	protectedHeaders.Set("service_name", "user-service")
+	protectedHeaders.Set("api_version", "v1.2.3")
+	protectedHeaders.Set("request_id", "req-789abc")
+	protectedHeaders.Set("correlation_id", "corr-456def")
+
+	// Operational headers
+	protectedHeaders.Set("environment", "production")
+	protectedHeaders.Set("region", "us-east-1")
+	protectedHeaders.Set("trace_id", "trace-123xyz")
+
+	// Use the headers directly for filtering examples
+	headers := protectedHeaders
+
+	// Advanced Example 1: Service Integration - Filter service-related headers
+	serviceFilter := jwe.NewHeaderNameFilter("service_name", "api_version", "request_id", "correlation_id", jwe.KeyIDKey)
+	serviceHeaders, err := serviceFilter.Filter(headers)
+	if err != nil {
+		fmt.Printf("Failed to filter service headers: %s\n", err)
+		return
+	}
+
+	// Advanced Example 2: Security Headers - Filter security-related metadata
+	securityFilter := jwe.NewHeaderNameFilter("security_level", "access_control", "encryption_version", jwe.AlgorithmKey, jwe.ContentEncryptionKey)
+	securityHeaders, err := securityFilter.Filter(headers)
+	if err != nil {
+		fmt.Printf("Failed to filter security headers: %s\n", err)
+		return
+	}
+
+	// Advanced Example 3: Operational Headers - Filter operational metadata
+	operationalFilter := jwe.NewHeaderNameFilter("environment", "region", "trace_id")
+	operationalHeaders, err := operationalFilter.Filter(headers)
+	if err != nil {
+		fmt.Printf("Failed to filter operational headers: %s\n", err)
+		return
+	}
+	// Use operationalHeaders variable by checking its length
+	if len(operationalHeaders.Keys()) == 0 {
+		fmt.Printf("No operational headers found\n")
+		return
+	}
+
+	// Advanced Example 4: Public Headers - Remove sensitive headers for public APIs
+	sensitiveFilter := jwe.NewHeaderNameFilter("security_level", "access_control", "encryption_version", "trace_id")
+	publicHeaders, err := sensitiveFilter.Reject(headers)
+	if err != nil {
+		fmt.Printf("Failed to create public headers: %s\n", err)
+		return
+	}
+	// Use publicHeaders variable by checking its length
+	if len(publicHeaders.Keys()) == 0 {
+		fmt.Printf("No public headers found\n")
+		return
+	}
+
+	// Advanced Example 5: Minimal Headers - Keep only essential headers for bandwidth optimization
+	essentialFilter := jwe.NewHeaderNameFilter(jwe.AlgorithmKey, jwe.ContentEncryptionKey, jwe.KeyIDKey)
+	minimalHeaders, err := essentialFilter.Filter(headers)
+	if err != nil {
+		fmt.Printf("Failed to filter minimal headers: %s\n", err)
+		return
+	}
+	// Use minimalHeaders variable by checking its length
+	if len(minimalHeaders.Keys()) == 0 {
+		fmt.Printf("No minimal headers found\n")
+		return
+	}
+
+	// Advanced Example 6: Custom Validation - Filter headers based on security requirements
+	isValidSecurityLevel := validateJWESecurityHeaders(securityHeaders)
+	if !isValidSecurityLevel {
+		fmt.Printf("Security validation failed\n")
+		return
+	}
+
+	isValidServiceConfig := validateJWEServiceHeaders(serviceHeaders)
+	if !isValidServiceConfig {
+		fmt.Printf("Service configuration validation failed\n")
+		return
+	}
+
+	// Advanced Example 7: Header transformation for different environments
+	prodHeaders := createJWEEnvironmentHeaders(headers, "production")
+	if len(prodHeaders.Keys()) == 0 {
+		fmt.Printf("Failed to create production headers\n")
+		return
+	}
+
+	testHeaders := createJWEEnvironmentHeaders(headers, "testing")
+	if len(testHeaders.Keys()) == 0 {
+		fmt.Printf("Failed to create testing headers\n")
+		return
+	}
+
+	// OUTPUT:
+}
+
+// validateJWESecurityHeaders checks if security headers meet requirements
+func validateJWESecurityHeaders(headers jwe.Headers) bool {
+	// Check security level
+	var securityLevel string
+	if err := headers.Get("security_level", &securityLevel); err != nil || securityLevel != "high" {
+		return false
+	}
+
+	// Check access control
+	var accessControl string
+	if err := headers.Get("access_control", &accessControl); err != nil || accessControl != "restricted" {
+		return false
+	}
+
+	// Check encryption algorithm
+	if algValue, ok := headers.Algorithm(); !ok || algValue != jwa.RSA_OAEP_256() {
+		return false
+	}
+
+	return true
+}
+
+// validateJWEServiceHeaders checks if service headers are properly configured
+func validateJWEServiceHeaders(headers jwe.Headers) bool {
+	requiredHeaders := []string{"service_name", "api_version", "request_id", "correlation_id"}
+
+	for _, header := range requiredHeaders {
+		if !headers.Has(header) {
+			return false
+		}
+	}
+
+	// Validate API version format
+	var apiVersion string
+	if err := headers.Get("api_version", &apiVersion); err != nil || len(apiVersion) < 5 {
+		return false
+	}
+
+	return true
+}
+
+// createJWEEnvironmentHeaders creates environment-specific header configurations
+func createJWEEnvironmentHeaders(originalHeaders jwe.Headers, environment string) jwe.Headers {
+	switch environment {
+	case "production":
+		// Production: Include security and service headers, exclude debug info
+		prodFilter := jwe.NewHeaderNameFilter(
+			jwe.AlgorithmKey, jwe.ContentEncryptionKey, jwe.ContentTypeKey, jwe.KeyIDKey,
+			"security_level", "access_control", "service_name", "api_version", "environment", "region",
+		)
+		filtered, err := prodFilter.Filter(originalHeaders)
+		if err != nil {
+			fmt.Printf("Failed to create production headers: %s\n", err)
+			return jwe.NewHeaders()
+		}
+		return filtered
+
+	case "testing":
+		// Testing: Include debug headers, exclude some security headers
+		testFilter := jwe.NewHeaderNameFilter(
+			jwe.AlgorithmKey, jwe.ContentEncryptionKey, jwe.ContentTypeKey, jwe.KeyIDKey,
+			"service_name", "api_version", "request_id", "correlation_id", "trace_id", "environment",
+		)
+		filtered, err := testFilter.Filter(originalHeaders)
+		if err != nil {
+			fmt.Printf("Failed to create testing headers: %s\n", err)
+			return jwe.NewHeaders()
+		}
+		return filtered
+
+	default:
+		// Default: Use standard headers only
+		stdFilter := jwe.StandardHeadersFilter()
+		filtered, err := stdFilter.Filter(originalHeaders)
+		if err != nil {
+			fmt.Printf("Failed to create default headers: %s\n", err)
+			return jwe.NewHeaders()
+		}
+		return filtered
+	}
+}
diff --git a/examples/jwe_filter_basic_example_test.go b/examples/jwe_filter_basic_example_test.go
new file mode 100644
index 00000000..89ebceba
--- /dev/null
+++ b/examples/jwe_filter_basic_example_test.go
@@ -0,0 +1,81 @@
+package examples_test
+
+import (
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v3/jwa"
+	"github.com/lestrrat-go/jwx/v3/jwe"
+)
+
+// Example_jwe_filter_basic demonstrates basic JWE HeaderFilter functionality
+// with HeaderNameFilter.Filter(), StandardHeadersFilter(), and HeaderNameFilter.Reject() methods.
+func Example_jwe_filter_basic() {
+	// Create JWE headers with custom headers for filtering demonstration
+	protectedHeaders := jwe.NewHeaders()
+	protectedHeaders.Set(jwe.AlgorithmKey, jwa.RSA_OAEP_256())
+	protectedHeaders.Set(jwe.ContentEncryptionKey, jwa.A256GCM)
+	protectedHeaders.Set(jwe.ContentTypeKey, "application/json")
+	protectedHeaders.Set(jwe.KeyIDKey, "example-key-1")
+	protectedHeaders.Set("custom-header", "custom-value")
+	protectedHeaders.Set("app-id", "my-app")
+	protectedHeaders.Set("version", "1.0")
+
+	// Use the headers directly for filtering examples
+	headers := protectedHeaders
+
+	// Example 1: HeaderNameFilter.Filter() - Include only specific headers
+	customFilter := jwe.NewHeaderNameFilter("custom-header", "app-id", jwe.KeyIDKey)
+
+	filteredHeaders, err := customFilter.Filter(headers)
+	if err != nil {
+		fmt.Printf("HeaderNameFilter.Filter failed: %s\n", err)
+		return
+	}
+	// Use filteredHeaders variable by checking its length
+	if len(filteredHeaders.Keys()) == 0 {
+		fmt.Printf("No filtered headers found\n")
+		return
+	}
+
+	// Example 2: StandardHeadersFilter() - Include only standard JWE headers
+	stdFilter := jwe.StandardHeadersFilter()
+
+	standardHeaders, err := stdFilter.Filter(headers)
+	if err != nil {
+		fmt.Printf("StandardHeadersFilter.Filter failed: %s\n", err)
+		return
+	}
+	// Use standardHeaders variable by checking its length
+	if len(standardHeaders.Keys()) == 0 {
+		fmt.Printf("No standard headers found\n")
+		return
+	}
+
+	// Example 3: HeaderNameFilter.Reject() - Exclude specific headers
+	rejectFilter := jwe.NewHeaderNameFilter("version", "custom-header")
+
+	rejectedHeaders, err := rejectFilter.Reject(headers)
+	if err != nil {
+		fmt.Printf("HeaderNameFilter.Reject failed: %s\n", err)
+		return
+	}
+	// Use rejectedHeaders variable by checking its length
+	if len(rejectedHeaders.Keys()) == 0 {
+		fmt.Printf("No rejected headers found\n")
+		return
+	}
+
+	// Example 4: StandardHeadersFilter().Reject() - Exclude standard headers, keep custom
+	customOnlyHeaders, err := stdFilter.Reject(headers)
+	if err != nil {
+		fmt.Printf("StandardHeadersFilter.Reject failed: %s\n", err)
+		return
+	}
+	// Use customOnlyHeaders variable by checking its length
+	if len(customOnlyHeaders.Keys()) == 0 {
+		fmt.Printf("No custom only headers found\n")
+		return
+	}
+
+	// OUTPUT:
+}
diff --git a/examples/jwk_cache_example_test.go b/examples/jwk_cache_example_test.go
index 095ba085..aa0fd535 100644
--- a/examples/jwk_cache_example_test.go
+++ b/examples/jwk_cache_example_test.go
@@ -11,6 +11,7 @@ import (
 
 func Example_jwk_cache() {
 	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
 
 	const googleCerts = `https://www.googleapis.com/oauth2/v3/certs`
 
diff --git a/examples/jwk_filter_advanced_example_test.go b/examples/jwk_filter_advanced_example_test.go
new file mode 100644
index 00000000..50b62678
--- /dev/null
+++ b/examples/jwk_filter_advanced_example_test.go
@@ -0,0 +1,217 @@
+package examples_test
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v3/jwk"
+)
+
+func Example_jwk_filter_advanced_use_cases() {
+	// Create multiple keys with different security classifications
+
+	// 1. High-security production key
+	prodKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+	if err != nil {
+		fmt.Printf("failed to generate production key: %s\n", err)
+		return
+	}
+	prodJWK, err := jwk.Import(prodKey)
+	if err != nil {
+		fmt.Printf("failed to import production key: %s\n", err)
+		return
+	}
+
+	prodJWK.Set(jwk.KeyIDKey, "prod-ecdsa-384-2024")
+	prodJWK.Set(jwk.AlgorithmKey, "ES384")
+	prodJWK.Set(jwk.KeyUsageKey, "sig")
+	prodJWK.Set("securityLevel", "high")
+	prodJWK.Set("environment", "production")
+	prodJWK.Set("classification", "confidential")
+	prodJWK.Set("owner", "security-team")
+	prodJWK.Set("contactEmail", "security@company.com")
+	prodJWK.Set("purpose", "payment-processing")
+	prodJWK.Set("dataTypes", []string{"pii", "financial", "authentication"})
+	prodJWK.Set("compliance", map[string]interface{}{
+		"pci-dss": "level-1",
+		"sox":     true,
+		"gdpr":    true,
+		"hipaa":   false,
+	})
+	prodJWK.Set("auditRequired", true)
+	prodJWK.Set("backupLocation", "hsm-cluster-primary")
+	prodJWK.Set("lastAudit", "2024-03-15T09:00:00Z")
+
+	// 2. Development key
+	devKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		fmt.Printf("failed to generate dev key: %s\n", err)
+		return
+	}
+	devJWK, err := jwk.Import(devKey)
+	if err != nil {
+		fmt.Printf("failed to import dev key: %s\n", err)
+		return
+	}
+
+	devJWK.Set(jwk.KeyIDKey, "dev-ecdsa-256-2024")
+	devJWK.Set(jwk.AlgorithmKey, "ES256")
+	devJWK.Set(jwk.KeyUsageKey, "sig")
+	devJWK.Set("securityLevel", "low")
+	devJWK.Set("environment", "development")
+	devJWK.Set("classification", "public")
+	devJWK.Set("owner", "dev-team")
+	devJWK.Set("contactEmail", "dev@company.com")
+	devJWK.Set("purpose", "testing")
+	devJWK.Set("dataTypes", []string{"test-data", "mock-data"})
+	devJWK.Set("compliance", map[string]interface{}{
+		"pci-dss": "not-applicable",
+		"sox":     false,
+		"gdpr":    false,
+		"hipaa":   false,
+	})
+	devJWK.Set("auditRequired", false)
+	devJWK.Set("backupLocation", "local-storage")
+	devJWK.Set("lastAudit", "never")
+
+	// 3. Staging key
+	stagingKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		fmt.Printf("failed to generate staging key: %s\n", err)
+		return
+	}
+	stagingJWK, err := jwk.Import(stagingKey)
+	if err != nil {
+		fmt.Printf("failed to import staging key: %s\n", err)
+		return
+	}
+
+	stagingJWK.Set(jwk.KeyIDKey, "staging-ecdsa-256-2024")
+	stagingJWK.Set(jwk.AlgorithmKey, "ES256")
+	stagingJWK.Set(jwk.KeyUsageKey, "sig")
+	stagingJWK.Set("securityLevel", "medium")
+	stagingJWK.Set("environment", "staging")
+	stagingJWK.Set("classification", "internal")
+	stagingJWK.Set("owner", "qa-team")
+	stagingJWK.Set("contactEmail", "qa@company.com")
+	stagingJWK.Set("purpose", "integration-testing")
+	stagingJWK.Set("dataTypes", []string{"sanitized-production-data"})
+	stagingJWK.Set("compliance", map[string]interface{}{
+		"pci-dss": "level-3",
+		"sox":     true,
+		"gdpr":    true,
+		"hipaa":   false,
+	})
+	stagingJWK.Set("auditRequired", true)
+	stagingJWK.Set("backupLocation", "cloud-backup-encrypted")
+	stagingJWK.Set("lastAudit", "2024-02-28T14:30:00Z")
+
+	// Advanced Use Case 1: Security classification filter
+	// Create different filters based on security levels
+	publicFieldsFilter := jwk.NewFieldNameFilter(
+		"securityLevel", "environment", "classification", "owner",
+		"contactEmail", "purpose", "auditRequired",
+	)
+
+	confidentialFieldsFilter := jwk.NewFieldNameFilter(
+		"dataTypes", "compliance", "backupLocation", "lastAudit",
+	)
+
+	// Apply public fields filter to production key
+	publicProdKey, err := publicFieldsFilter.Filter(prodJWK)
+	if err != nil {
+		fmt.Printf("failed to create public key: %s\n", err)
+		return
+	}
+
+	// Apply confidential fields filter to production key
+	confidentialProdKey, err := confidentialFieldsFilter.Filter(prodJWK)
+	if err != nil {
+		fmt.Printf("failed to create confidential key: %s\n", err)
+		return
+	}
+
+	// Advanced Use Case 2: Compliance-specific filtering
+	complianceFilter := jwk.NewFieldNameFilter(
+		jwk.KeyIDKey, "environment", "purpose", "compliance",
+		"auditRequired", "lastAudit", "dataTypes",
+	)
+
+	// Apply compliance filter to production key for demonstration
+	if _, err := complianceFilter.Filter(prodJWK); err != nil {
+		fmt.Printf("failed to create compliance key: %s\n", err)
+		return
+	}
+
+	// Apply compliance filter to dev key for demonstration
+	if _, err := complianceFilter.Filter(devJWK); err != nil {
+		fmt.Printf("failed to create compliance dev key: %s\n", err)
+		return
+	}
+
+	// Advanced Use Case 3: Operational monitoring filter
+	opsFilter := jwk.NewFieldNameFilter(
+		jwk.KeyIDKey, "environment", "owner", "contactEmail",
+		"backupLocation", "lastAudit", "auditRequired",
+	)
+
+	// Apply ops filter to staging key for demonstration
+	if _, err := opsFilter.Filter(stagingJWK); err != nil {
+		fmt.Printf("failed to create ops key: %s\n", err)
+		return
+	}
+
+	// Advanced Use Case 4: Remove all custom metadata for pure cryptographic use
+	stdFilter := jwk.ECDSAStandardFieldsFilter()
+	cryptoDevKey, err := stdFilter.Filter(devJWK)
+	if err != nil {
+		fmt.Printf("failed to create crypto key: %s\n", err)
+		return
+	}
+
+	// Validate that filtered keys have the correct structure
+
+	// Check original production key has all expected fields
+	if !prodJWK.Has("d") || !prodJWK.Has("x") || !prodJWK.Has("y") {
+		fmt.Printf("missing cryptographic fields in production key\n")
+		return
+	}
+	if !prodJWK.Has("environment") || !prodJWK.Has("classification") {
+		fmt.Printf("missing metadata fields in production key\n")
+		return
+	}
+
+	// Check public key excludes cryptographic data
+	if publicProdKey.Has("d") || publicProdKey.Has("x") || publicProdKey.Has("y") {
+		fmt.Printf("public key should not contain cryptographic fields\n")
+		return
+	}
+	if !publicProdKey.Has("securityLevel") || !publicProdKey.Has("environment") {
+		fmt.Printf("public key missing expected fields\n")
+		return
+	}
+
+	// Check confidential key has restricted fields only
+	if confidentialProdKey.Has("contactEmail") || confidentialProdKey.Has("owner") {
+		fmt.Printf("confidential key should not contain public fields\n")
+		return
+	}
+	if !confidentialProdKey.Has("dataTypes") || !confidentialProdKey.Has("compliance") {
+		fmt.Printf("confidential key missing expected fields\n")
+		return
+	}
+
+	// Check crypto-only key has standard fields but no metadata
+	if cryptoDevKey.Has("environment") || cryptoDevKey.Has("classification") {
+		fmt.Printf("crypto-only key should not contain metadata\n")
+		return
+	}
+	if !cryptoDevKey.Has("kty") || !cryptoDevKey.Has("use") {
+		fmt.Printf("crypto-only key missing standard JWK fields\n")
+		return
+	}
+
+	// OUTPUT:
+}
diff --git a/examples/jwk_filter_basic_example_test.go b/examples/jwk_filter_basic_example_test.go
new file mode 100644
index 00000000..c8590949
--- /dev/null
+++ b/examples/jwk_filter_basic_example_test.go
@@ -0,0 +1,80 @@
+package examples_test
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v3/jwk"
+)
+
+func Example_jwk_filter_basic_fields() {
+	// Create an RSA key and import it as a JWK with custom fields
+	rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		fmt.Printf("failed to generate RSA key: %s\n", err)
+		return
+	}
+
+	key, err := jwk.Import(rsaPrivateKey)
+	if err != nil {
+		fmt.Printf("failed to import RSA key: %s\n", err)
+		return
+	}
+
+	// Add standard fields
+	key.Set(jwk.KeyIDKey, "my-rsa-key-001")
+	key.Set(jwk.KeyUsageKey, "sig")
+	key.Set(jwk.AlgorithmKey, "RS256")
+
+	// Add custom fields
+	key.Set("keyOwner", "alice@example.com")
+	key.Set("environment", "production")
+	key.Set("department", "security")
+	key.Set("rotationPolicy", "quarterly")
+
+	// Create a custom field name filter
+	customFilter := jwk.NewFieldNameFilter("keyOwner", "environment", "department", "rotationPolicy")
+
+	// Filter to get only custom fields
+	customOnlyKey, err := customFilter.Filter(key)
+	if err != nil {
+		fmt.Printf("failed to filter custom fields: %s\n", err)
+		return
+	}
+
+	// Use RSAStandardFieldsFilter to get only standard RSA JWK fields
+	standardFilter := jwk.RSAStandardFieldsFilter()
+	standardOnlyKey, err := standardFilter.Filter(key)
+	if err != nil {
+		fmt.Printf("failed to filter standard fields: %s\n", err)
+		return
+	}
+
+	// Create a copy for display purposes, replacing long values with "..."
+	displayKey, err := standardOnlyKey.Clone()
+	if err != nil {
+		fmt.Printf("failed to clone standard key: %s\n", err)
+		return
+	}
+
+	// Validate that the filtering worked correctly
+
+	// Check custom-only key has expected custom fields and no sensitive data
+	if !customOnlyKey.Has("keyOwner") || !customOnlyKey.Has("environment") {
+		fmt.Printf("custom key missing expected fields\n")
+		return
+	}
+	if customOnlyKey.Has("d") || customOnlyKey.Has("n") {
+		fmt.Printf("custom key should not contain cryptographic fields\n")
+		return
+	}
+
+	// Check that display key has expected standard fields
+	if !displayKey.Has("alg") || !displayKey.Has("kty") || !displayKey.Has("use") {
+		fmt.Printf("display key missing standard JWK fields\n")
+		return
+	}
+
+	// Output:
+}
diff --git a/examples/jwk_readfile_example_test.go b/examples/jwk_readfile_example_test.go
index f4d607b4..da8a7065 100644
--- a/examples/jwk_readfile_example_test.go
+++ b/examples/jwk_readfile_example_test.go
@@ -32,7 +32,7 @@ func Example_jwk_readfile() {
 	}
 	defer os.Remove(f.Name())
 
-	fmt.Fprintf(f, src)
+	fmt.Fprint(f, src)
 	f.Close()
 
 	key, err := jwk.ReadFile(f.Name())
diff --git a/examples/jwk_readfile_with_pem_example_test.go b/examples/jwk_readfile_with_pem_example_test.go
index 296b491c..a2900925 100644
--- a/examples/jwk_readfile_with_pem_example_test.go
+++ b/examples/jwk_readfile_with_pem_example_test.go
@@ -44,7 +44,7 @@ c4wOvhbalcX0FqTM3mXCgMFRbibquhwdxbU=
 	}
 	defer os.Remove(f.Name())
 
-	fmt.Fprintf(f, src)
+	fmt.Fprint(f, src)
 	f.Close()
 
 	key, err := jwk.ReadFile(f.Name(), jwk.WithPEM(true))
diff --git a/examples/jws_filter_advanced_example_test.go b/examples/jws_filter_advanced_example_test.go
new file mode 100644
index 00000000..d3e9068a
--- /dev/null
+++ b/examples/jws_filter_advanced_example_test.go
@@ -0,0 +1,123 @@
+package examples_test
+
+import (
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v3/jwa"
+	"github.com/lestrrat-go/jwx/v3/jwk"
+	"github.com/lestrrat-go/jwx/v3/jws"
+)
+
+func Example_jws_header_filter_advanced() {
+	// Create keys for multi-signature JWS
+	key1, err := jwk.Import([]byte(`secret-key-1`))
+	if err != nil {
+		fmt.Printf("failed to create key1: %s\n", err)
+		return
+	}
+
+	key2, err := jwk.Import([]byte(`secret-key-2`))
+	if err != nil {
+		fmt.Printf("failed to create key2: %s\n", err)
+		return
+	}
+
+	// Create complex headers for first signature
+	headers1 := jws.NewHeaders()
+	headers1.Set(jws.KeyIDKey, "primary-key")
+	headers1.Set("service", "auth-service")
+	headers1.Set("version", "2.1")
+	headers1.Set("security-level", "high")
+	headers1.Set("internal-use", "true")
+
+	// Create headers for second signature with different custom fields
+	headers2 := jws.NewHeaders()
+	headers2.Set(jws.KeyIDKey, "backup-key")
+	headers2.Set("service", "backup-auth")
+	headers2.Set("datacenter", "us-west")
+	headers2.Set("backup-priority", "1")
+	headers2.Set("internal-use", "false")
+
+	payload := []byte(`{"action": "login", "timestamp": 1609459200}`)
+
+	// Create a multi-signature JWS message using JSON serialization
+	signed, err := jws.Sign(payload, jws.WithJSON(),
+		jws.WithKey(jwa.HS256(), key1, jws.WithProtectedHeaders(headers1)),
+		jws.WithKey(jwa.HS256(), key2, jws.WithProtectedHeaders(headers2)))
+	if err != nil {
+		fmt.Printf("failed to sign message: %s\n", err)
+		return
+	}
+
+	// Parse the signed message
+	parsedMsg, err := jws.Parse(signed)
+	if err != nil {
+		fmt.Printf("failed to parse message: %s\n", err)
+		return
+	}
+
+	// Advanced filtering scenarios
+	for i, sig := range parsedMsg.Signatures() {
+		originalHeaders := sig.ProtectedHeaders()
+
+		// Use case 1: Filter by service-related fields
+		serviceFilter := jws.NewHeaderNameFilter("service", "datacenter", "backup-priority")
+		_, err := serviceFilter.Filter(originalHeaders)
+		if err != nil {
+			fmt.Printf("failed to filter service headers: %s\n", err)
+			continue
+		}
+
+		// Use case 2: Create public headers (remove internal fields)
+		internalFilter := jws.NewHeaderNameFilter("internal-use", "security-level")
+		_, err = internalFilter.Reject(originalHeaders)
+		if err != nil {
+			fmt.Printf("failed to create public headers: %s\n", err)
+			continue
+		}
+
+		// Use case 3: Combine standard filter with custom filtering
+		standardFilter := jws.StandardHeadersFilter()
+		customFieldsOnly, err := standardFilter.Reject(originalHeaders)
+		if err != nil {
+			fmt.Printf("failed to extract custom fields: %s\n", err)
+			continue
+		}
+
+		// Then filter custom fields for specific categories
+		operationalFilter := jws.NewHeaderNameFilter("service", "version", "datacenter")
+		_, err = operationalFilter.Filter(customFieldsOnly)
+		if err != nil {
+			fmt.Printf("failed to filter operational headers: %s\n", err)
+			continue
+		}
+
+		if i == 0 {
+			// Use case 4: Validate security requirements for first signature
+			validateJWSSecurityHeaders(originalHeaders)
+		}
+	}
+
+	// OUTPUT:
+}
+
+// Helper function to demonstrate validation using filtered JWS headers
+func validateJWSSecurityHeaders(headers jws.Headers) {
+	// Check security level
+	var secLevel string
+	if err := headers.Get("security-level", &secLevel); err != nil {
+		fmt.Println("✗ Security level not found")
+	}
+
+	// Check internal use flag
+	var internalUse string
+	if err := headers.Get("internal-use", &internalUse); err != nil {
+		fmt.Println("✗ Internal use flag missing")
+	}
+
+	// Check service identification
+	var service string
+	if err := headers.Get("service", &service); err != nil {
+		fmt.Println("✗ Service identification missing")
+	}
+}
diff --git a/examples/jws_filter_basic_example_test.go b/examples/jws_filter_basic_example_test.go
new file mode 100644
index 00000000..f06dfeb3
--- /dev/null
+++ b/examples/jws_filter_basic_example_test.go
@@ -0,0 +1,70 @@
+package examples_test
+
+import (
+	"fmt"
+
+	"github.com/lestrrat-go/jwx/v3/jwa"
+	"github.com/lestrrat-go/jwx/v3/jwk"
+	"github.com/lestrrat-go/jwx/v3/jws"
+)
+
+func Example_jws_header_filter_basic() {
+	// Create a key for signing
+	key, err := jwk.Import([]byte(`my-secret-key`))
+	if err != nil {
+		fmt.Printf("failed to create key: %s\n", err)
+		return
+	}
+
+	// Create headers with both standard and custom fields
+	headers := jws.NewHeaders()
+	headers.Set(jws.AlgorithmKey, jwa.HS256())
+	headers.Set(jws.KeyIDKey, "key-2024")
+	headers.Set(jws.TypeKey, "JWT")
+	headers.Set("custom-claim", "important-data")
+	headers.Set("app-version", "v1.2.3")
+	headers.Set("environment", "production")
+
+	// Sign with custom headers
+	payload := []byte(`{"user": "alice", "role": "admin"}`)
+	signed, err := jws.Sign(payload, jws.WithKey(jwa.HS256(), key, jws.WithProtectedHeaders(headers)))
+	if err != nil {
+		fmt.Printf("failed to sign: %s\n", err)
+		return
+	}
+
+	// Parse the signed message to access headers
+	msg, err := jws.Parse(signed)
+	if err != nil {
+		fmt.Printf("failed to parse: %s\n", err)
+		return
+	}
+
+	originalHeaders := msg.Signatures()[0].ProtectedHeaders()
+
+	// Filter 1: Extract only custom fields using HeaderNameFilter
+	customFilter := jws.NewHeaderNameFilter("custom-claim", "app-version", "environment")
+	_, err = customFilter.Filter(originalHeaders)
+	if err != nil {
+		fmt.Printf("failed to filter custom headers: %s\n", err)
+		return
+	}
+
+	// Filter 2: Extract only standard fields using StandardHeadersFilter
+	standardFilter := jws.StandardHeadersFilter()
+	_, err = standardFilter.Filter(originalHeaders)
+	if err != nil {
+		fmt.Printf("failed to filter standard headers: %s\n", err)
+		return
+	}
+
+	// Filter 3: Remove sensitive custom fields using Reject
+	sensitiveFilter := jws.NewHeaderNameFilter("custom-claim")
+	_, err = sensitiveFilter.Reject(originalHeaders)
+	if err != nil {
+		fmt.Printf("failed to reject sensitive headers: %s\n", err)
+		return
+	}
+
+	// OUTPUT:
+}
diff --git a/examples/jws_readfile_example_test.go b/examples/jws_readfile_example_test.go
index 63c50087..aaff2f04 100644
--- a/examples/jws_readfile_example_test.go
+++ b/examples/jws_readfile_example_test.go
@@ -17,7 +17,7 @@ func Example_jws_readfile() {
 	}
 	defer os.Remove(f.Name())
 
-	fmt.Fprintf(f, src)
+	fmt.Fprint(f, src)
 	f.Close()
 
 	msg, err := jws.ReadFile(f.Name())
diff --git a/examples/jwt_filter_advanced_example_test.go b/examples/jwt_filter_advanced_example_test.go
new file mode 100644
index 00000000..822c66b6
--- /dev/null
+++ b/examples/jwt_filter_advanced_example_test.go
@@ -0,0 +1,78 @@
+package examples_test
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/lestrrat-go/jwx/v3/jwt"
+)
+
+func Example_jwt_filter_advanced_use_cases() {
+	// Create a comprehensive token with various types of claims
+	token, err := jwt.NewBuilder().
+		Issuer("auth-service.example.com").
+		Subject("user-456").
+		Audience([]string{"web-app", "mobile-app", "api-gateway"}).
+		IssuedAt(time.Unix(1234567890, 0)).
+		Expiration(time.Unix(1234567890+7200, 0)).
+		NotBefore(time.Unix(1234567890, 0)).
+		JwtID("session-xyz789").
+		Claim("userRole", "manager").
+		Claim("department", "sales").
+		Claim("permissions", []string{"read:reports", "write:orders", "approve:discounts"}).
+		Claim("profile", map[string]interface{}{
+			"name":  "John Doe",
+			"email": "john@example.com",
+			"phone": "+1-555-0123",
+		}).
+		Claim("sessionInfo", map[string]interface{}{
+			"loginIP":      "10.0.1.100",
+			"deviceType":   "desktop",
+			"browser":      "Chrome/91.0",
+			"lastActivity": "2023-01-01T12:30:00Z",
+		}).
+		Claim("features", []string{"beta-ui", "advanced-analytics", "mobile-push"}).
+		Build()
+	if err != nil {
+		fmt.Printf("failed to build comprehensive token: %s\n", err)
+		return
+	}
+
+	// Use case 1: Create a token for public APIs (remove sensitive information)
+	sensitiveFilter := jwt.NewClaimNameFilter("sessionInfo", "profile")
+	if _, err := sensitiveFilter.Reject(token); err != nil {
+		fmt.Printf("failed to create public API token: %s\n", err)
+		return
+	}
+
+	// Use case 2: Create an identity-only token (only user identification claims)
+	identityFilter := jwt.NewClaimNameFilter("sub", "iss", "userRole", "department")
+	if _, err := identityFilter.Filter(token); err != nil {
+		fmt.Printf("failed to create identity token: %s\n", err)
+		return
+	}
+
+	// Use case 3: Create a minimal security token (only time-based and security claims)
+	securityFilter := jwt.NewClaimNameFilter("iss", "sub", "aud", "exp", "iat", "nbf", "jti")
+	if _, err := securityFilter.Filter(token); err != nil {
+		fmt.Printf("failed to create security token: %s\n", err)
+		return
+	}
+
+	// Use case 4: Combine filters - remove both standard claims and specific custom claims
+	standardFilter := jwt.StandardClaimsFilter()
+	tempToken, err := standardFilter.Reject(token) // Remove standard claims first
+	if err != nil {
+		fmt.Printf("failed to remove standard claims: %s\n", err)
+		return
+	}
+
+	// Then remove specific custom claims
+	customSensitiveFilter := jwt.NewClaimNameFilter("sessionInfo", "profile")
+	if _, err := customSensitiveFilter.Reject(tempToken); err != nil {
+		fmt.Printf("failed to remove custom sensitive claims: %s\n", err)
+		return
+	}
+
+	// OUTPUT:
+}
diff --git a/examples/jwt_filter_basic_example_test.go b/examples/jwt_filter_basic_example_test.go
new file mode 100644
index 00000000..8b300935
--- /dev/null
+++ b/examples/jwt_filter_basic_example_test.go
@@ -0,0 +1,56 @@
+package examples_test
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/lestrrat-go/jwx/v3/jwt"
+)
+
+func Example_jwt_filter_basic_claims() {
+	// Create a token with standard and custom claims
+	token, err := jwt.NewBuilder().
+		Issuer("github.com/lestrrat-go/jwx").
+		Subject("jwt_filter_example").
+		Audience([]string{"developers", "users"}).
+		IssuedAt(time.Unix(1234567890, 0)).
+		Expiration(time.Unix(1234567890+3600, 0)).
+		Claim("customClaim", "customValue").
+		Claim("applicationRole", "admin").
+		Claim("department", "engineering").
+		Build()
+	if err != nil {
+		fmt.Printf("failed to build token: %s\n", err)
+		return
+	}
+
+	// Create a custom claim name filter
+	customFilter := jwt.NewClaimNameFilter("customClaim", "applicationRole", "department")
+
+	// Filter to get only custom claims
+	if _, err := customFilter.Filter(token); err != nil {
+		fmt.Printf("failed to filter custom claims: %s\n", err)
+		return
+	}
+	// You could also use Reject to get all claims except the specified ones
+	// Note that this may include other non-standard claims
+	if _, err := customFilter.Reject(token); err != nil {
+		fmt.Printf("failed to reject custom claims: %s\n", err)
+		return
+	}
+
+	// Use StandardClaimsFilter to get only standard JWT claims
+	if _, err = jwt.StandardClaimsFilter().Filter(token); err != nil {
+		fmt.Printf("failed to filter standard claims: %s\n", err)
+		return
+	}
+
+	// Use StandardClaimsFilter to reject standard claims, resulting
+	// in every non-standard claim being retained
+	if _, err = jwt.StandardClaimsFilter().Reject(token); err != nil {
+		fmt.Printf("failed to reject standard claims: %s\n", err)
+		return
+	}
+
+	// OUTPUT:
+}
diff --git a/examples/jwt_readfile_example_test.go b/examples/jwt_readfile_example_test.go
index 9ed4852f..13e7233c 100644
--- a/examples/jwt_readfile_example_test.go
+++ b/examples/jwt_readfile_example_test.go
@@ -15,7 +15,7 @@ func Example_jwt_readfile() {
 	}
 	defer os.Remove(f.Name())
 
-	fmt.Fprintf(f, exampleJWTSignedHMAC)
+	fmt.Fprint(f, exampleJWTSignedHMAC)
 	f.Close()
 
 	// Note: this JWT has NOT been verified because we have not passed jwt.WithKey() and used
diff --git a/jwe/filter_test.go b/jwe/filter_test.go
new file mode 100644
index 00000000..1a832528
--- /dev/null
+++ b/jwe/filter_test.go
@@ -0,0 +1,160 @@
+package jwe_test
+
+import (
+	"sync"
+	"testing"
+
+	"github.com/lestrrat-go/jwx/v3/jwa"
+	"github.com/lestrrat-go/jwx/v3/jwe"
+	"github.com/stretchr/testify/require"
+)
+
+func TestHeaderNameFilter(t *testing.T) {
+	t.Run("Basic functionality", func(t *testing.T) {
+		// Create a filter that includes specific fields
+		fn := jwe.NewHeaderNameFilter("custom1", "custom3")
+
+		// Create a header with standard and custom fields
+		headers := jwe.NewHeaders()
+		// Headers in JWE might need special handling to be recognized by Keys() and Has()
+		// so we'll primarily test with custom fields that are stored in privateParams
+		headers.Set("custom1", "value1")
+		headers.Set("custom2", "value2")
+		headers.Set("custom3", "value3")
+
+		t.Run("Filter specific fields", func(t *testing.T) {
+			// Filter should return a header with only specified fields
+			filtered, err := fn.Filter(headers)
+			require.NoError(t, err, "fn.Filter should succeed")
+
+			// Debug info
+			t.Logf("Original headers keys: %v", headers.Keys())
+			t.Logf("Filtered headers keys: %v", filtered.Keys())
+
+			// Verify included fields are present
+			require.True(t, filtered.Has("custom1"), "filtered header should have custom1 field")
+			require.True(t, filtered.Has("custom3"), "filtered header should have custom3 field")
+
+			// Verify excluded fields are not present
+			require.False(t, filtered.Has("custom2"), "filtered header should not have custom2 field")
+		})
+
+		t.Run("Reject specific fields", func(t *testing.T) {
+			// Reject should return a header without specified fields
+			rejected, err := fn.Reject(headers)
+			require.NoError(t, err, "fn.Reject should succeed")
+
+			// Verify included fields are not present
+			require.False(t, rejected.Has("custom1"), "rejected header should not have custom1 field")
+			require.False(t, rejected.Has("custom3"), "rejected header should not have custom3 field")
+
+			// Verify excluded fields are present
+			require.True(t, rejected.Has("custom2"), "rejected header should have custom2 field")
+		})
+	})
+
+	t.Run("Empty filter", func(t *testing.T) {
+		// Create an empty filter (no fields)
+		fn := jwe.NewHeaderNameFilter()
+
+		// Create a header with some fields
+		headers := jwe.NewHeaders()
+		headers.Set(jwe.AlgorithmKey, jwa.RSA_OAEP_256)
+		headers.Set("custom", "value")
+
+		// Filter with empty HeaderNameFilter should result in an empty header
+		filtered, err := fn.Filter(headers)
+		require.NoError(t, err, "fn.Filter should succeed")
+		require.Empty(t, filtered.Keys(), "filtered header should have no fields")
+
+		// Reject with empty HeaderNameFilter should result in a copy of the original header
+		rejected, err := fn.Reject(headers)
+		require.NoError(t, err, "fn.Reject should succeed")
+
+		// Check that rejected header has the same keys as original
+		originalKeys := headers.Keys()
+		rejectedKeys := rejected.Keys()
+		require.ElementsMatch(t, originalKeys, rejectedKeys, "rejected header should have the same fields as the original")
+	})
+
+	t.Run("Concurrency safety", func(t *testing.T) {
+		// This is more of a logical test than an actual concurrency test
+		// but it ensures the filter is being used correctly
+		fn := jwe.NewHeaderNameFilter("custom1")
+
+		// Create a header
+		headers := jwe.NewHeaders()
+		headers.Set("custom1", "value1")
+		headers.Set("custom2", "value2")
+
+		// Run filter and reject operations concurrently
+		var wg sync.WaitGroup
+		const iterations = 10
+		const numGoroutines = 2
+		wg.Add(iterations * numGoroutines)
+		for range iterations {
+			go func() {
+				defer wg.Done()
+				filtered, err := fn.Filter(headers)
+				require.NoError(t, err, "fn.Filter should succeed")
+				require.True(t, filtered.Has("custom1"), "filtered header should have custom1 field")
+				require.False(t, filtered.Has("custom2"), "filtered header should not have custom2 field")
+			}()
+			go func() {
+				defer wg.Done()
+				rejected, err := fn.Reject(headers)
+				require.NoError(t, err, "fn.Reject should succeed")
+				require.False(t, rejected.Has("custom1"), "rejected header should not have custom1 field")
+				require.True(t, rejected.Has("custom2"), "rejected header should have custom2 field")
+			}()
+		}
+		wg.Wait()
+	})
+}
+
+func TestStandardHeadersFilter(t *testing.T) {
+	t.Run("Filter standard headers", func(t *testing.T) {
+		// Create a header with standard and custom fields
+		headers := jwe.NewHeaders()
+		// Standard headers may not be properly reflected in Keys()
+		// so we focus on testing custom fields which are stored in privateParams
+		headers.Set("custom1", "value1")
+		headers.Set("custom2", "value2")
+
+		stdFilter := jwe.StandardHeadersFilter()
+
+		t.Run("Filter standard headers", func(t *testing.T) {
+			// Filter should return a header with only standard fields
+			filtered, err := stdFilter.Filter(headers)
+			require.NoError(t, err, "filter.Filter should succeed")
+
+			// Since our test only uses custom fields (non-standard),
+			// the filtered result should be empty
+			require.Empty(t, filtered.Keys(), "filtered header should have no fields")
+
+			// Verify custom fields are not present
+			require.False(t, filtered.Has("custom1"), "filtered header should not have custom1 field")
+			require.False(t, filtered.Has("custom2"), "filtered header should not have custom2 field")
+		})
+
+		t.Run("Reject standard headers", func(t *testing.T) {
+			// Reject should return a header with only custom fields
+			rejected, err := stdFilter.Reject(headers)
+			require.NoError(t, err, "filter.Reject should succeed")
+
+			// Verify custom fields are present (all original fields should be present)
+			require.True(t, rejected.Has("custom1"), "rejected header should have custom1 field")
+			require.True(t, rejected.Has("custom2"), "rejected header should have custom2 field")
+
+			// Check that all original keys are present
+			originalKeys := headers.Keys()
+			rejectedKeys := rejected.Keys()
+			require.ElementsMatch(t, originalKeys, rejectedKeys, "rejected should have all original fields")
+
+			// Verify values are preserved
+			var customValue string
+			require.NoError(t, rejected.Get("custom1", &customValue), "rejected.Get should succeed")
+			require.Equal(t, "value1", customValue, "value for custom1 field should be preserved")
+		})
+	})
+}
diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go
index d78dc5c0..38dcb61b 100644
--- a/jwe/jwe_test.go
+++ b/jwe/jwe_test.go
@@ -185,26 +185,31 @@ func TestParse_RSAES_OAEP_AES_GCM(t *testing.T) {
 		},
 	}
 
-	ntmpl := len(templates)
-	testcases := make([]struct {
+	var testcases []struct {
 		Name     string
 		Options  []jwe.EncryptOption
 		Expected string
-	}, ntmpl*2)
+	}
 
-	for i, tmpl := range templates {
+	for _, tmpl := range templates {
 		options := make([]jwe.EncryptOption, len(tmpl.Options))
 		copy(options, tmpl.Options)
 
-		for j, compression := range []jwa.CompressionAlgorithm{jwa.NoCompress(), jwa.Deflate()} {
+		for _, compression := range []jwa.CompressionAlgorithm{jwa.NoCompress(), jwa.Deflate()} {
 			compName := compression.String()
 			if compName == "" {
 				compName = "none"
 			}
-			tc := testcases[i+j]
-			tc.Name = tmpl.Name + " (compression=" + compName + ")"
-			tc.Expected = tmpl.Expected
-			tc.Options = append(options, jwe.WithCompress(compression))
+
+			testcases = append(testcases, struct {
+				Name     string
+				Options  []jwe.EncryptOption
+				Expected string
+			}{
+				Name:     tmpl.Name + " (compression=" + compName + ")",
+				Options:  append(options, jwe.WithCompress(compression)),
+				Expected: tmpl.Expected,
+			})
 		}
 	}
 
diff --git a/jwk/filter_test.go b/jwk/filter_test.go
new file mode 100644
index 00000000..d7f54554
--- /dev/null
+++ b/jwk/filter_test.go
@@ -0,0 +1,273 @@
+package jwk_test
+
+import (
+	"testing"
+
+	"github.com/lestrrat-go/jwx/v3/jwk"
+	"github.com/stretchr/testify/require"
+)
+
+func TestFieldNameFilter(t *testing.T) {
+	t.Run("NewFieldNameFilter", func(t *testing.T) {
+		fn := jwk.NewFieldNameFilter("a", "b", "c")
+		require.NotNil(t, fn, "NewFieldNameFilter should return a non-nil value")
+	})
+
+	t.Run("Filter", func(t *testing.T) {
+		// Create a FieldNameFilter with field names a, b, c
+		fn := jwk.NewFieldNameFilter("a", "b", "c")
+
+		// Create a key with fields kty, a, b, c, d, e
+		const src = `{
+			"kty": "oct",
+			"k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow",
+			"a": "value_a",
+			"b": "value_b",
+			"c": "value_c", 
+			"d": "value_d",
+			"e": "value_e"
+		}`
+
+		rawKey, err := jwk.ParseKey([]byte(src))
+		require.NoError(t, err, "jwk.ParseKey should succeed")
+
+		// Filter should return a key with only fields kty, a, b, c
+		filtered, err := fn.Filter(rawKey)
+		require.NoError(t, err, "fn.Filter should succeed")
+
+		// Debug: print keys for inspection
+		t.Logf("Original keys: %v", rawKey.Keys())
+		t.Logf("Filtered keys: %v", filtered.Keys())
+
+		// Check that filtered key contains kty, a, b, c by examining Keys()
+		filteredKeys := filtered.Keys()
+		require.Contains(t, filteredKeys, jwk.KeyTypeKey, "filtered key must have kty field")
+		require.Contains(t, filteredKeys, "a", "filtered key should have field 'a'")
+		require.Contains(t, filteredKeys, "b", "filtered key should have field 'b'")
+		require.Contains(t, filteredKeys, "c", "filtered key should have field 'c'")
+		require.NotContains(t, filteredKeys, "d", "filtered key should not have field 'd'")
+		require.NotContains(t, filteredKeys, "e", "filtered key should not have field 'e'")
+		require.True(t, filtered.Has("a"), "filtered key should have field 'a'")
+		require.True(t, filtered.Has("b"), "filtered key should have field 'b'")
+		require.True(t, filtered.Has("c"), "filtered key should have field 'c'")
+		require.False(t, filtered.Has("d"), "filtered key should not have field 'd'")
+		require.False(t, filtered.Has("e"), "filtered key should not have field 'e'")
+
+		// Verify values are preserved
+		var val string
+		require.NoError(t, filtered.Get("a", &val), "filtered.Get should succeed")
+		require.Equal(t, "value_a", val, "value for field 'a' should be preserved")
+	})
+
+	t.Run("Reject", func(t *testing.T) {
+		// Create a FieldNameFilter with field names a, b, c
+		fn := jwk.NewFieldNameFilter("a", "b", "c")
+
+		// Create a key with fields kty, a, b, c, d, e
+		const src = `{
+			"kty": "oct",
+			"k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow",
+			"a": "value_a",
+			"b": "value_b",
+			"c": "value_c", 
+			"d": "value_d",
+			"e": "value_e"
+		}`
+
+		rawKey, err := jwk.ParseKey([]byte(src))
+		require.NoError(t, err, "jwk.ParseKey should succeed")
+
+		// Reject should return a key with only fields kty, d, e
+		rejected, err := fn.Reject(rawKey)
+		require.NoError(t, err, "fn.Reject should succeed")
+
+		// Debug: print keys for inspection
+		t.Logf("Original keys: %v", rawKey.Keys())
+		t.Logf("Rejected keys: %v", rejected.Keys())
+
+		// Check that rejected key contains only kty, d, e by examining Keys()
+		rejectedKeys := rejected.Keys()
+		require.Contains(t, rejectedKeys, jwk.KeyTypeKey, "rejected key must have kty field")
+		require.NotContains(t, rejectedKeys, "a", "rejected key should not have field 'a'")
+		require.NotContains(t, rejectedKeys, "b", "rejected key should not have field 'b'")
+		require.NotContains(t, rejectedKeys, "c", "rejected key should not have field 'c'")
+		require.Contains(t, rejectedKeys, "d", "rejected key should have field 'd'")
+		require.Contains(t, rejectedKeys, "e", "rejected key should have field 'e'")
+		require.False(t, rejected.Has("a"), "rejected key should not have field 'a'")
+		require.False(t, rejected.Has("b"), "rejected key should not have field 'b'")
+		require.False(t, rejected.Has("c"), "rejected key should not have field 'c'")
+		require.True(t, rejected.Has("d"), "rejected key should have field 'd'")
+		require.True(t, rejected.Has("e"), "rejected key should have field 'e'")
+
+		// Verify values are preserved
+		var val string
+		require.NoError(t, rejected.Get("d", &val), "rejected.Get should succeed")
+		require.Equal(t, "value_d", val, "value for field 'd' should be preserved")
+	})
+
+	t.Run("Error handling with Clone", func(t *testing.T) {
+		// Since it's hard to create a failing Clone scenario,
+		// we'll just verify Filter and Reject don't panic with a simple key
+		const src = `{ "kty": "oct", "k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ" }`
+
+		rawKey, err := jwk.ParseKey([]byte(src))
+		require.NoError(t, err, "jwk.ParseKey should succeed")
+
+		fn := jwk.NewFieldNameFilter("a", "b", "c")
+		_, err = fn.Filter(rawKey)
+		require.NoError(t, err, "fn.Filter should succeed even for a basic key")
+
+		_, err = fn.Reject(rawKey)
+		require.NoError(t, err, "fn.Reject should succeed even for a basic key")
+	})
+
+	t.Run("Empty FieldNameFilter", func(t *testing.T) {
+		// Create an empty FieldNameFilter
+		fn := jwk.NewFieldNameFilter()
+
+		// Create a key with fields kty, a, b, c
+		const src = `{
+			"kty": "oct",
+			"k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow",
+			"a": "value_a",
+			"b": "value_b",
+			"c": "value_c"
+		}`
+
+		rawKey, err := jwk.ParseKey([]byte(src))
+		require.NoError(t, err, "jwk.ParseKey should succeed")
+
+		// Filter with empty FieldNameFilter should result in a key with only kty
+		filtered, err := fn.Filter(rawKey)
+		require.NoError(t, err, "fn.Filter should succeed")
+		keys := filtered.Keys()
+		require.Len(t, keys, 1, "filtered key should have only one field (kty)")
+		require.Equal(t, jwk.KeyTypeKey, keys[0], "the only field should be kty")
+
+		// Reject with empty FieldNameFilter should result in a copy of the original key
+		rejected, err := fn.Reject(rawKey)
+		require.NoError(t, err, "fn.Reject should succeed")
+
+		// Check that rejected key has the same keys as original
+		originalKeys := rawKey.Keys()
+		rejectedKeys := rejected.Keys()
+		require.ElementsMatch(t, originalKeys, rejectedKeys, "rejected key should have the same fields as the original")
+	})
+
+	t.Run("Concurrency safety", func(t *testing.T) {
+		// This is more of a logical test than an actual concurrency test
+		// but it ensures the mutex is being used correctly in the Filter/Reject methods
+
+		fn := jwk.NewFieldNameFilter("a", "b", "c")
+		const src = `{
+			"kty": "oct",
+			"k": "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow",
+			"a": "value_a",
+			"d": "value_d"
+		}`
+
+		rawKey, err := jwk.ParseKey([]byte(src))
+		require.NoError(t, err, "jwk.ParseKey should succeed")
+
+		// Should not deadlock or have race conditions
+		filtered, err := fn.Filter(rawKey)
+		require.NoError(t, err, "fn.Filter should succeed")
+		require.True(t, filtered.Has("a"), "filtered key should have field 'a'")
+		require.False(t, filtered.Has("d"), "filtered key should not have field 'd'")
+
+		rejected, err := fn.Reject(rawKey)
+		require.NoError(t, err, "fn.Reject should succeed")
+		require.False(t, rejected.Has("a"), "rejected key should not have field 'a'")
+		require.True(t, rejected.Has("d"), "rejected key should have field 'd'")
+	})
+}
+
+func TestStandardFieldsFilter(t *testing.T) {
+	t.Run("Filter standard fields", func(t *testing.T) {
+		// Create a key with standard and custom fields
+		const src = `{
+			"kty": "EC",
+			"crv": "P-256",
+			"x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
+			"y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
+			"kid": "kid-value",
+			"use": "sig",
+			"key_ops": ["sign", "verify"],
+			"alg": "ES256",
+			"x5u": "https://example.com/x509",
+			"x5c": ["cert1", "cert2"],
+			"x5t": "thumbprint",
+			"x5t#S256": "thumbprint-s256",
+			"custom1": "value1",
+			"custom2": "value2"
+		}`
+
+		rawKey, err := jwk.ParseKey([]byte(src))
+		require.NoError(t, err, "jwk.ParseKey should succeed")
+		require.NotNil(t, rawKey, "key should not be nil")
+
+		stdFilter := jwk.ECDSAStandardFieldsFilter()
+
+		t.Run("Filter standard fields", func(t *testing.T) {
+			// Filter should return a key with only standard fields
+			filtered, err := stdFilter.Filter(rawKey)
+			require.NoError(t, err, "filter.Filter should succeed")
+
+			// Debug: print keys for inspection
+			t.Logf("Original keys: %v", rawKey.Keys())
+			t.Logf("Filtered keys: %v", filtered.Keys())
+
+			// Verify standard fields are present by examining Keys()
+			filteredKeys := filtered.Keys()
+			require.Contains(t, filteredKeys, jwk.KeyTypeKey, "filtered key should have kty field")
+			require.Contains(t, filteredKeys, jwk.KeyIDKey, "filtered key should have kid field")
+			require.Contains(t, filteredKeys, jwk.KeyUsageKey, "filtered key should have use field")
+			require.Contains(t, filteredKeys, jwk.KeyOpsKey, "filtered key should have key_ops field")
+			require.Contains(t, filteredKeys, jwk.AlgorithmKey, "filtered key should have alg field")
+			require.Contains(t, filteredKeys, jwk.X509URLKey, "filtered key should have x5u field")
+			require.Contains(t, filteredKeys, jwk.X509CertChainKey, "filtered key should have x5c field")
+			require.Contains(t, filteredKeys, jwk.X509CertThumbprintKey, "filtered key should have x5t field")
+			require.Contains(t, filteredKeys, jwk.X509CertThumbprintS256Key, "filtered key should have x5t#S256 field")
+
+			// Verify custom fields are not present
+			require.NotContains(t, filteredKeys, "custom1", "filtered key should not have custom1 field")
+			require.NotContains(t, filteredKeys, "custom2", "filtered key should not have custom2 field")
+
+			// Verify values are preserved
+			var kid string
+			require.NoError(t, filtered.Get(jwk.KeyIDKey, &kid), "filtered.Get should succeed")
+			require.Equal(t, "kid-value", kid, "value for kid field should be preserved")
+		})
+
+		t.Run("Reject standard fields", func(t *testing.T) {
+			// Reject should return a key with only custom fields
+			rejected, err := stdFilter.Reject(rawKey)
+			require.NoError(t, err, "filter.Reject should succeed")
+
+			// Debug: print keys for inspection
+			t.Logf("Original keys: %v", rawKey.Keys())
+			t.Logf("Rejected keys: %v", rejected.Keys())
+
+			// Verify standard fields are not present (except kty which cannot be removed)
+			rejectedKeys := rejected.Keys()
+			require.Contains(t, rejectedKeys, jwk.KeyTypeKey, "rejected key must have kty field")
+			require.NotContains(t, rejectedKeys, jwk.KeyIDKey, "rejected key should not have kid field")
+			require.NotContains(t, rejectedKeys, jwk.KeyUsageKey, "rejected key should not have use field")
+			require.NotContains(t, rejectedKeys, jwk.KeyOpsKey, "rejected key should not have key_ops field")
+			require.NotContains(t, rejectedKeys, jwk.AlgorithmKey, "rejected key should not have alg field")
+			require.NotContains(t, rejectedKeys, jwk.X509URLKey, "rejected key should not have x5u field")
+			require.NotContains(t, rejectedKeys, jwk.X509CertChainKey, "rejected key should not have x5c field")
+			require.NotContains(t, rejectedKeys, jwk.X509CertThumbprintKey, "rejected key should not have x5t field")
+			require.NotContains(t, rejectedKeys, jwk.X509CertThumbprintS256Key, "rejected key should not have x5t#S256 field")
+
+			// Verify custom fields are present
+			require.True(t, rejected.Has("custom1"), "rejected key should have custom1 field")
+			require.True(t, rejected.Has("custom2"), "rejected key should have custom2 field")
+
+			// Verify values are preserved
+			var customValue string
+			require.NoError(t, rejected.Get("custom1", &customValue), "rejected.Get should succeed")
+			require.Equal(t, "value1", customValue, "value for custom1 field should be preserved")
+		})
+	})
+}
diff --git a/jws/filter_test.go b/jws/filter_test.go
new file mode 100644
index 00000000..eb53601c
--- /dev/null
+++ b/jws/filter_test.go
@@ -0,0 +1,192 @@
+package jws_test
+
+import (
+	"errors"
+	"sync"
+	"testing"
+
+	"github.com/lestrrat-go/jwx/v3/jwa"
+	"github.com/lestrrat-go/jwx/v3/jws"
+	"github.com/stretchr/testify/require"
+)
+
+func TestHeaderNameFilter(t *testing.T) {
+	t.Run("Basic functionality", func(t *testing.T) {
+		// Create a filter that includes specific fields
+		fn := jws.NewHeaderNameFilter("custom1", "custom3")
+
+		// Create a header with standard and custom fields
+		headers := jws.NewHeaders()
+		// Headers in JWS might need special handling to be recognized by Keys() and Has()
+		// so we'll primarily test with custom fields that are stored in privateParams
+		headers.Set("custom1", "value1")
+		headers.Set("custom2", "value2")
+		headers.Set("custom3", "value3")
+
+		t.Run("Filter specific fields", func(t *testing.T) {
+			// Filter should return a header with only specified fields
+			filtered, err := fn.Filter(headers)
+			require.NoError(t, err, "fn.Filter should succeed")
+
+			// Debug info
+			t.Logf("Original headers keys: %v", headers.Keys())
+			t.Logf("Filtered headers keys: %v", filtered.Keys())
+
+			// Verify included fields are present
+			require.True(t, filtered.Has("custom1"), "filtered header should have custom1 field")
+			require.True(t, filtered.Has("custom3"), "filtered header should have custom3 field")
+
+			// Verify excluded fields are not present
+			require.False(t, filtered.Has("custom2"), "filtered header should not have custom2 field")
+		})
+
+		t.Run("Reject specific fields", func(t *testing.T) {
+			// Reject should return a header without specified fields
+			rejected, err := fn.Reject(headers)
+			require.NoError(t, err, "fn.Reject should succeed")
+
+			// Verify included fields are not present
+			require.False(t, rejected.Has("custom1"), "rejected header should not have custom1 field")
+			require.False(t, rejected.Has("custom3"), "rejected header should not have custom3 field")
+
+			// Verify excluded fields are present
+			require.True(t, rejected.Has("custom2"), "rejected header should have custom2 field")
+		})
+	})
+
+	t.Run("Empty filter", func(t *testing.T) {
+		// Create an empty filter (no fields)
+		fn := jws.NewHeaderNameFilter()
+
+		// Create a header with some fields
+		headers := jws.NewHeaders()
+		headers.Set(jws.AlgorithmKey, jwa.ES256())
+		headers.Set("custom", "value")
+
+		// Filter with empty HeaderNameFilter should result in an empty header
+		filtered, err := fn.Filter(headers)
+		require.NoError(t, err, "fn.Filter should succeed")
+		require.Empty(t, filtered.Keys(), "filtered header should have no fields")
+
+		// Reject with empty HeaderNameFilter should result in a copy of the original header
+		rejected, err := fn.Reject(headers)
+		require.NoError(t, err, "fn.Reject should succeed")
+
+		// Check that rejected header has the same keys as original
+		originalKeys := headers.Keys()
+		rejectedKeys := rejected.Keys()
+		require.ElementsMatch(t, originalKeys, rejectedKeys, "rejected header should have the same fields as the original")
+	})
+
+	t.Run("Concurrency safety", func(t *testing.T) {
+		// This is more of a logical test than an actual concurrency test
+		// but it ensures the filter is being used correctly
+		fn := jws.NewHeaderNameFilter("custom1")
+
+		// Create a header
+		headers := jws.NewHeaders()
+		headers.Set("custom1", "value1")
+		headers.Set("custom2", "value2")
+
+		// Run filter and reject operations concurrently
+		var wg sync.WaitGroup
+		const iterations = 10
+		const numGoroutines = 2
+		wg.Add(iterations * numGoroutines)
+		for range iterations {
+			go func() {
+				defer wg.Done()
+				filtered, err := fn.Filter(headers)
+				require.NoError(t, err, "fn.Filter should succeed")
+				require.True(t, filtered.Has("custom1"), "filtered header should have custom1 field")
+				require.False(t, filtered.Has("custom2"), "filtered header should not have custom2 field")
+			}()
+			go func() {
+				defer wg.Done()
+				rejected, err := fn.Reject(headers)
+				require.NoError(t, err, "fn.Reject should succeed")
+				require.False(t, rejected.Has("custom1"), "rejected header should not have custom1 field")
+				require.True(t, rejected.Has("custom2"), "rejected header should have custom2 field")
+			}()
+		}
+		wg.Wait()
+	})
+}
+
+func TestStandardHeadersFilter(t *testing.T) {
+	t.Run("Filter standard headers", func(t *testing.T) {
+		// Create a header with standard and custom fields
+		headers := jws.NewHeaders()
+		// Standard headers may not be properly reflected in Keys()
+		// so we focus on testing custom fields which are stored in privateParams
+		headers.Set("custom1", "value1")
+		headers.Set("custom2", "value2")
+
+		stdFilter := jws.StandardHeadersFilter()
+
+		t.Run("Filter standard headers", func(t *testing.T) {
+			// Filter should return a header with only standard fields
+			filtered, err := stdFilter.Filter(headers)
+			require.NoError(t, err, "filter.Filter should succeed")
+
+			// Since our test only uses custom fields (non-standard),
+			// the filtered result should be empty
+			require.Empty(t, filtered.Keys(), "filtered header should have no fields")
+
+			// Verify custom fields are not present
+			require.False(t, filtered.Has("custom1"), "filtered header should not have custom1 field")
+			require.False(t, filtered.Has("custom2"), "filtered header should not have custom2 field")
+		})
+
+		t.Run("Reject standard headers", func(t *testing.T) {
+			// Reject should return a header with only custom fields
+			rejected, err := stdFilter.Reject(headers)
+			require.NoError(t, err, "filter.Reject should succeed")
+
+			// Verify custom fields are present (all original fields should be present)
+			require.True(t, rejected.Has("custom1"), "rejected header should have custom1 field")
+			require.True(t, rejected.Has("custom2"), "rejected header should have custom2 field")
+
+			// Check that all original keys are present
+			originalKeys := headers.Keys()
+			rejectedKeys := rejected.Keys()
+			require.ElementsMatch(t, originalKeys, rejectedKeys, "rejected should have all original fields")
+
+			// Verify values are preserved
+			var customValue string
+			require.NoError(t, rejected.Get("custom1", &customValue), "rejected.Get should succeed")
+			require.Equal(t, "value1", customValue, "value for custom1 field should be preserved")
+		})
+	})
+}
+
+// This test ensures proper error handling when Clone fails
+type errorHeaders struct {
+	jws.Headers
+}
+
+func (h *errorHeaders) Clone() (jws.Headers, error) {
+	return nil, errors.New("forced Clone error")
+}
+
+func (h *errorHeaders) Keys() []string {
+	return []string{}
+}
+
+func TestHeaderFilterErrors(t *testing.T) {
+	// Create a header that will cause errors
+	badHeaders := &errorHeaders{jws.NewHeaders()}
+	filter := jws.NewHeaderNameFilter("test")
+
+	t.Run("Filter error handling", func(t *testing.T) {
+		_, err := filter.Filter(badHeaders)
+		require.Error(t, err, "filter.Filter should fail with errorHeaders")
+		require.Contains(t, err.Error(), "forced Clone error", "error message should include Clone error")
+	})
+
+	t.Run("Reject error handling", func(t *testing.T) {
+		_, err := filter.Reject(badHeaders)
+		require.Error(t, err, "filter.Reject should fail with errorHeaders")
+		require.Contains(t, err.Error(), "forced Clone error", "error message should include Clone error")
+	})
+}
diff --git a/jwt/filter_test.go b/jwt/filter_test.go
new file mode 100644
index 00000000..726a1008
--- /dev/null
+++ b/jwt/filter_test.go
@@ -0,0 +1,204 @@
+package jwt_test
+
+import (
+	"testing"
+	"time"
+
+	"github.com/lestrrat-go/jwx/v3/jwt"
+	"github.com/stretchr/testify/require"
+)
+
+func TestClaimNames(t *testing.T) {
+	t.Run("NewClaimNames", func(t *testing.T) {
+		cn := jwt.NewClaimNameFilter("a", "b", "c")
+		require.NotNil(t, cn, "NewClaimNames should return a non-nil value")
+	})
+
+	t.Run("Filter", func(t *testing.T) {
+		// Create a ClaimNames with names a, b, c
+		cn := jwt.NewClaimNameFilter("a", "b", "c")
+
+		// Create a token with claims a, b, c, d, e
+		token := jwt.New()
+		require.NoError(t, token.Set("a", "value_a"), "token.Set should succeed")
+		require.NoError(t, token.Set("b", "value_b"), "token.Set should succeed")
+		require.NoError(t, token.Set("c", "value_c"), "token.Set should succeed")
+		require.NoError(t, token.Set("d", "value_d"), "token.Set should succeed")
+		require.NoError(t, token.Set("e", "value_e"), "token.Set should succeed")
+
+		// Filter should return a token with only claims a, b, c
+		filtered, err := cn.Filter(token)
+		require.NoError(t, err, "cn.Filter should succeed")
+
+		// Check that filtered token contains only a, b, c
+		require.True(t, filtered.Has("a"), "filtered token should have claim 'a'")
+		require.True(t, filtered.Has("b"), "filtered token should have claim 'b'")
+		require.True(t, filtered.Has("c"), "filtered token should have claim 'c'")
+		require.False(t, filtered.Has("d"), "filtered token should not have claim 'd'")
+		require.False(t, filtered.Has("e"), "filtered token should not have claim 'e'")
+
+		// Verify values are preserved
+		var val string
+		require.NoError(t, filtered.Get("a", &val), "filtered.Get should succeed")
+		require.Equal(t, "value_a", val, "value for claim 'a' should be preserved")
+	})
+
+	t.Run("Reject", func(t *testing.T) {
+		// Create a ClaimNames with names a, b, c
+		cn := jwt.NewClaimNameFilter("a", "b", "c")
+
+		// Create a token with claims a, b, c, d, e
+		token := jwt.New()
+		require.NoError(t, token.Set("a", "value_a"), "token.Set should succeed")
+		require.NoError(t, token.Set("b", "value_b"), "token.Set should succeed")
+		require.NoError(t, token.Set("c", "value_c"), "token.Set should succeed")
+		require.NoError(t, token.Set("d", "value_d"), "token.Set should succeed")
+		require.NoError(t, token.Set("e", "value_e"), "token.Set should succeed")
+
+		// Reject should return a token with only claims d, e
+		rejected, err := cn.Reject(token)
+		require.NoError(t, err, "cn.Reject should succeed")
+
+		// Check that rejected token contains only d, e
+		require.False(t, rejected.Has("a"), "rejected token should not have claim 'a'")
+		require.False(t, rejected.Has("b"), "rejected token should not have claim 'b'")
+		require.False(t, rejected.Has("c"), "rejected token should not have claim 'c'")
+		require.True(t, rejected.Has("d"), "rejected token should have claim 'd'")
+		require.True(t, rejected.Has("e"), "rejected token should have claim 'e'")
+
+		// Verify values are preserved
+		var val string
+		require.NoError(t, rejected.Get("d", &val), "rejected.Get should succeed")
+		require.Equal(t, "value_d", val, "value for claim 'd' should be preserved")
+	})
+
+	t.Run("Error handling with Clone", func(t *testing.T) {
+		// Mock a token that will fail Clone
+		token := jwt.New()
+		// Setting a value that cannot be retrieved to simulate a Clone error
+		// We can't actually create a token that fails to clone in the test code,
+		// so this test is a bit artificial. In real code, we'd use a mock.
+
+		// But let's at least verify Filter and Reject don't panic
+		cn := jwt.NewClaimNameFilter("a", "b", "c")
+		_, err := cn.Filter(token)
+		require.NoError(t, err, "cn.Filter should succeed even for an empty token")
+
+		_, err = cn.Reject(token)
+		require.NoError(t, err, "cn.Reject should succeed even for an empty token")
+	})
+
+	t.Run("Empty ClaimNames", func(t *testing.T) {
+		// Create an empty ClaimNames
+		cn := jwt.NewClaimNameFilter()
+
+		// Create a token with claims a, b, c
+		token := jwt.New()
+		require.NoError(t, token.Set("a", "value_a"), "token.Set should succeed")
+		require.NoError(t, token.Set("b", "value_b"), "token.Set should succeed")
+		require.NoError(t, token.Set("c", "value_c"), "token.Set should succeed")
+
+		// Filter with empty ClaimNames should result in an empty token
+		filtered, err := cn.Filter(token)
+		require.NoError(t, err, "cn.Filter should succeed")
+		require.Empty(t, filtered.Keys(), "filtered token should have no claims")
+
+		// Reject with empty ClaimNames should result in a copy of the original token
+		rejected, err := cn.Reject(token)
+		require.NoError(t, err, "cn.Reject should succeed")
+
+		// Check that rejected token has the same keys as original, ignoring order
+		originalKeys := token.Keys()
+		rejectedKeys := rejected.Keys()
+		require.ElementsMatch(t, originalKeys, rejectedKeys, "rejected token should have the same claims as the original")
+	})
+
+	t.Run("Concurrency safety", func(t *testing.T) {
+		// This is more of a logical test than an actual concurrency test
+		// but it ensures the mutex is being used correctly in the Filter/Reject methods
+
+		cn := jwt.NewClaimNameFilter("a", "b", "c")
+		token := jwt.New()
+		require.NoError(t, token.Set("a", "value_a"), "token.Set should succeed")
+		require.NoError(t, token.Set("d", "value_d"), "token.Set should succeed")
+
+		// Should not deadlock or have race conditions
+		filtered, err := cn.Filter(token)
+		require.NoError(t, err, "cn.Filter should succeed")
+		require.True(t, filtered.Has("a"), "filtered token should have claim 'a'")
+		require.False(t, filtered.Has("d"), "filtered token should not have claim 'd'")
+
+		rejected, err := cn.Reject(token)
+		require.NoError(t, err, "cn.Reject should succeed")
+		require.False(t, rejected.Has("a"), "rejected token should not have claim 'a'")
+		require.True(t, rejected.Has("d"), "rejected token should have claim 'd'")
+	})
+}
+
+func TestStandardClaimsFilter(t *testing.T) {
+	t.Run("Filter standard claims", func(t *testing.T) {
+		// Create a token with standard and custom claims
+		token, err := jwt.NewBuilder().
+			Audience([]string{"aud1", "aud2"}).
+			Expiration(time.Unix(aLongLongTimeAgo, 0)).
+			IssuedAt(time.Unix(aLongLongTimeAgo, 0)).
+			Issuer("issuer").
+			JwtID("jwt-id").
+			NotBefore(time.Unix(aLongLongTimeAgo, 0)).
+			Subject("subject").
+			Claim("custom1", "value1").
+			Claim("custom2", "value2").
+			Build()
+		require.NoError(t, err, "token should be created successfully")
+		require.NotNil(t, token, "token should not be nil")
+
+		stdFilter := jwt.StandardClaimsFilter()
+
+		t.Run("Filter standard claims", func(t *testing.T) {
+			// Filter should return a token with only standard claims
+			filtered, err := stdFilter.Filter(token)
+			require.NoError(t, err, "filter.Filter should succeed")
+
+			// Verify standard claims are present
+			require.True(t, filtered.Has(jwt.AudienceKey), "filtered token should have audience claim")
+			require.True(t, filtered.Has(jwt.ExpirationKey), "filtered token should have expiration claim")
+			require.True(t, filtered.Has(jwt.IssuedAtKey), "filtered token should have issued-at claim")
+			require.True(t, filtered.Has(jwt.IssuerKey), "filtered token should have issuer claim")
+			require.True(t, filtered.Has(jwt.JwtIDKey), "filtered token should have jwt-id claim")
+			require.True(t, filtered.Has(jwt.NotBeforeKey), "filtered token should have not-before claim")
+			require.True(t, filtered.Has(jwt.SubjectKey), "filtered token should have subject claim")
+
+			// Verify custom claims are not present
+			require.False(t, filtered.Has("custom1"), "filtered token should not have custom1 claim")
+			require.False(t, filtered.Has("custom2"), "filtered token should not have custom2 claim")
+
+			// Verify values are preserved
+			var issuer string
+			require.NoError(t, filtered.Get(jwt.IssuerKey, &issuer), "filtered.Get should succeed")
+			require.Equal(t, "issuer", issuer, "value for issuer claim should be preserved")
+		})
+		t.Run("Reject standard claims", func(t *testing.T) {
+			// Reject should return a token with only custom claims
+			rejected, err := stdFilter.Reject(token)
+			require.NoError(t, err, "filter.Reject should succeed")
+
+			// Verify standard claims are not present
+			require.False(t, rejected.Has(jwt.AudienceKey), "rejected token should not have audience claim")
+			require.False(t, rejected.Has(jwt.ExpirationKey), "rejected token should not have expiration claim")
+			require.False(t, rejected.Has(jwt.IssuedAtKey), "rejected token should not have issued-at claim")
+			require.False(t, rejected.Has(jwt.IssuerKey), "rejected token should not have issuer claim")
+			require.False(t, rejected.Has(jwt.JwtIDKey), "rejected token should not have jwt-id claim")
+			require.False(t, rejected.Has(jwt.NotBeforeKey), "rejected token should not have not-before claim")
+			require.False(t, rejected.Has(jwt.SubjectKey), "rejected token should not have subject claim")
+
+			// Verify custom claims are present
+			require.True(t, rejected.Has("custom1"), "rejected token should have custom1 claim")
+			require.True(t, rejected.Has("custom2"), "rejected token should have custom2 claim")
+
+			// Verify values are preserved
+			var customValue string
+			require.NoError(t, rejected.Get("custom1", &customValue), "rejected.Get should succeed")
+			require.Equal(t, "value1", customValue, "value for custom1 claim should be preserved")
+		})
+	})
+}