Skip to content

Commit f1e1c48

Browse files
committed
Take a defensive copy of expected checks so verifiers are independent
The JWTVerifier constructor wrapped the builder's live ArrayList with Collections.unmodifiableList (a view, not a copy), and build(Clock) appended the mandatory exp/nbf/iat checks to that same list. Calling build() twice on one BaseVerification therefore appended a second set of mandatory checks AND retroactively mutated the first, already-returned (and documented as immutable and thread-safe) verifier. Build the mandatory checks into a fresh list inside build() instead of mutating the builder's field, and take a defensive copy in the constructor. build() is now idempotent and each verifier owns an independent snapshot. Single-build behaviour and all existing check counts are unchanged.
1 parent b5e3eb9 commit f1e1c48

2 files changed

Lines changed: 26 additions & 7 deletions

File tree

lib/src/main/java/com/auth0/jwt/JWTVerifier.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ public final class JWTVerifier implements com.auth0.jwt.interfaces.JWTVerifier {
3030

3131
JWTVerifier(Algorithm algorithm, List<ExpectedCheckHolder> expectedChecks) {
3232
this.algorithm = algorithm;
33-
this.expectedChecks = Collections.unmodifiableList(expectedChecks);
33+
// Defensive copy: never wrap the builder's live list, otherwise a reused builder would
34+
// retroactively mutate an already-built (supposedly immutable, thread-safe) verifier.
35+
this.expectedChecks = Collections.unmodifiableList(new ArrayList<>(expectedChecks));
3436
this.parser = new JWTParser();
3537
}
3638

@@ -285,8 +287,11 @@ public JWTVerifier build() {
285287
*/
286288
public JWTVerifier build(Clock clock) {
287289
this.clock = clock;
288-
addMandatoryClaimChecks();
289-
return new JWTVerifier(algorithm, expectedChecks);
290+
// Build the mandatory checks into a fresh list rather than mutating this builder's own
291+
// field, so build() is idempotent and can be called repeatedly without duplicating them.
292+
List<ExpectedCheckHolder> checks = new ArrayList<>(expectedChecks);
293+
addMandatoryClaimChecks(checks);
294+
return new JWTVerifier(algorithm, checks);
290295
}
291296

292297
/**
@@ -299,17 +304,17 @@ public long getLeewayFor(String name) {
299304
return customLeeways.getOrDefault(name, defaultLeeway);
300305
}
301306

302-
private void addMandatoryClaimChecks() {
307+
private void addMandatoryClaimChecks(List<ExpectedCheckHolder> checks) {
303308
long expiresAtLeeway = getLeewayFor(RegisteredClaims.EXPIRES_AT);
304309
long notBeforeLeeway = getLeewayFor(RegisteredClaims.NOT_BEFORE);
305310
long issuedAtLeeway = getLeewayFor(RegisteredClaims.ISSUED_AT);
306311

307-
expectedChecks.add(constructExpectedCheck(RegisteredClaims.EXPIRES_AT, (claim, decodedJWT) ->
312+
checks.add(constructExpectedCheck(RegisteredClaims.EXPIRES_AT, (claim, decodedJWT) ->
308313
assertValidInstantClaim(RegisteredClaims.EXPIRES_AT, claim, expiresAtLeeway, true)));
309-
expectedChecks.add(constructExpectedCheck(RegisteredClaims.NOT_BEFORE, (claim, decodedJWT) ->
314+
checks.add(constructExpectedCheck(RegisteredClaims.NOT_BEFORE, (claim, decodedJWT) ->
310315
assertValidInstantClaim(RegisteredClaims.NOT_BEFORE, claim, notBeforeLeeway, false)));
311316
if (!ignoreIssuedAt) {
312-
expectedChecks.add(constructExpectedCheck(RegisteredClaims.ISSUED_AT, (claim, decodedJWT) ->
317+
checks.add(constructExpectedCheck(RegisteredClaims.ISSUED_AT, (claim, decodedJWT) ->
313318
assertValidInstantClaim(RegisteredClaims.ISSUED_AT, claim, issuedAtLeeway, false)));
314319
}
315320
}

lib/src/test/java/com/auth0/jwt/JWTVerifierTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,4 +1325,18 @@ public void shouldThrowIncorrectClaimNotNpeWhenNullSubjectExpectedButClaimPresen
13251325
JWTVerifier.init(Algorithm.HMAC256("secret")).withSubject(null).build().verify(token));
13261326
assertThat(e.getClaimName(), is("sub"));
13271327
}
1328+
1329+
@Test
1330+
public void shouldBuildReusableVerifierWithoutDuplicatingMandatoryChecks() {
1331+
// H3: building the same builder twice must not duplicate the mandatory exp/nbf/iat checks,
1332+
// nor retroactively mutate the first (supposedly immutable) verifier.
1333+
Verification verification = JWTVerifier.init(Algorithm.HMAC256("secret")).withIssuer("auth0");
1334+
JWTVerifier verifier1 = verification.build();
1335+
int sizeAfterFirstBuild = verifier1.expectedChecks.size(); // 1 issuer + 3 mandatory = 4
1336+
JWTVerifier verifier2 = verification.build();
1337+
1338+
assertThat(sizeAfterFirstBuild, is(4));
1339+
assertThat(verifier1.expectedChecks.size(), is(sizeAfterFirstBuild)); // first not retroactively mutated
1340+
assertThat(verifier2.expectedChecks.size(), is(4)); // second not doubled to 7
1341+
}
13281342
}

0 commit comments

Comments
 (0)