diff --git a/README.md b/README.md index d550e5118..e42a87fd6 100644 --- a/README.md +++ b/README.md @@ -3322,11 +3322,11 @@ String jwe = Jwts.builder().audience("Alice") .compact(); // Alice receives and decrypts the compact JWE: -String audience = Jwts.parser() +Set audience = Jwts.parser() .decryptWith(pair.getPrivate()) // <-- Alice's RSA private key .build().parseClaimsJwe(jwe).getPayload().getAudience(); -assert "Alice".equals(audience); +assert audience.contains("Alice"); ``` @@ -3390,11 +3390,11 @@ String jwe = Jwts.builder().audience("Alice") .compact(); // Alice receives and decrypts the compact JWE: -String audience = Jwts.parser() +Set audience = Jwts.parser() .decryptWith(pair.getPrivate()) // <-- Alice's EC private key .build().parseClaimsJwe(jwe).getPayload().getAudience(); -assert "Alice".equals(audience); +assert audience.contains("Alice"); ``` diff --git a/api/src/main/java/io/jsonwebtoken/Claims.java b/api/src/main/java/io/jsonwebtoken/Claims.java index e29e74bef..4d43d1cf5 100644 --- a/api/src/main/java/io/jsonwebtoken/Claims.java +++ b/api/src/main/java/io/jsonwebtoken/Claims.java @@ -17,6 +17,7 @@ import java.util.Date; import java.util.Map; +import java.util.Set; /** * A JWT Claims set. @@ -38,25 +39,39 @@ */ public interface Claims extends Map, Identifiable { - /** JWT {@code Issuer} claims parameter name: "iss" */ + /** + * JWT {@code Issuer} claims parameter name: "iss" + */ String ISSUER = "iss"; - /** JWT {@code Subject} claims parameter name: "sub" */ + /** + * JWT {@code Subject} claims parameter name: "sub" + */ String SUBJECT = "sub"; - /** JWT {@code Audience} claims parameter name: "aud" */ + /** + * JWT {@code Audience} claims parameter name: "aud" + */ String AUDIENCE = "aud"; - /** JWT {@code Expiration} claims parameter name: "exp" */ + /** + * JWT {@code Expiration} claims parameter name: "exp" + */ String EXPIRATION = "exp"; - /** JWT {@code Not Before} claims parameter name: "nbf" */ + /** + * JWT {@code Not Before} claims parameter name: "nbf" + */ String NOT_BEFORE = "nbf"; - /** JWT {@code Issued At} claims parameter name: "iat" */ + /** + * JWT {@code Issued At} claims parameter name: "iat" + */ String ISSUED_AT = "iat"; - /** JWT {@code JWT ID} claims parameter name: "jti" */ + /** + * JWT {@code JWT ID} claims parameter name: "jti" + */ String ID = "jti"; /** @@ -81,7 +96,7 @@ public interface Claims extends Map, Identifiable { * * @return the JWT {@code aud} value or {@code null} if not present. */ - String getAudience(); + Set getAudience(); /** * Returns the JWT @@ -124,7 +139,8 @@ public interface Claims extends Map, Identifiable { * * @return the JWT {@code jti} value or {@code null} if not present. */ - @Override // just for JavaDoc specific to the JWT spec + @Override + // just for JavaDoc specific to the JWT spec String getId(); /** @@ -133,14 +149,15 @@ public interface Claims extends Map, Identifiable { *

JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more * complex is expected to be already converted to your desired type by the JSON * {@link io.jsonwebtoken.io.Deserializer Deserializer} implementation. You may specify a custom Deserializer for a - * JwtParser with the desired conversion configuration via the {@link JwtParserBuilder#deserializeJsonWith} method. + * JwtParser with the desired conversion configuration via the + * {@link JwtParserBuilder#deserializer deserializer} method. * See custom JSON processor for more * information. If using Jackson, you can specify custom claim POJO types as described in * custom claim types. * - * @param claimName name of claim + * @param claimName name of claim * @param requiredType the type of the value expected to be returned - * @param the type of the value expected to be returned + * @param the type of the value expected to be returned * @return the JWT {@code claimName} value or {@code null} if not present. * @throws RequiredTypeException throw if the claim value is not null and not of type {@code requiredType} */ diff --git a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java index 0c5829ee1..1941348ba 100644 --- a/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java +++ b/api/src/main/java/io/jsonwebtoken/ClaimsMutator.java @@ -15,6 +15,7 @@ */ package io.jsonwebtoken; +import java.util.Collection; import java.util.Date; /** @@ -72,8 +73,12 @@ public interface ClaimsMutator> { T subject(String sub); /** - * Sets the JWT - * aud (audience) value. A {@code null} value will remove the property from the JSON map. + * Sets the JWT aud (audience) + * Claim as a single String, NOT a String array. This method exists only for producing + * JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; it is + * strongly recommended to avoid calling this method whenever possible and favor the + * {@link #audience(String)} or {@link #audience(Collection)} methods instead, as they ensure a single deterministic + * data type for recipients. * * @param aud the JWT {@code aud} value or {@code null} to remove the property from the JSON map. * @return the {@code Claims} instance for method chaining. @@ -84,14 +89,45 @@ public interface ClaimsMutator> { T setAudience(String aud); /** - * Sets the JWT - * aud (audience) value. A {@code null} value will remove the property from the JSON map. + * Sets the JWT aud (audience) + * Claim as a single String, NOT a String array. This method exists only for producing + * JWTs sent to legacy recipients that are unable to interpret the {@code aud} value as a JSON String Array; it is + * strongly recommended to avoid calling this method whenever possible and favor the + * {@link #audience(String)} or {@link #audience(Collection)} methods instead, as they ensure a single deterministic + * data type for recipients. + * + * @param aud the value to use as the {@code aud} Claim single-String value (and not an array of Strings), or + * {@code null} to remove the property from the JSON map. + * @return the instance for method chaining + * @since JJWT_RELEASE_VERSION + * @deprecated This is technically not deprecated because the JWT RFC mandates support for single string values, + * but it is marked as deprecated to discourage its use when possible. + */ + // DO NOT REMOVE EVER. This is a required RFC feature, but marked as deprecated to discourage its use + @Deprecated + T audienceSingle(String aud); + + /** + * Adds the specified {@code aud} value to the {@link #audience(Collection) audience} Claim set (JSON Array). This + * method may be called multiple times. * - * @param aud the JWT {@code aud} value or {@code null} to remove the property from the JSON map. + * @param aud a JWT {@code aud} value to add to the {@link #audience(Collection) audience} Claim set. * @return the {@code Claims} instance for method chaining. + * @throws IllegalArgumentException if the {@code aud} argument is null or empty. + * @since JJWT_RELEASE_VERSION + */ + T audience(String aud) throws IllegalArgumentException; + + /** + * Sets the JWT aud (audience) + * Claim set, replacing any previous value(s). + * + * @param aud the values to set as the {@code aud} Claim set (JSON Array), or {@code null}/empty to remove the + * {@code aud} claim from the JSON map entirely. + * @return the instance for method chaining * @since JJWT_RELEASE_VERSION */ - T audience(String aud); + T audience(Collection aud); /** * Sets the JWT diff --git a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java index 44c0657d3..e12a6f99e 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java @@ -131,7 +131,7 @@ public interface JwtParserBuilder extends Builder { /** * Ensures that the specified {@code aud} exists in the parsed JWT. If missing or if the parsed - * value does not equal the specified value, an exception will be thrown indicating that the + * value does not contain the specified value, an exception will be thrown indicating that the * JWT is invalid and may not be used. * * @param audience the required value of the {@code aud} header parameter. @@ -328,7 +328,7 @@ public interface JwtParserBuilder extends Builder { * {@link #verifyWith(SecretKey)} for type safety, to reflect accurate naming of the concept, and for name * congruence with the {@link #decryptWith(SecretKey)} method.

* - *

This method merely delegates directly to {@link #verifyWith(SecretKey) or {@link #verifyWith(PublicKey)}}.

+ *

This method merely delegates directly to {@link #verifyWith(SecretKey)} or {@link #verifyWith(PublicKey)}}.

* * @param key the algorithm-specific signature verification key to use to verify all encountered JWS digital * signatures. diff --git a/api/src/main/java/io/jsonwebtoken/lang/Collections.java b/api/src/main/java/io/jsonwebtoken/lang/Collections.java index 2ca8f5e2f..0dab4f5a7 100644 --- a/api/src/main/java/io/jsonwebtoken/lang/Collections.java +++ b/api/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -83,6 +83,24 @@ public static List of(T... elements) { return java.util.Collections.unmodifiableList(Arrays.asList(elements)); } + /** + * Returns the specified collection as a {@link Set} instance. + * + * @param c the collection to represent as a set + * @param collection element type + * @return a type-safe immutable {@code Set} containing the specified collection elements. + * @since JJWT_RELEASE_VERSION + */ + public static Set asSet(Collection c) { + if (c instanceof Set) { + return (Set) c; + } + if (isEmpty(c)) { + return java.util.Collections.emptySet(); + } + return java.util.Collections.unmodifiableSet(new LinkedHashSet<>(c)); + } + /** * Returns a type-safe immutable {@code Set} containing the specified array elements. * diff --git a/api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java b/api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java index b223144b0..1202a46e9 100644 --- a/api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java +++ b/api/src/main/java/io/jsonwebtoken/security/PrivateKeyBuilder.java @@ -16,8 +16,23 @@ package io.jsonwebtoken.security; import java.security.PrivateKey; +import java.security.Provider; import java.security.PublicKey; +/** + * A builder that allows a {@code PrivateKey} to be transparently associated with a {@link #provider(Provider)} or + * {@link #publicKey(PublicKey)} if necessary for algorithms that require them. + * + * @since JJWT_RELEASE_VERSION + */ public interface PrivateKeyBuilder extends KeyBuilder { + + /** + * Sets the private key's corresponding {@code PublicKey} so that its public key material will be available to + * algorithms that require it. + * + * @param publicKey the private key's corresponding {@code PublicKey} + * @return the builder for method chaining. + */ PrivateKeyBuilder publicKey(PublicKey publicKey); } diff --git a/api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy b/api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy new file mode 100644 index 000000000..53181d7b5 --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy @@ -0,0 +1,52 @@ +/* + * 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.lang + +import org.junit.Test + +import static org.junit.Assert.* + +class CollectionsTest { + + @Test + void testAsSetFromNull() { + assertSame java.util.Collections.emptySet(), Collections.asSet(null) + } + + @Test + void testAsSetFromEmpty() { + def list = [] + assertSame java.util.Collections.emptySet(), Collections.asSet(list) + } + + @Test + void testAsSetFromSet() { + def set = Collections.setOf('foo') + assertSame set, Collections.asSet(set) + } + + @Test + void testAsSetFromList() { + def list = Collections.of('one', 'two') + def set = Collections.asSet(list) + assertTrue set.containsAll(list) + try { + set.add('another') + fail() + } catch (UnsupportedOperationException ignored) { // expected, asSet returns immutable instances + } + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java index 1fad20c5b..045c3dec0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -25,6 +25,7 @@ import java.util.Date; import java.util.Map; +import java.util.Set; public class DefaultClaims extends FieldMap implements Claims { @@ -38,7 +39,7 @@ public class DefaultClaims extends FieldMap implements Claims { static final Field ISSUER = Fields.string(Claims.ISSUER, "Issuer"); static final Field SUBJECT = Fields.string(Claims.SUBJECT, "Subject"); - static final Field AUDIENCE = Fields.string(Claims.AUDIENCE, "Audience"); + static final Field> AUDIENCE = Fields.stringSet(Claims.AUDIENCE, "Audience"); static final Field EXPIRATION = Fields.rfcDate(Claims.EXPIRATION, "Expiration Time"); static final Field NOT_BEFORE = Fields.rfcDate(Claims.NOT_BEFORE, "Not Before"); static final Field ISSUED_AT = Fields.rfcDate(Claims.ISSUED_AT, "Issued At"); @@ -51,6 +52,10 @@ protected DefaultClaims() { // visibility for testing super(FIELDS); } + public DefaultClaims(FieldMap m) { + super(m.FIELDS, m); + } + public DefaultClaims(Map map) { super(FIELDS, map); } @@ -71,7 +76,7 @@ public String getSubject() { } @Override - public String getAudience() { + public Set getAudience() { return get(AUDIENCE); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java index 462128aa1..f906b6437 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java @@ -52,7 +52,7 @@ public DefaultJweHeaderMutator(DefaultJweHeaderMutator src) { // MapMutator methods // ============================================================= - private T put(Field field, Object value) { + private T put(Field field, F value) { this.DELEGATE.put(field, value); return self(); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index ddf71fddc..afc0247bc 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -61,6 +61,7 @@ import java.security.Provider; import java.security.PublicKey; import java.security.SecureRandom; +import java.util.Collection; import java.util.Date; import java.util.Map; @@ -393,7 +394,14 @@ public JwtBuilder subject(String sub) { @Override public JwtBuilder setAudience(String aud) { - return audience(aud); + this.claimsBuilder.setAudience(aud); + return this; + } + + @Override + public JwtBuilder audienceSingle(String aud) { + this.claimsBuilder.audienceSingle(aud); + return this; } @Override @@ -402,6 +410,12 @@ public JwtBuilder audience(String aud) { return this; } + @Override + public JwtBuilder audience(Collection aud) { + this.claimsBuilder.audience(aud); + return this; + } + @Override public JwtBuilder setExpiration(Date exp) { return expiration(exp); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index af424285c..1834279e0 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -52,6 +52,7 @@ import io.jsonwebtoken.io.Deserializer; import io.jsonwebtoken.lang.Arrays; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.DateFormats; import io.jsonwebtoken.lang.Strings; import io.jsonwebtoken.security.AeadAlgorithm; @@ -89,6 +90,9 @@ public class DefaultJwtParser implements JwtParser { "used to encrypt, and PrivateKeys are used to decrypt."; public static final String INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was: %s."; + + public static final String MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE = + "Missing expected '%s' value in '%s' claim %s."; public static final String MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE = "Expected %s claim to be: %s, but was not " + "present in the JWT claims."; @@ -627,9 +631,25 @@ private void validateExpectedClaims(Header header, Claims claims) { } if (actualClaimValue == null) { - String msg = String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, - expectedClaimName, expectedClaimValue); + boolean collection = expectedClaimValue instanceof Collection; + String msg = "Missing '" + expectedClaimName + "' claim. Expected value"; + if (collection) { + msg += "s: " + expectedClaimValue; + } else { + msg += ": " + expectedClaimValue; + } throw new MissingClaimException(header, claims, expectedClaimName, expectedClaimValue, msg); + } else if (expectedClaimValue instanceof Collection) { + Collection expectedValues = (Collection) expectedClaimValue; + Collection actualValues = actualClaimValue instanceof Collection ? (Collection) actualClaimValue : + Collections.setOf(actualClaimValue); + for (Object expectedValue : expectedValues) { + if (!Collections.contains(actualValues.iterator(), expectedValue)) { + String msg = String.format(MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE, + expectedValue, expectedClaimName, actualValues); + throw new IncorrectClaimException(header, claims, expectedClaimName, expectedClaimValue, msg); + } + } } else if (!expectedClaimValue.equals(actualClaimValue)) { String msg = String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, expectedClaimName, expectedClaimValue, actualClaimValue); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java index 751c94c6d..e798cfee9 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java @@ -155,7 +155,7 @@ public JwtParserBuilder requireIssuer(String issuer) { @Override public JwtParserBuilder requireAudience(String audience) { - expectedClaims.setAudience(audience); + expectedClaims.audience(audience); return this; } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java index 44227b2df..aaa31b265 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java @@ -18,9 +18,16 @@ import io.jsonwebtoken.ClaimsMutator; import io.jsonwebtoken.impl.lang.DelegatingMapMutator; import io.jsonwebtoken.impl.lang.Field; +import io.jsonwebtoken.impl.lang.Fields; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.MapMutator; +import io.jsonwebtoken.lang.Strings; +import java.util.Collection; import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Set; /** * @param subclass type @@ -30,15 +37,22 @@ public class DelegatingClaimsMutator & C extends DelegatingMapMutator implements ClaimsMutator { + private static final Field AUDIENCE_STRING = + Fields.string(DefaultClaims.AUDIENCE.getId(), DefaultClaims.AUDIENCE.getName()); + protected DelegatingClaimsMutator() { super(new FieldMap(DefaultClaims.FIELDS)); } - T put(Field field, Object value) { + T put(Field field, F value) { this.DELEGATE.put(field, value); return self(); } + F get(Field field) { + return this.DELEGATE.get(field); + } + @Override public T setIssuer(String iss) { return issuer(iss); @@ -61,12 +75,49 @@ public T subject(String sub) { @Override public T setAudience(String aud) { - return audience(aud); + return audienceSingle(aud); + } + + private Set getAudience() { + // caller expects that we're working with a String so ensure that: + if (!this.DELEGATE.FIELDS.get(AUDIENCE_STRING.getId()).supports(Collections.emptySet())) { + String existing = get(AUDIENCE_STRING); + remove(AUDIENCE_STRING.getId()); // clear out any canonical/idiomatic values since we're replacing + setDelegate(this.DELEGATE.replace(DefaultClaims.AUDIENCE)); + if (Strings.hasText(existing)) { + put(DefaultClaims.AUDIENCE, Collections.setOf(existing)); // replace as Set + } + } + Set aud = get(DefaultClaims.AUDIENCE); + return aud != null ? aud : Collections.emptySet(); + } + + @Override + public T audienceSingle(String aud) { + if (!Strings.hasText(aud)) { + return put(DefaultClaims.AUDIENCE, null); + } + // otherwise it's an actual single string, we need to ensure that we can represent it as a single + // string by swapping out the AUDIENCE field if necessary: + if (this.DELEGATE.FIELDS.get(AUDIENCE_STRING.getId()).supports(Collections.emptySet())) { // need to swap: + remove(AUDIENCE_STRING.getId()); //remove any existing value, as conversion will throw an exception + setDelegate(this.DELEGATE.replace(AUDIENCE_STRING)); + } + return put(AUDIENCE_STRING, aud); } @Override public T audience(String aud) { - return put(DefaultClaims.AUDIENCE, aud); + aud = Assert.hasText(Strings.clean(aud), "Audience string value cannot be null or empty."); + Set set = new LinkedHashSet<>(getAudience()); + set.add(aud); + return audience(set); + } + + @Override + public T audience(Collection aud) { + getAudience(); //coerce to Set if necessary + return put(DefaultClaims.AUDIENCE, Collections.asSet(aud)); } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/FieldMap.java b/impl/src/main/java/io/jsonwebtoken/impl/FieldMap.java index 65d66f0ee..7fd53e245 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/FieldMap.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/FieldMap.java @@ -82,6 +82,11 @@ private void assertMutable() { } } + protected FieldMap replace(Field field) { + Registry> registry = Fields.replace(this.FIELDS, field); + return new FieldMap(registry, this, this.mutable); + } + @Override public String getName() { return "Map"; @@ -128,37 +133,39 @@ public Object get(Object o) { 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 a canonical field. + * Convenience method to put a value for an idiomatic field. * * @param field the field representing the property name to set * @param value the value to set - * @return the previous value for the field name, or {@code null} if there was no previous value + * @return the previous value for the field, or {@code null} if there was no previous value * @since JJWT_RELEASE_VERSION */ - protected Object put(Field field, Object value) { - return put(field.getId(), value); + protected final Object put(Field field, Object value) { + assertMutable(); + Assert.notNull(field, "Field cannot be null."); + Assert.hasText(field.getId(), "Field id cannot be null or empty."); + return apply(field, clean(value)); } @Override - public Object put(String name, Object value) { + public final Object put(String name, Object value) { assertMutable(); name = Assert.notNull(Strings.clean(name), "Member name cannot be null or empty."); - if (value instanceof String) { - value = Strings.clean((String) value); - } - return idiomaticPut(name, value); - } - - // ensures that if a property name matches an RFC-specified name, that value can be represented - // as an idiomatic type-safe Java value in addition to the canonical RFC/encoded value. - private Object idiomaticPut(String name, Object value) { - Assert.stateNotNull(name, "Name cannot be null."); // asserted by caller Field field = FIELDS.get(name); - if (field != null) { //Setting a JWA-standard property - let's ensure we can represent it idiomatically: - return apply(field, value); - } else { //non-standard/custom property: - return nullSafePut(name, value); + if (field != null) { + // standard property, represent it idiomatically: + return put(field, value); + } else { + // non-standard or custom property, just apply directly: + return nullSafePut(name, clean(value)); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java index 191ef074a..5b2f4709c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/lang/Fields.java @@ -16,6 +16,7 @@ package io.jsonwebtoken.impl.lang; import io.jsonwebtoken.lang.Arrays; +import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Registry; import java.math.BigInteger; @@ -23,8 +24,10 @@ import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Date; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; public final class Fields { @@ -84,4 +87,14 @@ public static Registry> registry(Registry> par set.addAll(Arrays.asList(fields)); return new IdRegistry<>("Field", set, true); } + + public static Registry> replace(Registry> registry, Field field) { + Assert.notEmpty(registry, "Registry cannot be null or empty."); + Assert.notNull(field, "Field cannot be null."); + String id = Assert.hasText(field.getId(), "Field id cannot be null or empty."); + Map> newFields = new LinkedHashMap<>(registry); + newFields.remove(id); // remove old/default + newFields.put(id, field); // add new one + return registry(newFields.values()); + } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java index f9655a43e..7d75d292b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultJwkContext.java @@ -20,6 +20,7 @@ import io.jsonwebtoken.impl.lang.Fields; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Registry; import io.jsonwebtoken.security.HashAlgorithm; import io.jsonwebtoken.security.Jwks; import io.jsonwebtoken.security.KeyOperation; @@ -30,7 +31,6 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.util.Collection; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -124,14 +124,11 @@ public DefaultJwkContext(Set> fields, JwkContext other, boolean remo @Override public JwkContext field(Field field) { - Assert.notNull(field, "Field cannot be null."); - Map> newFields = new LinkedHashMap<>(this.FIELDS); - newFields.remove(field.getId()); // remove old/default - newFields.put(field.getId(), field); // add new one - Set> fieldSet = new LinkedHashSet<>(newFields.values()); + Registry> registry = Fields.replace(this.FIELDS, field); + Set> fields = new LinkedHashSet<>(registry.values()); return this.key != null ? - new DefaultJwkContext<>(fieldSet, this, key) : - new DefaultJwkContext(fieldSet, this, false); + new DefaultJwkContext<>(fields, this, key) : + new DefaultJwkContext(fields, this, false); } @Override diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index d994ea58f..14aebd518 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -31,7 +31,7 @@ import java.security.SecureRandom import static io.jsonwebtoken.DateTestUtils.truncateMillis import static io.jsonwebtoken.impl.DefaultJwtParser.INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE -import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE +import static io.jsonwebtoken.impl.DefaultJwtParser.MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE import static org.junit.Assert.* @SuppressWarnings('GrDeprecatedAPIUsage') @@ -958,10 +958,8 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (MissingClaimException e) { - assertEquals( - String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, claimName, claimValue), - e.getMessage() - ) + String msg = "Missing '$claimName' claim. Expected value: $claimValue" + assertEquals msg, e.getMessage() } } @@ -1077,10 +1075,8 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (MissingClaimException e) { - assertEquals( - String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ISSUER, issuer), - e.getMessage() - ) + String msg = "Missing 'iss' claim. Expected value: $issuer" + assertEquals msg, e.message } } @@ -1099,7 +1095,60 @@ class JwtParserTest { build(). parseClaimsJws(compact) - assertEquals jwt.getPayload().getAudience(), audience + assertEquals audience, jwt.getPayload().getAudience().iterator().next() + } + + @Test + void testParseExpectedEqualAudiences() { + def one = 'one' + def two = 'two' + def expected = [one, two] + String jwt = Jwts.builder().audience(one).audience(two).compact() + def aud = Jwts.parser().enableUnsecured().requireAudience(one).requireAudience(two).build() + .parseClaimsJwt(jwt).getPayload().getAudience() + assertEquals expected.size(), aud.size() + assertTrue aud.containsAll(expected) + } + + @Test + void testParseAtLeastOneAudiences() { + def one = 'one' + + String jwt = Jwts.builder().audience(one).audience('two').compact() // more audiences than required + + def aud = Jwts.parser().enableUnsecured().requireAudience(one) // require only one + .build().parseClaimsJwt(jwt).getPayload().getAudience() + + assertNotNull aud + assertTrue aud.contains(one) + } + + @Test + void testParseMissingAudiences() { + def one = 'one' + def two = 'two' + String jwt = Jwts.builder().id('foo').compact() + try { + Jwts.parser().enableUnsecured().requireAudience(one).requireAudience(two).build().parseClaimsJwt(jwt) + fail() + } catch (MissingClaimException expected) { + String msg = "Missing 'aud' claim. Expected values: [$one, $two]" + assertEquals msg, expected.message + } + } + + @Test + void testParseSingleValueClaimExpectingMultipleValues() { + def one = 'one' + def two = 'two' + def expected = [one, two] + String jwt = Jwts.builder().claim('custom', one).compact() + try { + Jwts.parser().enableUnsecured().require('custom', expected).build().parseClaimsJwt(jwt) + } catch (IncorrectClaimException e) { + String msg = "Missing expected '$two' value in 'custom' claim [$one]." + assertEquals msg, e.message + } } @Test @@ -1120,10 +1169,9 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (IncorrectClaimException e) { - assertEquals( - String.format(INCORRECT_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, goodAudience, badAudience), - e.getMessage() - ) + String msg = String.format(MISSING_EXPECTED_CLAIM_VALUE_MESSAGE_TEMPLATE, goodAudience, + Claims.AUDIENCE, [badAudience]) + assertEquals msg, e.getMessage() } } @@ -1144,10 +1192,8 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (MissingClaimException e) { - assertEquals( - String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.AUDIENCE, audience), - e.getMessage() - ) + String msg = "Missing 'aud' claim. Expected values: [$audience]" + assertEquals msg, e.message } } @@ -1211,10 +1257,8 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (MissingClaimException e) { - assertEquals( - String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.SUBJECT, subject), - e.getMessage() - ) + String msg = "Missing 'sub' claim. Expected value: $subject" + assertEquals msg, e.getMessage() } } @@ -1278,10 +1322,8 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (MissingClaimException e) { - assertEquals( - String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, Claims.ID, id), - e.getMessage() - ) + String msg = "Missing 'jti' claim. Expected value: $id" + assertEquals msg, e.getMessage() } } @@ -1477,10 +1519,8 @@ class JwtParserTest { parseClaimsJws(compact) fail() } catch (MissingClaimException e) { - assertEquals( - String.format(MISSING_EXPECTED_CLAIM_MESSAGE_TEMPLATE, "aDate", aDate), - e.getMessage() - ) + String msg = "Missing 'aDate' claim. Expected value: $aDate" + assertEquals msg, e.getMessage() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 0fa2786fd..a52745d34 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -351,7 +351,7 @@ class JwtsTest { void testConvenienceAudience() { String compact = Jwts.builder().setAudience("You").compact() Claims claims = Jwts.parser().enableUnsecured().build().parse(compact).payload as Claims - assertEquals 'You', claims.getAudience() + assertEquals 'You', claims.getAudience().iterator().next() compact = Jwts.builder().setIssuer("Me") .setAudience("You") //set it @@ -437,17 +437,17 @@ class JwtsTest { String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) + String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compact() - def jws = Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact) + def jws = Jwts.parser().verifyWith(key).build().parseClaimsJws(compact) Claims claims = jws.payload assertNull jws.header.getCompressionAlgorithm() assertEquals id, claims.getId() - assertEquals "an audience", claims.getAudience() + assertEquals "an issuer", claims.getIssuer() assertEquals "hello this is an amazing jwt", claims.state } @@ -459,17 +459,17 @@ class JwtsTest { String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) + String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(Jwts.ZIP.DEF).compact() - def jws = Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact) + def jws = Jwts.parser().verifyWith(key).build().parseClaimsJws(compact) Claims claims = jws.payload assertEquals "DEF", jws.header.getCompressionAlgorithm() assertEquals id, claims.getId() - assertEquals "an audience", claims.getAudience() + assertEquals "an issuer", claims.getIssuer() assertEquals "hello this is an amazing jwt", claims.state } @@ -481,17 +481,17 @@ class JwtsTest { String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) + String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(Jwts.ZIP.GZIP).compact() - def jws = Jwts.parser().setSigningKey(key).build().parseClaimsJws(compact) + def jws = Jwts.parser().verifyWith(key).build().parseClaimsJws(compact) Claims claims = jws.payload assertEquals "GZIP", jws.header.getCompressionAlgorithm() assertEquals id, claims.getId() - assertEquals "an audience", claims.getAudience() + assertEquals "an issuer", claims.getIssuer() assertEquals "hello this is an amazing jwt", claims.state } @@ -503,7 +503,7 @@ class JwtsTest { String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(key, alg) + String compact = Jwts.builder().id(id).issuer("an issuer").signWith(key, alg) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionAlgorithm() { @Override String getId() { @@ -511,7 +511,7 @@ class JwtsTest { } }).compact() - def jws = Jwts.parser().setSigningKey(key).setCompressionCodecResolver(new CompressionCodecResolver() { + def jws = Jwts.parser().verifyWith(key).setCompressionCodecResolver(new CompressionCodecResolver() { @Override CompressionCodec resolveCompressionCodec(Header header) throws CompressionException { String algorithm = header.getCompressionAlgorithm() @@ -529,7 +529,7 @@ class JwtsTest { assertEquals "CUSTOM", jws.header.getCompressionAlgorithm() assertEquals id, claims.getId() - assertEquals "an audience", claims.getAudience() + assertEquals "an issuer", claims.getIssuer() assertEquals "hello this is an amazing jwt", claims.state } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 069c2dca2..40a1ad501 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -24,6 +24,7 @@ import io.jsonwebtoken.impl.security.Randoms import io.jsonwebtoken.impl.security.TestKey import io.jsonwebtoken.impl.security.TestKeys import io.jsonwebtoken.io.* +import io.jsonwebtoken.jackson.io.JacksonDeserializer import io.jsonwebtoken.security.* import org.junit.Before import org.junit.Test @@ -559,4 +560,171 @@ class DefaultJwtBuilderTest { assertEquals msg, expected.getMessage() } } + + @Test + void testAudienceSingle() { + def key = TestKeys.HS256 + String audienceSingleString = 'test' + def jwt = builder.audienceSingle(audienceSingleString).compact() + // can't use the parser here to validate because it coerces the string value into an array automatically, + // so we need to check the raw payload: + def encoded = new JwtTokenizer().tokenize(jwt).getPayload() + byte[] bytes = Decoders.BASE64URL.decode(encoded) + Map claims = new JacksonDeserializer<>().deserialize(bytes) as Map + + assertEquals audienceSingleString, claims.aud + } + + /** + * Asserts that an additional call to audienceSingle is a full replacement operation and fully replaces the + * previous audienceSingle value + */ + @Test + void testAudienceSingleMultiple() { + def first = 'first' + def second = 'second' + def jwt = builder.audienceSingle(first).audienceSingle(second).compact() + // can't use the parser here to validate because it coerces the string value into an array automatically, + // so we need to check the raw payload: + def encoded = new JwtTokenizer().tokenize(jwt).getPayload() + byte[] bytes = Decoders.BASE64URL.decode(encoded) + Map claims = new JacksonDeserializer<>().deserialize(bytes) as Map + + assertEquals second, claims.aud // second audienceSingle call replaces first value + } + + /** + * Asserts that an additional call to audienceSingle is a full replacement operation and fully replaces the + * previous audienceSingle value + */ + @Test + void testAudienceSingleThenNull() { + def jwt = builder.id('test') + .audienceSingle('single') // set one + .audienceSingle(null) // remove it entirely + .compact() + + // shouldn't be an audience at all: + assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + } + + /** + * Asserts that, even if audienceSingle is called and then the value removed, a final call to audience(Collection) + * still represents a collection without any value errors + */ + @Test + void testAudienceSingleThenNullThenCollection() { + def first = 'first' + def second = 'second' + def expected = [first, second] as Set + def jwt = builder.audienceSingle(first) // sets single value + .audienceSingle(null) // removes entirely + .audience([first, second]) // sets collection + .compact() + + def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + assertEquals expected, aud + } + + /** + * Test to ensure that if we receive a JWT with a single string value, that the parser coerces it to a String array + * so we don't have to worry about different data types: + */ + @Test + void testParseAudienceSingle() { + def key = TestKeys.HS256 + String audienceSingleString = 'test' + def jwt = builder.audienceSingle(audienceSingleString).compact() + + assertEquals audienceSingleString, Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload + .getAudience().iterator().next() // a collection, not a single string + } + + @Test + void testAudience() { + def aud = 'fubar' + def jwt = Jwts.builder().audience(aud).compact() + assertEquals aud, Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience().iterator().next() + } + + @Test + void testAudienceMultipleTimes() { + def one = 'one' + def two = 'two' + def jwt = Jwts.builder().audience(one).audience(two).compact() + def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + assertTrue aud.contains(one) + assertTrue aud.contains(two) + } + + /** + * Asserts that if someone calls builder.audienceSingle and then audience(String), that the audience value + * will automatically be coerced from a String to a Set and contain both elements. + */ + @Test + void testAudienceSingleThenAudience() { + def one = 'one' + def two = 'two' + def jwt = Jwts.builder().audienceSingle(one).audience(two).compact() + def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + assertTrue aud.contains(one) + assertTrue aud.contains(two) + } + + /** + * Asserts that if someone calls builder.audience and then audienceSingle, that the audience value + * will automatically be coerced to a single String contain only the single value since audienceSingle is a + * full-replacement operation. + */ + @Test + void testAudienceThenAudienceSingle() { + def one = 'one' + def two = 'two' + def jwt = Jwts.builder().audience(one).audienceSingle(two).compact() + + // can't use the parser here to validate because it coerces the string value into an array automatically, + // so we need to check the raw payload: + def encoded = new JwtTokenizer().tokenize(jwt).getPayload() + byte[] bytes = Decoders.BASE64URL.decode(encoded) + Map claims = new JacksonDeserializer<>().deserialize(bytes) as Map + + assertEquals two, claims.aud + } + + /** + * Asserts that if someone calls builder.audienceSingle and then audience(Collection), the builder coerces the + * aud to a Set and only the elements in the Collection will be applied since audience(Collection) is a + * full-replacement operation. + */ + @Test + void testAudienceSingleThenAudienceCollection() { + def single = 'one' + def collection = ['two', 'three'] as Set + def jwt = Jwts.builder().audienceSingle(single).audience(collection).compact() + def aud = Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience() + assertEquals collection.size(), aud.size() + assertTrue aud.containsAll(collection) + } + + /** + * Asserts that if someone calls builder.audience(Collection) and then audienceSingle, that the audience value + * will automatically be coerced to a single String contain only the single value since audienceSingle is a + * full-replacement operation. + */ + @Test + void testAudienceCollectionThenAudienceSingle() { + def one = 'one' + def two = 'two' + def three = 'three' + def jwt = Jwts.builder().audience([one, two]).audienceSingle(three).compact() + + // can't use the parser here to validate because it coerces the string value into an array automatically, + // so we need to check the raw payload: + def encoded = new JwtTokenizer().tokenize(jwt).getPayload() + byte[] bytes = Decoders.BASE64URL.decode(encoded) + Map claims = new JacksonDeserializer<>().deserialize(bytes) as Map + + assertEquals three, claims.aud + } + } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy index 89927a0e2..9267782d1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtTest.groovy @@ -31,7 +31,7 @@ class DefaultJwtTest { void testToString() { String compact = Jwts.builder().header().add('foo', 'bar').and().audience('jsmith').compact() Jwt jwt = Jwts.parser().enableUnsecured().build().parseClaimsJwt(compact) - assertEquals 'header={foo=bar, alg=none},payload={aud=jsmith}', jwt.toString() + assertEquals 'header={foo=bar, alg=none},payload={aud=[jsmith]}', jwt.toString() } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy index aa1efaa41..4147369d4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/AbstractJwkBuilderTest.groovy @@ -235,14 +235,13 @@ class AbstractJwkBuilderTest { assertEquals canonical, jwk.key_ops } - @Test void testCustomOperationOverridesDefault() { def op = Jwks.OP.builder().id('sign').description('Different Description') .related(Jwks.OP.VERIFY.id).build() def builder = builder().operationPolicy(Jwks.OP.policy().add(op).build()) - def jwk = builder.operations(Collections.setOf(op, Jwks.OP.VERIFY)).build() - println jwk + def jwk = builder.operations(Collections.setOf(op, Jwks.OP.VERIFY)).build() as Jwk + assertSame op, jwk.getOperations().find({it.id == 'sign'}) } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy index 9fe17ca16..bd88dbc17 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/EdSignatureAlgorithmTest.groovy @@ -128,6 +128,6 @@ class EdSignatureAlgorithmTest { def token = Jwts.parser().verifyWith(verification).build().parseClaimsJws(jwt) assertEquals([alg: alg.getId()], token.header) assertEquals 'me', token.getPayload().getIssuer() - assertEquals 'you', token.getPayload().getAudience() + assertEquals 'you', token.getPayload().getAudience().iterator().next() } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/issues/Issue438Test.groovy b/impl/src/test/groovy/io/jsonwebtoken/issues/Issue438Test.groovy index f987a5c1d..7ba646349 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/issues/Issue438Test.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/issues/Issue438Test.groovy @@ -27,7 +27,7 @@ class Issue438Test { @Test(expected = UnsupportedJwtException /* not IllegalArgumentException */) void testIssue438() { - String jws = Jwts.builder().audience('test').signWith(TestKeys.RS256.pair.private).compact() + String jws = Jwts.builder().issuer('test').signWith(TestKeys.RS256.pair.private).compact() Jwts.parser().verifyWith(TestKeys.HS256).build().parseClaimsJws(jws) } } diff --git a/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java b/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java index 60e15c955..c014726fb 100644 --- a/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java +++ b/tdjar/src/test/java/io/jsonwebtoken/all/JavaReadmeTest.java @@ -45,6 +45,7 @@ import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.util.Set; import static io.jsonwebtoken.security.Jwks.builder; @@ -183,11 +184,11 @@ public void testExampleJweRSA() { .compact(); // Alice receives and decrypts the compact JWE: - String audience = Jwts.parser() + Set audience = Jwts.parser() .decryptWith(pair.getPrivate()) // <-- Alice's RSA private key .build().parseClaimsJwe(jwe).getPayload().getAudience(); - assert "Alice".equals(audience); + assert audience.contains("Alice"); } /** @@ -231,11 +232,11 @@ public void testExampleJweECDHES() { .compact(); // Alice receives and decrypts the compact JWE: - String audience = Jwts.parser() + Set audience = Jwts.parser() .decryptWith(pair.getPrivate()) // <-- Alice's EC private key .build().parseClaimsJwe(jwe).getPayload().getAudience(); - assert "Alice".equals(audience); + assert audience.contains("Alice"); } /**