auth0 / java-jwt

Java implementation of JSON Web Token (JWT)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

ConcurrentModificationException bug

ppawlowski15 opened this issue · comments

commented

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)

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

commented

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

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();
    }
}

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.

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.

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!

Thanks much! Your library is much appreciated :)

commented

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

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! 🙇