From b0401547909a3255eed02f615a63d7fd3c007162 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:25:07 -0800 Subject: [PATCH] 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. --- CHANGELOG.md | 9 ++++++ .../impl/DelegatingClaimsMutator.java | 26 ++++++++++++++++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 31 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7133667dc..4f1b9f7d7 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 8636b5351..7740fe275 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.Strings; import java.util.Date; +import java.util.Map; import java.util.Set; /** @@ -46,6 +47,31 @@ T put(Parameter param, F value) { 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 m) { + if (m == null) return; + for (Map.Entry 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 get(Parameter param) { return this.DELEGATE.get(param); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 5ded80389..95e2e8e07 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}). + * + * @since 0.12.4 + * @see JJWT Issue 890 + */ + @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 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 + + assertEquals aud, m.get('aud') // single string value + } + //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20 @Test void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() {