Document how to validate token is not expired

Issue #29 closed
Ivam dos Santos Luz created an issue

Hi,

We are using jose4j for generating JWTs in our application.

Our flow is:

  1. User requests a token;

  2. We generate the token using JsonWebEncryption class

  3. Clients store the token and pass it on every request to our API;

  4. In our API, there will be a filter which will intercept the requests, pull the token from HTTP headers and validate it to approve or reject the request.

We are able to parse the token by calling JwtClaims.parse(), but this method doesn't seem to consider the JWT expiration value.

What's the best way to validate if the token is still valid in our server? We tried to use JwtConsumer (in a way similar to the example in the docs) to do the task, but the following error is raised:

            JwtConsumer jwtConsumer = new JwtConsumerBuilder()
            .setRequireExpirationTime() // the JWT must have an expiration time
            .setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
            .setRequireSubject() // the JWT must have a subject claim
            .setExpectedIssuer(ISSUER) // whom the JWT needs to have been issued by
            .setExpectedAudience(AUDIENCE) // to whom the JWT is intended for
            .build(); // create the JwtConsumer instance
Invalid JWT! org.jose4j.jwt.consumer.InvalidJwtException: Unable to process JOSE object (cause: 

org.jose4j.lang.InvalidKeyException: The key must not be null.)

Comments (15)

  1. Brian Campbell repo owner

    Right, JwtClaims.parse() only parses from JSON into the JwtClaims object - it doesn't validate or check exp or any of the claim values. With that, if all you care about is checking the expiration, you could do something like this:

       JwtClaims claims = JwtClaims.parse(... JSON claims ....);
       NumericDate expirationTime = claims.getExpirationTime();
       if (expirationTime.isBefore(NumericDate.now()))
       {
          throw new InvalidJwtException("expired at " + expirationTime); // or whatever 
       }
    

    But JwtConsumer and JwtConsumerBuilder are really what is intended to do JWT processing and validation. Sounds like you are encrypting your JWTs (JsonWebEncryption) so you'll need to provide a decryption key to the JwtConsumerBuilder with .setDecryptionKey(...) or setDecryptionKeyResolver(...). If it's only encrypted (if so, hopefully using symmetric encryption), you'll also need to tell the builder that it's okay to not have a signature with .setDisableRequireSignature().

  2. Steve Hu

    @b_c I am using this library on several projects and want to say thank you for this wonderful creation. I have one issue/question that I want to clarify. When you verify jwt token from the following code, how do you identify if the token is expired or other exceptions happen as there is only one InvalidTokenException thrown. I need to send a clear error message to the API consumer that token is expired and renew/refresh is needed. Thanks.

                JwtConsumer jwtConsumer = new JwtConsumerBuilder()
                        .setRequireExpirationTime()
                        .setAllowedClockSkewInSeconds(
                                (Integer) jwtConfig.get(JwT_CLOCK_SKEW_IN_SECONDS))
                        .setSkipDefaultAudienceValidation()
                        .setVerificationKeyResolver(x509VerificationKeyResolver)
                        .build();
                JwtContext jwtContext = jwtConsumer.process(jwt);
                claims = jwtContext.getJwtClaims();
    
  3. Brian Campbell repo owner

    @stevehu, yes there is just InvalidJwtException which has a list of string "details" about individual validation issues. You could look for the substring of "The JWT is no longer valid - the evaluation time" in the string returned from getMessage() from InvalidJwtException. That'd work well as long as the error message doesn't change :)

    You could also check again specifically for expired token after a validation error and handle that however you need to. Something like this:

            try
            {
                jwtContext = jwtConsumer.process(jwt);
                claims = jwtContext.getJwtClaims();
            }
            catch (InvalidJwtException e)
            {
                JwtConsumer consumerAgain = new JwtConsumerBuilder()
                        .setSkipAllValidators()
                        .setDisableRequireSignature()
                        .setSkipSignatureVerification()
                        .build();
    
                claims = consumerAgain.processToClaims(jwt);
    
                if ((NumericDate.now().getValue() - secondsOfAllowedClockSkew) >= claims.getExpirationTime().getValue())
                {
                    // send a clear error message to the API consumer that token is expired and renew/refresh is needed
                    System.out.println("JWT is Expired!!!!!");
                }
                else
                {
                    throw e;
                }
            }
    
  4. Steve Hu

    @b_c Thanks for the quick response. I actually forked the library and checked "The JWT is no longer valid -" and throw a TokenExpiredException, but I don't feel comfortable in doing so as this string might be changed. I am wondering if there is anything can be done internal to throw another exception instead of one generic InvalidJwtException. I can work on it under your guidance and submit a pull request. Your second option is robust but it is a little weird by processing twice. If we can set the error message to "The JWT is no longer valid", can we just throw an exception there? It breaks backward compatibility but I think most people will be happy to separate signature verification failure vs expired token. Thanks.

  5. Brian Campbell repo owner

    @stevehu, backward compatibility is something I need to take seriously and there are already a significant number of users of this library whom I have to consider .

    There are many other reasons a JWT might be considered invalid and providing programatic access to common reasons is a bigger chuck of work. I would want to do something holistic rather than a one-off for one particular invalid claim. I'm not sure when or if that work could get done.

    I understand not wanting to rely on a specific string but it's a value that's unlikely to change and you could put a little unit test in your code base to catch it if it did break/change on upgrade of the library. No forking is needed either. Just something like this in the code that uses JwtConsumer:

            try
            {
                JwtContext jwtContext = jwtConsumer.process(jwt);
                claims = jwtContext.getJwtClaims();
            }
            catch (InvalidJwtException e)
            {
    
                if (e.getMessage().contains("The JWT is no longer valid - the evaluation time"))
                {
                    // send a clear error message to the API consumer that token is expired and renew/refresh is needed
                    System.out.println("JWT is Expired!!!!!");
                }
                else
                {
                    throw e;
                }
            }
    

    Processing twice may seem weird but there are legitimate use cases for it. See https://bitbucket.org/b_c/jose4j/wiki/JWT%20Examples#markdown-header-two-pass-jwt-consumption for example. There is only a little overhead in the double processing shown before - decoding and parsing happens twice. But crypto (the expensive part) only happens once. But you can make it have nearly zero overhead doing the two pass processing like this with a check on exp in the middle.

            JwtConsumer consumer = new JwtConsumerBuilder()
                    .setSkipAllValidators()
                    .setDisableRequireSignature()
                    .setSkipSignatureVerification()
                    .build();
    
            JwtContext jwtContext = consumer.process(jwt);
            JwtClaims jwtClaims = jwtContext.getJwtClaims();
    
            int secondsOfAllowedClockSkew = 30;
            if ((NumericDate.now().getValue() - secondsOfAllowedClockSkew) >= jwtClaims.getExpirationTime().getValue())
            {
                // send a clear error message to the API consumer that token is expired and renew/refresh is needed
                System.out.println("JWT is Expired!!!!!");
                throw new RuntimeException("or whatever that says JWT is Expired!!!!!");
            }
    
            consumer = new JwtConsumerBuilder()
                    .setRequireExpirationTime()
                    .setAllowedClockSkewInSeconds(secondsOfAllowedClockSkew)
                    .setSkipDefaultAudienceValidation()
                    .setVerificationKey(rsaFig4Jwk.getPublicKey())
                    .build();
    
            consumer.processContext(jwtContext);
    
            System.out.println(jwtClaims);
    

    And, of course, you can fork it and make whatever changes you want (likley in NumericDateValidator ~ 92 and JwtConsumer ~ 413). But you'd have to keep your changes in sync with updates. And I can't take a pull request that's just a one-off for expired jwts.

  6. Steve Hu

    @b_c Thanks for the detailed explanation and all different options. I will test some of the options and adopt the fastest one. Thanks for the great work!

  7. Brian Campbell repo owner

    Created #76 to track the feature request of a way to provide programmatic access to specific reasons for JWT invalidity

  8. Brian Campbell repo owner

    related is 1ff420d to "address issue #76 by providing programatic access to (some) specific reasons for JWT invalidity through error codes on InvalidJwtException"

  9. Vaishali Jain

    @b_c, I am using this library on several projects and want to say thank you all for this effort. I am using it in my project and facing one issue which I am unable to troubleshoot. I got following exception org.jose4j.jwt.consumer.InvalidJwtException: Unable to process JOSE object (cause: org.jose4j.lang.InvalidKeyException: Key cannot be null) Can you please help me understand in what scenario we get this exception?

    Thanks!

  10. Log in to comment