java-jwt icon indicating copy to clipboard operation
java-jwt copied to clipboard

ConcurrentModificationException bug

Open ppawlowski15 opened this issue 2 years ago • 3 comments

Describe the problem

I'm using single instance of Verifier private static final Verification verifier = JWT.require(Algorithm.RSA256(getPublicKey("/keys/public.pem"), null));

and I'm getting ConcurrentModificationException from time to time during call verify method

java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1043) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:997) at java.base/java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1047) at com.auth0.jwt.JWTVerifier.verifyClaims(JWTVerifier.java:473) at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:460) at com.auth0.jwt.JWTVerifier.verify(JWTVerifier.java:441

Environment

Version of this library used: java-jwt-4.0.0
Version of Java used: 11
OS: Ubuntu 20.04

Exception doesn't appear with previous lib version (3.19.2)

ppawlowski15 avatar Jun 28 '22 10:06 ppawlowski15

Hi @ppawlowski15, can you share the code snippet that is throwing this error as well?

poovamraj avatar Jun 30 '22 08:06 poovamraj

private static final Verification verifier = JWT.require(Algorithm.RSA256(getPublicKey("/keys/public.pem"), null));

public DecodedJWT verifyToken(String token){ return verifier.build().verify(token); }

Maybe I should finalize builder of Verifier in first line to get JWTVerifier there once. Anyway the exception shouldn't be thrown, I think

@poovamraj

ppawlowski15 avatar Jul 01 '22 08:07 ppawlowski15

Hi @ppawlowski15 I used your following code to reproduce this issue and tried it even with multiple threads and still not reproducing. Any help in reproducing this would be amazing

private static final Verification verifier = JWT.require(Algorithm.HMAC256("your-256-bit-secret"));

public static DecodedJWT verifyToken(String token){
    return verifier.build().verify(token);
}

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            DecodedJWT a = verifyToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c");
            System.out.println(a);
        }).start();
    }
}

poovamraj avatar Jul 26 '22 12:07 poovamraj

hi,

I encountered a similar problem. I think this is caused by the documentation being unclear about how to use the builder and build methods when it comes to concurrency and thread-safety. (at least, I found it unclear on how to do it with thread-safety)

When the build method is called, there is an array of checks that are copied into the new JWTVerifier instance.

public static DecodedJWT verifyToken(String token){
    return verifier.build().verify(token);
}

Then in the new instance, the claims are copied:

        private void addMandatoryClaimChecks() {
            long expiresAtLeeway = getLeewayFor(RegisteredClaims.EXPIRES_AT);
            long notBeforeLeeway = getLeewayFor(RegisteredClaims.NOT_BEFORE);
            long issuedAtLeeway = getLeewayFor(RegisteredClaims.ISSUED_AT);

            expectedChecks.add(constructExpectedCheck(RegisteredClaims.EXPIRES_AT, (claim, decodedJWT) ->
                    assertValidInstantClaim(RegisteredClaims.EXPIRES_AT, claim, expiresAtLeeway, true)));
            expectedChecks.add(constructExpectedCheck(RegisteredClaims.NOT_BEFORE, (claim, decodedJWT) ->
                    assertValidInstantClaim(RegisteredClaims.NOT_BEFORE, claim, notBeforeLeeway, false)));
            if (!ignoreIssuedAt) {
                expectedChecks.add(constructExpectedCheck(RegisteredClaims.ISSUED_AT, (claim, decodedJWT) ->
                        assertValidInstantClaim(RegisteredClaims.ISSUED_AT, claim, issuedAtLeeway, false)));
            }
        }

This line creates new checks that are then added to the array:

           expectedChecks.add(constructExpectedCheck(RegisteredClaims.EXPIRES_AT, (claim, decodedJWT) ->
                    assertValidInstantClaim(RegisteredClaims.EXPIRES_AT, claim, expiresAtLeeway, true)));

Because this is calling expectedChecks.add on the JWTVerifier instance itself, this method is NOT thread-safe. The array gets read during the verify method. I'm also not sure it's a good idea, because I think this would add the same item repeatedly? But I didn't debug to that point.

To make it safe, a new list should be created that's returned as part of the call and used to create the new instance, or something similar. The list inside the instance cannot be modified once it's created to be thread-safe, or a concurrent version of the list needs to be used

private final List<ExpectedCheckHolder> expectedChecks;

However, the solution is a lot simpler: the build() method should be called only once, to create a single JWTVerifier instance. I believe it can be re-used and is thread-safe once it's built.

In other words, the original bugs code should be more like

private static final JWTVerifer verifier = JWT.require(Algorithm.RSA256(getPublicKey("/keys/public.pem"), null)).build();

public DecodedJWT verifyToken(String token){ return verifier.verify(token); }

and the test instance should be

private static final JWTVerifier verifier = JWT.require(Algorithm.HMAC256("your-256-bit-secret")).build();

public static DecodedJWT verifyToken(String token){
    return verifier.verify(token);
}

This is what I'm using now, and I think it's thread-safe.

I think the test didn't find it because when the verifier tried to verify has to occur at the same time a build is happening, during the list copy portion, while the verified is reading the list. It would probably take more threads and instances to find it.

kpmueller avatar Aug 20 '22 15:08 kpmueller

I think it would be helpful to split out the builder functionality into a separate builder class, which is more the common pattern you find out there. Because they are in the same class, it's not clear on how to use them.

It would also make it clear you can create a final JWTVerifier instance from the builder, and that could be clearly marked as thread-safe and final.

kpmueller avatar Aug 20 '22 15:08 kpmueller

@poovamraj

ppawlowski15 avatar Aug 24 '22 09:08 ppawlowski15

Thanks @ppawlowski15 and @kpmueller for the info.

@kpmueller is correct that you should create an instance of JWTVerifier using build() once, then reuse it.

@ppawlowski15 does doing that resolve the issue?

I also agree that the documentation needs to be improved, and if we can pull the builder out to its own class without introducing any breaking changes, we should do that.

Let us know @ppawlowski15 if building the verifier once addresses the problem, and we'll update the documentation and maybe pull out the builder class.

Thanks!

jimmyjames avatar Aug 26 '22 13:08 jimmyjames

Thanks much! Your library is much appreciated :)

kpmueller avatar Aug 28 '22 08:08 kpmueller

@jimmyjames Yes, i've updated to 4.0.0 and change my code to build Verifier only once and no more errors spotted ;)

ppawlowski15 avatar Aug 29 '22 08:08 ppawlowski15

I've updated the documentation in #605. We can't split out the builder to its own class in the 4.x release as it would be a breaking change, but the additional documentation should help provide clarity. Thanks all for raising and providing all the good info! 🙇

jimmyjames avatar Aug 30 '22 00:08 jimmyjames