Skip to content

Commit

Permalink
Ensured various builder collection argument methods were semantically…
Browse files Browse the repository at this point in the history
… the same (append operations, not one append, one replace) (jwtk#830)
  • Loading branch information
lhazlewood committed Sep 15, 2023
1 parent 8cb59d7 commit bf5d81c
Show file tree
Hide file tree
Showing 13 changed files with 232 additions and 56 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ enforcement.
* Convenient and readable [fluent](http://en.wikipedia.org/wiki/Fluent_interface) interfaces, great for IDE
auto-completion to write code quickly
* Fully RFC specification compliant on all implemented functionality, tested against RFC-specified test vectors
* Stable implementation with over 1,300+ tests and enforced 100% test code coverage. Every single method, statement
* Stable implementation with over 1,400+ tests and enforced 100% test code coverage. Every single method, statement
and conditional branch variant in the entire codebase is tested and required to pass on every build.
* Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms:

Expand Down
20 changes: 12 additions & 8 deletions api/src/main/java/io/jsonwebtoken/ClaimsMutator.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
* 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.
* {@code null}, empty or whitespace 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,
Expand All @@ -108,22 +108,26 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
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.
* Adds (appends) the specified {@code aud} value to the {@link #audience(Collection) audience} Claim set
* (JSON Array) unless it is {@code null}, empty, whitespace-only or already exists in the set.
*
* <p>This method may be called multiple times.</p>
*
* @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;
T audience(String aud);

/**
* Sets the JWT <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code></a> (audience)
* Claim set, replacing any previous value(s).
* Adds (appends) the specified values to the JWT
* <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code></a> (audience) Claim
* set, quietly ignoring any null, empty, whitespace-only, or existing value already in the set.
*
* <p>This method may be called multiple times.</p>
*
* @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.
* @param aud the values to add to the {@code aud} Claim set (JSON Array)
* @return the instance for method chaining
* @since JJWT_RELEASE_VERSION
*/
Expand Down
4 changes: 2 additions & 2 deletions api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
*
* <p><b>Extension Behavior</b></p>
*
* <p>The {@code crit} set only identifies header parameter names that are used in extensions supported by the
* application. <b>Application developers, <em>not JJWT</em>, MUST perform the associated extension behavior
* <p>The {@code crit} set argument only identifies header parameter names that are used in extensions supported
* by the application. <b>Application developers, <em>not JJWT</em>, MUST perform the associated extension behavior
* using the parsed JWT</b>.</p>
*
* @param crit the header parameter names used in JWT extensions supported by the application.
Expand Down
26 changes: 19 additions & 7 deletions api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import io.jsonwebtoken.security.X509Mutator;

import java.net.URI;
import java.util.Set;
import java.util.Collection;

/**
* Mutation (modifications) to a {@link ProtectedHeader Header} instance.
Expand All @@ -30,17 +30,29 @@
public interface ProtectedHeaderMutator<T extends ProtectedHeaderMutator<T>> extends HeaderMutator<T>, X509Mutator<T> {

/**
* Sets the header parameter names that use extensions to the JWT or JWA specification that <em>MUST</em>
* be understood and supported by the JWT recipient. A {@code null} value will remove the
* property from the JSON map.
* Adds the name of a header parameter used by a JWT or JWA specification extension that <em>MUST</em> be understood
* and supported by the JWT recipient. A {@code null}, empty, whitespace-only or already existing value is ignored.
*
* @param crit the header parameter names that use extensions to the JWT or JWA specification that <em>MUST</em>
* be understood and supported by the JWT recipient.
* @param crit the name of a header parameter used by a JWT or JWA specification extension that <em>MUST</em> be
* understood and supported by the JWT recipient.
* @return the header for method chaining.
* @see <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11">JWS <code>crit</code> (Critical) Header Parameter</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.13">JWS <code>crit</code> (Critical) Header Parameter</a>
*/
T critical(Set<String> crit);
T critical(String crit);

/**
* Adds names of header parameters used by JWT or JWA specification extensions that <em>MUST</em> be
* understood and supported by the JWT recipient. {@code null}, empty, whitespace-only or already existing
* values are ignored.
*
* @param crit names of header parameters used by JWT or JWA specification extensions that <em>MUST</em> be
* understood and supported by the JWT recipient
* @return the header for method chaining.
* @see <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11">JWS <code>crit</code> (Critical) Header Parameter</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc7516.html#section-4.1.13">JWS <code>crit</code> (Critical) Header Parameter</a>
*/
T critical(Collection<String> crit);

/**
* Sets the {@code jwk} (JSON Web Key) associated with the JWT. When set for a {@link JwsHeader}, the
Expand Down
13 changes: 12 additions & 1 deletion api/src/main/java/io/jsonwebtoken/lang/Collections.java
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ public static <T, C extends Collection<T>> C immutable(C c) {

/**
* Returns a non-null set, either {@code s} if it is not null, or {@link #emptySet()} otherwise.
* Returns the specified set if not null or {@link #emptySet()} otherwise.
*
* @param s the set to check for null
* @param <T> type of elements in the set
Expand All @@ -191,6 +190,18 @@ public static <T> Set<T> nullSafe(Set<T> s) {
return s == null ? Collections.<T>emptySet() : s;
}

/**
* Returns a non-null collection, either {@code c} if it is not null, or {@link #emptyList()} otherwise.
*
* @param c the collection to check for null
* @param <T> type of elements in the collection
* @return a non-null collection, either {@code c} if it is not null, or {@link #emptyList()} otherwise.
* @since JJWT_RELEASE_VERSION
*/
public static <T> Collection<T> nullSafe(Collection<T> c) {
return c == null ? Collections.<T>emptyList() : c;
}

/**
* Return <code>true</code> if the supplied Collection is <code>null</code>
* or empty. Otherwise, return <code>false</code>.
Expand Down
12 changes: 6 additions & 6 deletions api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,14 @@ public interface JwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilde
*
* @param operation the value to add to the JWK {@code key_ops} value set
* @return the builder for method chaining.
* @throws IllegalArgumentException if {@code op} is {@code null} or if the operation is not permitted
* by the operations {@link #operationPolicy(KeyOperationPolicy) policy}.
* @throws IllegalArgumentException if the {@code op} is not permitted by the operations
* {@link #operationPolicy(KeyOperationPolicy) policy}.
* @see Jwks.OP
*/
T operation(KeyOperation operation) throws IllegalArgumentException;

/**
* Sets the JWK <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">{@code key_ops}
* Adds {@code ops} to the JWK <a href="https://www.rfc-editor.org/rfc/rfc7517.html#section-4.3">{@code key_ops}
* (Key Operations) Parameter</a> values.
*
* <p>The {@code key_ops} (key operations) parameter identifies the operation(s) for which the key is
Expand Down Expand Up @@ -176,10 +176,10 @@ public interface JwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilde
* <p>For best interoperability with other applications however, it is recommended to use only the {@link Jwks.OP}
* constants.</p>
*
* @param ops the JWK {@code key_ops} value set, or {@code null} if not present.
* @param ops the {@code KeyOperation} values to add to the JWK {@code key_ops} value set
* @return the builder for method chaining.
* @throws IllegalArgumentException {@code ops} is {@code null} or empty, or if any of the operations are not
* permitted by the operations {@link #operationPolicy(KeyOperationPolicy) policy}.
* @throws IllegalArgumentException if any of the operations are not permitted by the operations
* {@link #operationPolicy(KeyOperationPolicy) policy}.
* @see Jwks.OP
*/
T operations(Collection<KeyOperation> ops) throws IllegalArgumentException;
Expand Down
23 changes: 22 additions & 1 deletion api/src/test/groovy/io/jsonwebtoken/lang/CollectionsTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class CollectionsTest {

@Test
void testNullSafeSetWithNullArgument() {
def set = Collections.nullSafe(null)
def set = Collections.nullSafe((Set)null)
assertNotNull set
assertTrue set.isEmpty()
}
Expand All @@ -70,4 +70,25 @@ class CollectionsTest {
def b = Collections.nullSafe(a)
assertSame a, b
}

@Test
void testNullSafeCollectionWithNullArgument() {
Collection c = Collections.nullSafe((Collection)null)
assertNotNull c
assertTrue c.isEmpty()
}

@Test
void testNullSafeCollectionWithEmptyArgument() {
Collection a = new LinkedHashSet() as Collection
def b = Collections.nullSafe(a)
assertSame a, b
}

@Test
void testNullSafeCollectionWithNonEmptyArgument() {
Collection a = ["hello"] as Collection<String>
def b = Collections.nullSafe(a)
assertSame a, b
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
import io.jsonwebtoken.impl.lang.DelegatingMapMutator;
import io.jsonwebtoken.impl.lang.Parameter;
import io.jsonwebtoken.impl.security.X509BuilderSupport;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.PublicJwk;

import java.net.URI;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

Expand Down Expand Up @@ -102,8 +105,29 @@ public T setCompressionAlgorithm(String zip) {
// =============================================================

@Override
public T critical(Set<String> crit) {
return put(DefaultProtectedHeader.CRIT, crit);
public T critical(String crit) {
crit = Strings.clean(crit);
if (Strings.hasText(crit)) {
critical(Collections.setOf(crit));
}
return self();
}

@Override
public T critical(Collection<String> crit) {
if (!Collections.isEmpty(crit)) {
Set<String> existing = Collections.nullSafe(this.DELEGATE.get(DefaultProtectedHeader.CRIT));
Set<String> set = new LinkedHashSet<>(existing.size() + crit.size());
set.addAll(existing);
for (String s : crit) {
s = Strings.clean(s);
if (s != null) {
set.add(s);
}
}
put(DefaultProtectedHeader.CRIT, set);
}
return self();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import io.jsonwebtoken.impl.lang.DelegatingMapMutator;
import io.jsonwebtoken.impl.lang.Parameter;
import io.jsonwebtoken.impl.lang.Parameters;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.MapMutator;
import io.jsonwebtoken.lang.Strings;
Expand Down Expand Up @@ -88,8 +87,7 @@ private Set<String> getAudience() {
put(DefaultClaims.AUDIENCE, Collections.setOf(existing)); // replace as Set
}
}
Set<String> aud = get(DefaultClaims.AUDIENCE);
return aud != null ? aud : Collections.<String>emptySet();
return get(DefaultClaims.AUDIENCE);
}

@Override
Expand All @@ -108,16 +106,28 @@ public T audienceSingle(String aud) {

@Override
public T audience(String aud) {
aud = Assert.hasText(Strings.clean(aud), "Audience string value cannot be null or empty.");
Set<String> set = new LinkedHashSet<>(getAudience());
set.add(aud);
return audience(set);
aud = Strings.clean(aud);
if (Strings.hasText(aud)) {
audience(java.util.Collections.singleton(aud));
}
return self();
}

@Override
public T audience(Collection<String> aud) {
getAudience(); //coerce to Set<String> if necessary
return put(DefaultClaims.AUDIENCE, Collections.asSet(aud));
if (!Collections.isEmpty(aud)) {
Set<String> existing = Collections.nullSafe(getAudience());
Set<String> set = new LinkedHashSet<>(existing.size() + aud.size());
set.addAll(existing);
for (String s : aud) {
s = Strings.clean(s);
if (s != null) {
set.add(s);
}
}
put(DefaultClaims.AUDIENCE, set);
}
return self();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,14 @@ public T idFromThumbprint(HashAlgorithm alg) {

@Override
public T operation(KeyOperation operation) throws IllegalArgumentException {
Assert.notNull(operation, "KeyOperation cannot be null.");
return operations(Collections.setOf(operation));
return operation != null ? operations(Collections.setOf(operation)) : self();
}

@Override
public T operations(Collection<KeyOperation> ops) {
Assert.notEmpty(ops, "KeyOperations collection argument cannot be null or empty.");
Set<KeyOperation> set = new LinkedHashSet<>(ops); // new ones override existing ones
Set<KeyOperation> existing = this.DELEGATE.getOperations();
if (!Collections.isEmpty(existing)) {
set.addAll(existing);
}
Set<KeyOperation> set = new LinkedHashSet<>(Collections.nullSafe(ops)); // new ones override existing ones
Set<KeyOperation> existing = Collections.nullSafe(this.DELEGATE.getOperations());
set.addAll(existing);
this.opsPolicy.validate(set);
this.DELEGATE.setOperations(set);
return self();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,18 @@ class DefaultJwtBuilderTest {
assertEquals aud, Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience().iterator().next()
}

@Test
void testAudienceNullString() {
def jwt = Jwts.builder().subject('me').audience(null).compact()
assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
}

@Test
void testAudienceEmptyString() {
def jwt = Jwts.builder().subject('me').audience(' ').compact()
assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
}

@Test
void testAudienceMultipleTimes() {
def one = 'one'
Expand All @@ -657,6 +669,28 @@ class DefaultJwtBuilderTest {
assertTrue aud.contains(two)
}

@Test
void testAudienceNullCollection() {
Collection c = null
def jwt = Jwts.builder().subject('me').audience(c).compact()
assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
}

@Test
void testAudienceEmptyCollection() {
Collection c = new ArrayList()
def jwt = Jwts.builder().subject('me').audience(c).compact()
assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
}

@Test
void testAudienceCollectionWithNullElement() {
Collection c = new ArrayList()
c.add(null)
def jwt = Jwts.builder().subject('me').audience(c).compact()
assertNull Jwts.parser().enableUnsecured().build().parseClaimsJwt(jwt).payload.getAudience()
}

/**
* 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<String> and contain both elements.
Expand Down Expand Up @@ -693,17 +727,17 @@ class DefaultJwtBuilderTest {

/**
* Asserts that if someone calls builder.audienceSingle and then audience(Collection), the builder coerces the
* aud to a Set<String> and only the elements in the Collection will be applied since audience(Collection) is a
* full-replacement operation.
* aud to a Set<String> and all elements will be applied since audience(Collection).
*/
@Test
void testAudienceSingleThenAudienceCollection() {
def single = 'one'
def collection = ['two', 'three'] as Set<String>
def expected = ['one', 'two', 'three'] as Set<String>
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)
assertEquals expected.size(), aud.size()
assertTrue aud.contains(single) && aud.containsAll(collection)
}

/**
Expand Down
Loading

0 comments on commit bf5d81c

Please sign in to comment.