Skip to content

Commit 5acdc49

Browse files
committed
* Ensured NestedCollections 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 5acdc49

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)