Repository: jwtk/jjwt (java)

Issue:
A single audience value gets converted into a set with one entry
**Describe the bug**

In the scenario when we first build claims with `DefaultClaimsBuilder` and then pass them to `JwtBuilder`, a single audience value gets converted somewhere on the way from `String` to `Set<String>`. **This is a breaking change for the APIs, which expect a string audience value.**

**To Reproduce**
Steps to reproduce the behavior:
1. Build claims:
```java
var claims = new DefaultClaimsBuilder().audience().single("https://login.salesforce.com").build();
```
2. Check the type of the claim value of in Debugger (must be `String`):
![image](https://github.com/jwtk/jjwt/assets/26978790/dc84e5ec-a275-4a2f-ae6b-a9a95228ce15)
3. Create a JWT builder with the claims object:
```java
var jwtBuilder = Jwts.builder().claims(claims);
```
4. Check again the type of the claim value in Debugger (see `jwtBuilder`; this time it's `Set<String>`):
![image](https://github.com/jwtk/jjwt/assets/26978790/d672db7d-1ccd-4f4b-b581-ee76938d84e9)


**Expected behavior**
In case of using `audience().single(...)` the type of the `audience` claim should stay `String` after calling `claims(...)` on the JWT builder.

**Workaround**
Note, if setting the claim directly on the JWT builder, the type is preserved:
```java
var jwtBuilder = Jwts.builder().audience().single("https://login.salesforce.com");
```


Pull request:
Audience single string claim data type retention when copying
Ensured a single string `aud` (Audience) claim would be retained (without converting it to a `Set`) when copying/applying a source Claims instance to a destination Claims builder.

Updated CHANGELOG.md accordingly.

Fixes #890.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7133667..4f1b9f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 ## Release Notes
 
+### 0.12.4
+
+This patch release:
+
+* Ensures Android environments and older `org.json` library usages can parse JSON from a `JwtBuilder`-provided
+  `java.io.Reader` instance. [Issue 882](https://github.com/jwtk/jjwt/issues/882).
+* Ensures a single string `aud` (Audience) claim is retained (without converting it to a `Set`) when copying/applying a 
+  source Claims instance to a destination Claims builder. [Issue 890](https://github.com/jwtk/jjwt/issues/890).
+
 ### 0.12.3
 
 This patch release:
diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java
index 8636b53..7740fe2 100644
--- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java
+++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java
@@ -24,6 +24,7 @@ import io.jsonwebtoken.lang.MapMutator;
 import io.jsonwebtoken.lang.Strings;
 
 import java.util.Date;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -46,6 +47,31 @@ public class DelegatingClaimsMutator<T extends MapMutator<String, Object, T> & C
         return self();
     }
 
+    @Override
+    public Object put(String key, Object value) {
+        if (AUDIENCE_STRING.getId().equals(key)) { // https://github.com/jwtk/jjwt/issues/890
+            if (value instanceof String) {
+                Object existing = get(key);
+                //noinspection deprecation
+                audience().single((String) value);
+                return existing;
+            }
+            // otherwise ensure that the Parameter type is the RFC-default data type (JSON Array of Strings):
+            getAudience();
+        }
+        // otherwise retain expected behavior:
+        return super.put(key, value);
+    }
+
+    @Override
+    public void putAll(Map<? extends String, ?> m) {
+        if (m == null) return;
+        for (Map.Entry<? extends String, ?> entry : m.entrySet()) {
+            String s = entry.getKey();
+            put(s, entry.getValue()); // ensure local put is called per https://github.com/jwtk/jjwt/issues/890
+        }
+    }
+
     <F> F get(Parameter<F> param) {
         return this.DELEGATE.get(param);
     }


Test patch:
diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy
index 5ded803..95e2e8e 100644
--- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy
+++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy
@@ -23,6 +23,7 @@ import io.jsonwebtoken.impl.lang.Bytes
 import io.jsonwebtoken.impl.lang.Services
 import io.jsonwebtoken.impl.security.*
 import io.jsonwebtoken.io.Decoders
+import io.jsonwebtoken.io.Deserializer
 import io.jsonwebtoken.io.Encoders
 import io.jsonwebtoken.io.Serializer
 import io.jsonwebtoken.lang.Strings
@@ -1167,6 +1168,36 @@ class JwtsTest {
                 .build().parseSignedClaims(jws)
     }
 
+    /**
+     * Asserts that if a {@link Jwts#claims()} builder is used to set a single string Audience value, and the
+     * resulting constructed {@link Claims} instance is used on a {@link Jwts#builder()}, that the resulting JWT
+     * retains a single-string Audience value (and it is not automatically coerced to a {@code Set<String>}).
+     *
+     * @since 0.12.4
+     * @see <a href="https://github.com/jwtk/jjwt/issues/890">JJWT Issue 890</a>
+     */
+    @Test
+    void testClaimsBuilderSingleStringAudienceThenJwtBuilder() {
+
+        def key = TestKeys.HS256
+        def aud = 'foo'
+        def claims = Jwts.claims().audience().single(aud).build()
+        def jws = Jwts.builder().claims(claims).signWith(key).compact()
+
+        // we can't use a JwtParser here because that will automatically normalize a single String value as a
+        // Set<String> for app developer convenience.  So we assert that the JWT looks as expected by simple
+        // json parsing and map inspection
+
+        int i = jws.indexOf('.')
+        int j = jws.lastIndexOf('.')
+        def b64 = jws.substring(i, j)
+        def json = Strings.utf8(Decoders.BASE64URL.decode(b64))
+        def deser = Services.loadFirst(Deserializer)
+        def m = deser.deserialize(new StringReader(json)) as Map<String,?>
+
+        assertEquals aud, m.get('aud') // single string value
+    }
+
     //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20
     @Test
     void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() {