Skip to content

Commit a0a123e

Browse files
authored
PR #917
* Ensured `NestedCollection`s do not need their `.and()` method called to apply collection changes. Instead, changes are applied immediately as they occur (via `.add`, `.remove`, etc), and `.and()` is now purely for returning to the parent builder if necessary/desired. * Updated associated JavaDoc with code examples to make the `.and()` method's purpose a little clearer. * Updated CHANGELOG.md Closes #916
1 parent afcd889 commit a0a123e

17 files changed

+292
-35
lines changed

CHANGELOG.md

+26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,31 @@
11
## Release Notes
22

3+
### 0.12.5
4+
5+
This patch release:
6+
7+
* Ensures that builders' `NestedCollection` changes are applied to the collection immediately as mutation methods are called, no longer
8+
requiring application developers to call `.and()` to 'commit' or apply a change. For example, prior to this release,
9+
the following code did not apply changes:
10+
```java
11+
JwtBuilder builder = Jwts.builder();
12+
builder.audience().add("an-audience"); // no .and() call
13+
builder.compact(); // would not keep 'an-audience'
14+
```
15+
Now this code works as expected and all other `NestedCollection` instances like it apply changes immediately (e.g. when calling
16+
`.add(value)`).
17+
18+
However, standard fluent builder chains are still recommended for readability when feasible, e.g.
19+
20+
```java
21+
Jwts.builder()
22+
.audience().add("an-audience").and() // allows fluent chaining
23+
.subject("Joe")
24+
// etc...
25+
.compact()
26+
```
27+
See [Issue 916](https://github.com/jwtk/jjwt/issues/916).
28+
329
### 0.12.4
430

531
This patch release includes various changes listed below.

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,7 @@ String jws = Jwts.builder()
993993

994994
.issuer("me")
995995
.subject("Bob")
996-
.audience("you")
996+
.audience().add("you").and()
997997
.expiration(expiration) //a java.util.Date
998998
.notBefore(notBefore) //a java.util.Date
999999
.issuedAt(new Date()) // for example, now

api/src/main/java/io/jsonwebtoken/ClaimsMutator.java

+21
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
9696
* <a href="https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.3"><code>aud</code></a> (audience) Claim
9797
* set, quietly ignoring any null, empty, whitespace-only, or existing value already in the set.
9898
*
99+
* <p>When finished, the {@code audience} collection's {@link AudienceCollection#and() and()} method may be used
100+
* to continue configuration. For example:</p>
101+
* <blockquote><pre>
102+
* Jwts.builder() // or Jwts.claims()
103+
*
104+
* .audience().add("anAudience")<b>.and() // return parent</b>
105+
*
106+
* .subject("Joe") // resume configuration...
107+
* // etc...
108+
* </pre></blockquote>
109+
*
99110
* @return the {@link AudienceCollection AudienceCollection} to use for {@code aud} configuration.
100111
* @see AudienceCollection AudienceCollection
101112
* @see AudienceCollection#single(String) AudienceCollection.single(String)
@@ -221,6 +232,16 @@ public interface ClaimsMutator<T extends ClaimsMutator<T>> {
221232
* A {@code NestedCollection} for setting {@link #audience()} values that also allows overriding the collection
222233
* to be a {@link #single(String) single string value} for legacy JWT recipients if necessary.
223234
*
235+
* <p>Because this interface extends {@link NestedCollection}, the {@link #and()} method may be used to continue
236+
* parent configuration. For example:</p>
237+
* <blockquote><pre>
238+
* Jwts.builder() // or Jwts.claims()
239+
*
240+
* .audience().add("anAudience")<b>.and() // return parent</b>
241+
*
242+
* .subject("Joe") // resume parent configuration...
243+
* // etc...</pre></blockquote>
244+
*
224245
* @param <P> the type of ClaimsMutator to return for method chaining.
225246
* @see #single(String)
226247
* @since 0.12.0

api/src/main/java/io/jsonwebtoken/JwtParserBuilder.java

+17-4
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,25 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
101101
* the parser encounters a Protected JWT that {@link ProtectedHeader#getCritical() requires} extensions, and
102102
* those extensions' header names are not specified via this method, the parser will reject that JWT.
103103
*
104+
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
105+
* configuration, for example:</p>
106+
* <blockquote><pre>
107+
* parserBuilder.critical().add("headerName")<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
108+
*
104109
* <p><b>Extension Behavior</b></p>
105110
*
106111
* <p>The {@code critical} collection only identifies header parameter names that are used in extensions supported
107112
* by the application. <b>Application developers, <em>not JJWT</em>, MUST perform the associated extension behavior
108113
* using the parsed JWT</b>.</p>
109114
*
115+
* <p><b>Continued Parser Configuration</b></p>
116+
* <p>When finished, use the collection's
117+
* {@link Conjunctor#and() and()} method to continue parser configuration, for example:
118+
* <blockquote><pre>
119+
* Jwts.parser()
120+
* .critical().add("headerName").<b>{@link Conjunctor#and() and()} // return parent</b>
121+
* // resume parser configuration...</pre></blockquote>
122+
*
110123
* @return the {@link NestedCollection} to use for {@code crit} configuration.
111124
* @see ProtectedHeader#getCritical()
112125
* @since 0.12.0
@@ -557,7 +570,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
557570
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
558571
* configuration, for example:</p>
559572
* <blockquote><pre>
560-
* parserBuilder.enc().add(anAeadAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
573+
* parserBuilder.enc().add(anAeadAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
561574
*
562575
* <p><b>Standard Algorithms and Overrides</b></p>
563576
*
@@ -597,7 +610,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
597610
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
598611
* configuration, for example:</p>
599612
* <blockquote><pre>
600-
* parserBuilder.key().add(aKeyAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
613+
* parserBuilder.key().add(aKeyAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
601614
*
602615
* <p><b>Standard Algorithms and Overrides</b></p>
603616
*
@@ -639,7 +652,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
639652
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
640653
* configuration, for example:</p>
641654
* <blockquote><pre>
642-
* parserBuilder.sig().add(aSignatureAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
655+
* parserBuilder.sig().add(aSignatureAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
643656
*
644657
* <p><b>Standard Algorithms and Overrides</b></p>
645658
*
@@ -680,7 +693,7 @@ public interface JwtParserBuilder extends Builder<JwtParser> {
680693
* <p>The collection's {@link Conjunctor#and() and()} method returns to the builder for continued parser
681694
* configuration, for example:</p>
682695
* <blockquote><pre>
683-
* parserBuilder.zip().add(aCompressionAlgorithm).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
696+
* parserBuilder.zip().add(aCompressionAlgorithm)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
684697
*
685698
* <p><b>Standard Algorithms and Overrides</b></p>
686699
*

api/src/main/java/io/jsonwebtoken/ProtectedHeaderMutator.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ public interface ProtectedHeaderMutator<T extends ProtectedHeaderMutator<T>> ext
3333
/**
3434
* Configures names of header parameters used by JWT or JWA specification extensions that <em>MUST</em> be
3535
* understood and supported by the JWT recipient. When finished, use the collection's
36-
* {@link Conjunctor#and() and()} method to return to the header builder, for example:
36+
* {@link Conjunctor#and() and()} method to continue header configuration, for example:
3737
* <blockquote><pre>
38-
* builder.critical().add("headerName").{@link Conjunctor#and() and()} // etc...</pre></blockquote>
38+
* headerBuilder
39+
* .critical().add("headerName").<b>{@link Conjunctor#and() and()} // return parent</b>
40+
* // resume header configuration...</pre></blockquote>
3941
*
4042
* @return the {@link NestedCollection} to use for {@code crit} configuration.
4143
* @see <a href="https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11">JWS <code>crit</code> (Critical) Header Parameter</a>

api/src/main/java/io/jsonwebtoken/lang/NestedCollection.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717

1818
/**
1919
* A {@link CollectionMutator} that can return access to its parent via the {@link Conjunctor#and() and()} method for
20-
* continued configuration.
20+
* continued configuration. For example:
21+
* <blockquote><pre>
22+
* builder
23+
* .aNestedCollection()// etc...
24+
* <b>.and() // return parent</b>
25+
* // resume parent configuration...</pre></blockquote>
2126
*
2227
* @param <E> the type of elements in the collection
2328
* @param <P> the parent to return

api/src/main/java/io/jsonwebtoken/security/JwkBuilder.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ public interface JwkBuilder<K extends Key, J extends Jwk<K>, T extends JwkBuilde
109109
* the key is intended to be used. When finished, use the collection's {@link Conjunctor#and() and()} method to
110110
* return to the JWK builder, for example:
111111
* <blockquote><pre>
112-
* jwkBuilder.operations().add(aKeyOperation).{@link Conjunctor#and() and()} // etc...</pre></blockquote>
112+
* jwkBuilder.operations().add(aKeyOperation)<b>.{@link Conjunctor#and() and()} // etc...</b></pre></blockquote>
113113
*
114-
* <p>The {@code and()} method will throw an {@link IllegalArgumentException} if any of the specified
114+
* <p>The {@code add()} method(s) will throw an {@link IllegalArgumentException} if any of the specified
115115
* {@code KeyOperation}s are not permitted by the JWK's
116116
* {@link #operationPolicy(KeyOperationPolicy) operationPolicy}. See that documentation for more
117117
* information on security vulnerabilities when using the same key with multiple algorithms.</p>

impl/src/main/java/io/jsonwebtoken/impl/DefaultJweHeaderMutator.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,8 @@ public T setCompressionAlgorithm(String zip) {
107107
public NestedCollection<String, T> critical() {
108108
return new DefaultNestedCollection<String, T>(self(), this.DELEGATE.get(DefaultProtectedHeader.CRIT)) {
109109
@Override
110-
public T and() {
110+
protected void changed() {
111111
put(DefaultProtectedHeader.CRIT, Collections.asSet(getCollection()));
112-
return super.and();
113112
}
114113
};
115114
}

impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParserBuilder.java

+5-10
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,8 @@ public JwtParserBuilder clock(Clock clock) {
220220
public NestedCollection<String, JwtParserBuilder> critical() {
221221
return new DefaultNestedCollection<String, JwtParserBuilder>(this, this.critical) {
222222
@Override
223-
public JwtParserBuilder and() {
223+
protected void changed() {
224224
critical = Collections.asSet(getCollection());
225-
return super.and();
226225
}
227226
};
228227
}
@@ -304,9 +303,8 @@ private JwtParserBuilder decryptWith(final Key key) {
304303
public NestedCollection<CompressionAlgorithm, JwtParserBuilder> zip() {
305304
return new DefaultNestedCollection<CompressionAlgorithm, JwtParserBuilder>(this, this.zipAlgs.values()) {
306305
@Override
307-
public JwtParserBuilder and() {
306+
protected void changed() {
308307
zipAlgs = new IdRegistry<>(StandardCompressionAlgorithms.NAME, getCollection());
309-
return super.and();
310308
}
311309
};
312310
}
@@ -315,9 +313,8 @@ public JwtParserBuilder and() {
315313
public NestedCollection<AeadAlgorithm, JwtParserBuilder> enc() {
316314
return new DefaultNestedCollection<AeadAlgorithm, JwtParserBuilder>(this, this.encAlgs.values()) {
317315
@Override
318-
public JwtParserBuilder and() {
316+
public void changed() {
319317
encAlgs = new IdRegistry<>(StandardEncryptionAlgorithms.NAME, getCollection());
320-
return super.and();
321318
}
322319
};
323320
}
@@ -326,9 +323,8 @@ public JwtParserBuilder and() {
326323
public NestedCollection<SecureDigestAlgorithm<?, ?>, JwtParserBuilder> sig() {
327324
return new DefaultNestedCollection<SecureDigestAlgorithm<?, ?>, JwtParserBuilder>(this, this.sigAlgs.values()) {
328325
@Override
329-
public JwtParserBuilder and() {
326+
public void changed() {
330327
sigAlgs = new IdRegistry<>(StandardSecureDigestAlgorithms.NAME, getCollection());
331-
return super.and();
332328
}
333329
};
334330
}
@@ -337,9 +333,8 @@ public JwtParserBuilder and() {
337333
public NestedCollection<KeyAlgorithm<?, ?>, JwtParserBuilder> key() {
338334
return new DefaultNestedCollection<KeyAlgorithm<?, ?>, JwtParserBuilder>(this, this.keyAlgs.values()) {
339335
@Override
340-
public JwtParserBuilder and() {
336+
public void changed() {
341337
keyAlgs = new IdRegistry<>(StandardKeyAlgorithms.NAME, getCollection());
342-
return super.and();
343338
}
344339
};
345340
}

impl/src/main/java/io/jsonwebtoken/impl/DelegatingClaimsMutator.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ public AudienceCollection<T> audience() {
130130
@Override
131131
public T single(String audience) {
132132
return audienceSingle(audience);
133+
// DO NOT call changed() here - we don't want to replace the value with a collection
133134
}
134135

135136
@Override
136-
public T and() {
137+
protected void changed() {
137138
put(DefaultClaims.AUDIENCE, Collections.asSet(getCollection()));
138-
return super.and();
139139
}
140140
};
141141
}

impl/src/main/java/io/jsonwebtoken/impl/lang/DefaultCollectionMutator.java

+21-7
Original file line numberDiff line numberDiff line change
@@ -37,38 +37,52 @@ protected final M self() {
3737
return (M) this;
3838
}
3939

40-
@Override
41-
public M add(E e) {
42-
if (Objects.isEmpty(e)) return self();
40+
private boolean doAdd(E e) {
41+
if (Objects.isEmpty(e)) return false;
4342
if (e instanceof Identifiable && !Strings.hasText(((Identifiable) e).getId())) {
4443
String msg = e.getClass() + " getId() value cannot be null or empty.";
4544
throw new IllegalArgumentException(msg);
4645
}
47-
this.collection.remove(e);
48-
this.collection.add(e);
46+
return this.collection.add(e);
47+
}
48+
49+
@Override
50+
public M add(E e) {
51+
if (doAdd(e)) changed();
4952
return self();
5053
}
5154

5255
@Override
5356
public M remove(E e) {
54-
this.collection.remove(e);
57+
if (this.collection.remove(e)) changed();
5558
return self();
5659
}
5760

5861
@Override
5962
public M add(Collection<? extends E> c) {
63+
boolean changed = false;
6064
for (E element : Collections.nullSafe(c)) {
61-
add(element);
65+
changed = doAdd(element) || changed;
6266
}
67+
if (changed) changed();
6368
return self();
6469
}
6570

6671
@Override
6772
public M clear() {
73+
boolean changed = !Collections.isEmpty(this.collection);
6874
this.collection.clear();
75+
if (changed) changed();
6976
return self();
7077
}
7178

79+
/**
80+
* Callback for subclasses that wish to be notified if the internal collection has changed via builder mutation
81+
* methods.
82+
*/
83+
protected void changed() {
84+
}
85+
7286
protected Collection<E> getCollection() {
7387
return Collections.immutable(this.collection);
7488
}

impl/src/main/java/io/jsonwebtoken/impl/security/AbstractJwkBuilder.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,10 @@ public T idFromThumbprint(HashAlgorithm alg) {
111111
public NestedCollection<KeyOperation, T> operations() {
112112
return new DefaultNestedCollection<KeyOperation, T>(self(), this.DELEGATE.getOperations()) {
113113
@Override
114-
public T and() {
114+
protected void changed() {
115115
Collection<? extends KeyOperation> c = getCollection();
116116
opsPolicy.validate(c);
117117
DELEGATE.setOperations(c);
118-
return super.and();
119118
}
120119
};
121120
}

impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy

+43
Original file line numberDiff line numberDiff line change
@@ -791,4 +791,47 @@ class DefaultJwtBuilderTest {
791791
assertEquals three, claims.aud
792792
}
793793

794+
/**
795+
* Asserts that if a .audience() builder is used, and its .and() method is not called, the change to the
796+
* audience is still applied when building the JWT.
797+
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
798+
* @since JJWT_RELEASE_VERSION
799+
*/
800+
@Test
801+
void testAudienceWithoutConjunction() {
802+
def aud = 'my-web'
803+
def builder = Jwts.builder()
804+
builder.audience().add(aud) // no .and() call
805+
def jwt = builder.compact()
806+
807+
// assert that the resulting claims has the audience array set as expected:
808+
def parsed = Jwts.parser().unsecured().build().parseUnsecuredClaims(jwt)
809+
assertEquals aud, parsed.payload.getAudience()[0]
810+
}
811+
812+
/**
813+
* Asserts that if a .header().critical() builder is used, and its .and() method is not called, the change to the
814+
* crit collection is still applied when building the header.
815+
* @see <a href="https://github.com/jwtk/jjwt/issues/916">JJWT Issue 916</a>
816+
* @since JJWT_RELEASE_VERSION
817+
*/
818+
@Test
819+
void testCritWithoutConjunction() {
820+
def crit = 'test'
821+
def builder = Jwts.builder().issuer('me')
822+
def headerBuilder = builder.header()
823+
headerBuilder.critical().add(crit) // no .and() method
824+
headerBuilder.add(crit, 'foo') // no .and() method
825+
builder.signWith(TestKeys.HS256)
826+
def jwt = builder.compact()
827+
828+
def headerBytes = Decoders.BASE64URL.decode(jwt.split('\\.')[0])
829+
def headerMap = Services.get(Deserializer).deserialize(Streams.reader(headerBytes)) as Map<String, ?>
830+
831+
def expected = [crit] as Set<String>
832+
def val = headerMap.get('crit') as Set<String>
833+
assertNotNull val
834+
assertEquals expected, val
835+
}
836+
794837
}

0 commit comments

Comments
 (0)