Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ConcurrentModificationException bug #592

Closed
ppawlowski15 opened this issue Jun 28, 2022 · 10 comments · Fixed by #605
Closed

ConcurrentModificationException bug #592

ppawlowski15 opened this issue Jun 28, 2022 · 10 comments · Fixed by #605
Labels
bug This points to a verified bug in the code

Comments

@ppawlowski15
Copy link

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)

@poovamraj
Copy link
Contributor

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

@poovamraj poovamraj added the waiting for customer This issue is waiting for a response from the issue or PR author label Jun 30, 2022
@ppawlowski15
Copy link
Author

ppawlowski15 commented Jul 1, 2022

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

@poovamraj poovamraj added bug This points to a verified bug in the code and removed waiting for customer This issue is waiting for a response from the issue or PR author labels Jul 8, 2022
@poovamraj
Copy link
Contributor

poovamraj commented Jul 26, 2022

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

@kpmueller
Copy link

kpmueller commented Aug 20, 2022

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
Copy link

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.

@ppawlowski15
Copy link
Author

@poovamraj

@jimmyjames
Copy link
Contributor

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!

@kpmueller
Copy link

Thanks much! Your library is much appreciated :)

@ppawlowski15
Copy link
Author

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

@jimmyjames
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This points to a verified bug in the code
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants