Repository: jwtk/jjwt (java)

Issue:
Support empty claims values
<!--
PLEASE DO NOT USE GITHUB ISSUES TO ASK QUESTIONS! 

The JJWT team uses GitHub Issues only to track actionable work that requires changes to the JJWT codebase.

If you have a usability or general question about JJWT, please instead post your question to StackOverflow using the following JJWT-specific link:

https://stackoverflow.com/questions/ask?tags=jjwt&guided=false

To help ensure you receive a timely response, please follow the StackOverflow guidelines on how to ask a good question: https://stackoverflow.com/help/how-to-ask

Ok, now that that's out of the way - if you're still sure you need to report a bug, please fill in the sections below...
-->

**Describe the bug**
In versions before 0.12.x it was possible to add empty values in the claims of the JWT (for example `.claim("test", "")` or `.claim("test", new ArrayList<>())`). In versions after `0.12.0` empty claims are not serialized (compacted) anymore which breaks my use case where the receiving end expects the JWT to have all keys (even ones with empty values) in the claim map.

**To Reproduce**
Try to create a JWT with some claims that have empty values (See above example)
My exact code: https://github.com/RaphiMC/ViaBedrock/blob/5674c4b0a04f64b5560064ba326d8ac3e6e5403a/src/main/java/net/raphimc/viabedrock/protocol/packets/LoginPackets.java#L341

**Expected behavior**
I am expecting to be able to add empty values into the claims map (See above example)


Pull request description:
Closes #858.
Custom claims can now be empty again (behavior for <= 0.11.5).

Patch:
diff --git a/api/src/main/java/io/jsonwebtoken/lang/Objects.java b/api/src/main/java/io/jsonwebtoken/lang/Objects.java
index 12a92df..4284713 100644
--- a/api/src/main/java/io/jsonwebtoken/lang/Objects.java
+++ b/api/src/main/java/io/jsonwebtoken/lang/Objects.java
@@ -94,7 +94,7 @@ public final class Objects {
      * Returns {@code true} if the specified argument:
      * <ol>
      *     <li>is {@code null}, or</li>
-     *     <li>is a String and {@link Strings#hasText(String)} is {@code false}, or</li>
+     *     <li>is a CharSequence and {@link Strings#hasText(CharSequence)} is {@code false}, or</li>
      *     <li>is a Collection or Map with zero size, or</li>
      *     <li>is an empty array</li>
      * </ol>
@@ -106,7 +106,7 @@ public final class Objects {
      */
     public static boolean isEmpty(Object v) {
         return v == null ||
-                (v instanceof String && !Strings.hasText((String) v)) ||
+                (v instanceof CharSequence && !Strings.hasText((CharSequence) v)) ||
                 (v instanceof Collection && Collections.isEmpty((Collection<?>) v)) ||
                 (v instanceof Map && Collections.isEmpty((Map<?, ?>) v)) ||
                 (v.getClass().isArray() && Array.getLength(v) == 0);
diff --git a/impl/src/main/java/io/jsonwebtoken/impl/ParameterMap.java b/impl/src/main/java/io/jsonwebtoken/impl/ParameterMap.java
index 2daf1b7..c8e462e 100644
--- a/impl/src/main/java/io/jsonwebtoken/impl/ParameterMap.java
+++ b/impl/src/main/java/io/jsonwebtoken/impl/ParameterMap.java
@@ -124,13 +124,6 @@ public class ParameterMap implements Map<String, Object>, ParameterReadable, Nam
         return values.get(o);
     }
 
-    private static Object clean(Object o) {
-        if (o instanceof String) {
-            o = Strings.clean((String) o);
-        }
-        return o;
-    }
-
     /**
      * Convenience method to put a value for an idiomatic param.
      *
@@ -143,7 +136,7 @@ public class ParameterMap implements Map<String, Object>, ParameterReadable, Nam
         assertMutable();
         Assert.notNull(param, "Parameter cannot be null.");
         Assert.hasText(param.getId(), "Parameter id cannot be null or empty.");
-        return apply(param, clean(value));
+        return apply(param, value);
     }
 
     @Override
@@ -156,12 +149,12 @@ public class ParameterMap implements Map<String, Object>, ParameterReadable, Nam
             return put(param, value);
         } else {
             // non-standard or custom property, just apply directly:
-            return nullSafePut(name, clean(value));
+            return nullSafePut(name, value);
         }
     }
 
     private Object nullSafePut(String name, Object value) {
-        if (Objects.isEmpty(value)) {
+        if (value == null) {
             return remove(name);
         } else {
             this.idiomaticValues.put(name, value);
@@ -199,9 +192,8 @@ public class ParameterMap implements Map<String, Object>, ParameterReadable, Nam
             String msg = sb.toString();
             throw new IllegalArgumentException(msg, e);
         }
-        Object retval = nullSafePut(id, canonicalValue);
         this.idiomaticValues.put(id, idiomaticValue);
-        return retval;
+        return this.values.put(id, canonicalValue);
     }
 
     @Override


Test patch:
diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy
index bfeebeb..3d0dfa9 100644
--- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy
+++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy
@@ -219,13 +219,6 @@ class DefaultJwtBuilderTest {
         assertEquals b.claimsBuilder.foo, 'bar'
     }
 
-    @Test
-    void testClaimEmptyString() {
-        String value = ' '
-        builder.claim('foo', value)
-        assertTrue builder.claimsBuilder.isEmpty() // shouldn't populate claims instance
-    }
-
     @Test
     void testExistingClaimsAndSetClaim() {
         Claims c = Jwts.claims().add('foo', 'bar').build()
diff --git a/impl/src/test/groovy/io/jsonwebtoken/issues/Issue858Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/issues/Issue858Test.groovy
new file mode 100644
index 0000000..a470c10
--- /dev/null
+++ b/impl/src/test/groovy/io/jsonwebtoken/issues/Issue858Test.groovy
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2023 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.issues
+
+import io.jsonwebtoken.Jwts
+import org.junit.Test
+
+import static org.junit.Assert.assertEquals
+
+class Issue858Test {
+
+    @Test
+    void testEmptyAndNullEntries() {
+        def jwt = Jwts.builder()
+                .subject('Joe')
+                .claim('foo', '')       // empty allowed
+                .claim('list', [])            // empty allowed
+                .claim('map', [:])            // empty map allowed
+                .claim('another', null) // null not allowed (same behavior since <= 0.11.5), won't be added
+                .compact()
+
+        def claims = Jwts.parser().unsecured().build().parseUnsecuredClaims(jwt).getPayload()
+        assertEquals 4, claims.size()
+        assertEquals 'Joe', claims.getSubject()
+        assertEquals '', claims.get('foo')
+        assertEquals([], claims.get('list'))
+        assertEquals([:], claims.get('map'))
+    }
+}