Why does VerifyJWT return success, even if the exp claim (expiry) is missing?

a customer writes:

I have configured my VerifyJWT policy like this:

<VerifyJWT name="JWT-VerifyAssertionFromApp">
    <Algorithm>RS256</Algorithm>
    <Source>request.formparam.assertion</Source>
    <TimeAllowance>10</TimeAllowance>
    <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
   <PublicKey>
        <Value ref="verifyapikey.VerifyAPIKey-1.public_key"/>
    </PublicKey>
    <Audience>urn://www.apigee.com/apitechforum/token</Audience>
</VerifyJWT>

The JWT payload looks like this:

{
  "iss": "3r6bjRdkqnwG8v9Kb0KSOCjWS2ARnpnj",
  "sub": "somebody@example.com",
  "aud": "urn://www.apigee.com/apitechforum/token",
  "iat": 1526919625,
  "uid": "example1",
  "email": "fabricator.1123971817@example.com"
}

Notice: No Expiry claim! Yet the VerifyJWT policy succeeds. What gives?

0 1 900
1 REPLY 1

It is not required to have an exp claim in a JWT. Here's the relevant section in the JWT specification. The money quote:

The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a NumericDate value. Use of this claim is OPTIONAL.

The VerifyJWT policy

  • enforces the exp claim if it is present.
  • does not insist that the exp claim must be present.

If you want to check that an exp claim is present, for now, you must insert a separate logical step following the VerifyJWT. It should look like this in the flow:

  <Step>
    <!-- verify that the JWT was signed by our expected party, uses 
         the expected algorithm, and if it has time settings, that the JWT 
         is valid -->
    <Name>VerifyJWT-1</Name>
  </Step>
  <Step> 
    <!-- verify that there is an expiry on the JWT -->
    <Condition>jwt.VerifyJWT-1.claim.expiry = null </Condition>
    <Name>RaiseFault-MissingExpectedExpiryClaim</Name>
  </Step>

If the exp claim is present, then the jwt.VerifyJWT-1.claim.expiry variable will be non-null (assuming the VerifyJWT policy name is VerifyJWT-1). If exp is not present, then the value will be null and the RaiseFault policy will execute. The RaiseFault-MissingExpectedExpiryClaim is a RaiseFault policy that you would have to provide that sends back an appropriate message.

Checking that an exp claim is present may or may not be valuable and important. For example if the JWT is issued by accounts.google.com, then it will always have an expiry. It would be redundant to check that an expiry exists, if you have already verified that the JWT is signed by Google. But some people prefer explicit redundancy.

BTW, there's little to no performance impact to checking for a null value. (It will be nanoseconds, even at scale. It's just a memory comparison.)

You would use similar approaches for other "extra check" cases - for example, some systems that accept JWT not only check that the JWT is not expired, but also that

  1. The expiry is not more than 5 minutes in the future. (In other words, the JWT must be very short lived)
  2. The JWT has never before been presented.
  3. The JWT is presented over a mutual-TLS connection and the client-side fingerprint is X
For case 1, the condition would be:
   jwt.Verify-JWT-1.seconds_remaining > 300

This means: throw a fault if the JWT has a remaining life longer than 300 seconds. You might also want to check jwt.Verify-JWT-1.claim.issuedat to see that it is not more than 10 or 20 seconds ago, if you want to insure that the JWT has been created "recently."

And for case 2, you'd want to perform a LookupCache on the JTI or something similar, to check that the JWT has not been presented to Apigee Edge previously.

For case 3, the TLS thing, that's another variable check and RaiseFault.

You might think: Well, why not endow the VerifyJWT policy with all the capability to perform these checks automatically? And the reason is: it becomes more complicated to use in simple cases.

The intent of the VerifyJWT policy is to serve as a building block that complements the other policy "building blocks" within Apigee Edge. We want the basic things to be easy, and the optional things to be possible. And also, we'd like the policy configuration and flow to be "readable" so someone could look at it, and understand what's happening. Adding a set of optional checks into the VerifyJWT policy, we judged, would make it too complex.

You can configure the policy to check for a claim value, but you cannot configure it (today) to check that the exp claim is present. That is where we drew the line.

Typically customers embed the VerifyJWT policy along with the other checks (for existence of an exp claim, for example) in a FlowCallout. Any proxy that wants to follow the JWT verification convention for Company X, will then call the flowcallout and get the standard behavior.

We are always looking for feedback. We may enhance the capability of the VerifyJT policy to check that an exp claim is present, if it looks like it would significantly simplify the usage of many (most?) customers.