With VerifyJWT, How can I insure that a JWT is used once only?

I have a JWT, that I pass to VerifyJWT. How can I configure VerifyJWT so that it accepts a particular inbound token, once-and-only-once?

Solved Solved
0 6 727
1 ACCEPTED SOLUTION

You can't. (But read on)

It's not possible to configure the VerifyJWT policy to reject JWT that have been previously "seen" or handled.

However, it is straightforward to configure a policy flow in Apigee Edge to reject previously seen JWT. You can do this with some simple use of the Apigee Edge Cache.

The way it works is this:

  • Verify the JWT
  • Look in the ache for THIS particular JWT. If found, raise a fault.
  • store a unique identifier for the JWT in cache

That's the basic idea. The key to making this work is getting the time-to-live correct. The Cache TTL must be at least as long as the remaining life on the JWT, in order for this to work. Conveniently, the VerifyJWT policy sets a context variable containing the time remaining (jwt.POLICYNAME.seconds_remaining), and the PopulateCache accepts such a variable for the timeout.

For the "unique identifier", it's easiest if the JWT contains an jti claim. This is an identifier that is expected to be unique, and is set by the issuer. If that claim isn't present in the JWT, you can still use this caching approach. To get a unique identifier, though, you will have to compute a hash over the JWT (using one of the static functions available in AssignMessage /AssignVariable, like sha256Hex or md5Hex). The example that follows assumes the jti is present in the JWT.

The entire flow looks like this:

  <Step>
      <Name>VerifyJWT-1</Name>
  </Step>
  <Step>
      <Name>LookupCache-JTI</Name>
  </Step>
  <Step>
    <Condition>(cached_jti != null)</Condition>
    <Name>RaiseFault-Invalid-Authorization</Name>
  </Step>
  <Step>
      <Name>PopulateCache-JTI</Name>
  </Step>

The Lookup would be configured like this:

<LookupCache name='LookupCache-JTI'>
    <CacheResource>cache1</CacheResource>
    <AssignTo>cached_jti</AssignTo>
  <Scope>Application</Scope>
  <CacheKey>
    <KeyFragment ref='jwt.VerifyJT-1.claim.id' />
  </CacheKey>
</LookupCache>

The population would look like this:

<PopulateCache name='CP-1'>
  <CacheResource>cache1</CacheResource>
  <Source>jwt.VerifyJT-1.claim.id</Source>
  <Scope>Application</Scope>
  <CacheKey>
     <-- if id is not present in the JWT, replace the ref in the following
         line with a ref to a variable that contains a computed hash 
         of the JWT. -->
    <KeyFragment ref='jwt.VerifyJT-1.claim.id' />
  </CacheKey>
  <ExpirySettings>
    <TimeoutInSec ref="jwt.VerifyJWT-1.seconds_remaining"/>
  </ExpirySettings>
</PopulateCache>

Note: Expiry is not a required claim on JWT. For better error handling, You will probably want some error checks in there to verify that there actually IS an expiry on the JWT. Something like this:

  <Step>
      <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> 
  <Step>
      <Name>LookupCache-JTI</Name>
  </Step>
  <Step>
    <!-- check for replay -->
    <Condition>(cached_jti != null)</Condition>
    <Name>RaiseFault-Invalid-Authorization</Name>
  </Step>
  <Step>
      <Name>PopulateCache-JTI</Name>
  </Step>

The actual VerifyJWT is nothing special. It's like any VerifyJWT, something like this:

<VerifyJWT name='VerifyJWT-1'>
  <Source>variable_containing_jwt_here</Source>
  <Algorithm>RS256,RS384</Algorithm>
  <PublicKey>
    <Value ref='a_context_variable'/>
  </PublicKey>
  <!-- the following elements optionally specify claims to be verified -->
  <Subject ref='var-name-here'>explicit-string-here</Subject>
  <Issuer ref='var-name-here'>explicit-string-here</Issuer>
  <Audience ref='var-name-here'>explicit-string-here</Audience>
  <TimeAllowance>120s</TimeAllowance> <!-- defaults to 60s -->
</VerifyJWT><br>

You don't need all of Subject, Issuer, Audience... Only include those elements if you want the VerifyJWT to check those claims in the inbound JWT. (Not all applications require checking all claims!)


As for how to compute a unique hash on a JWT that does not have a jti, try something like this:

<AssignMessage name='AM-ComputeJwtHash'>
  <AssignVariable>
   <Name>jwthash</Name>
   <Template>{sha256Hex(variable_containing_jwt_here)}</Template>
  </AssignVariable>
</AssignMessage>

The sha256 hash will be computed - that's a byte array. Then it will be hex-encoded into a string, and placed in the variable jwthash. You can use THAT as the cache key in the Lookup and Populate steps above.

View solution in original post

6 REPLIES 6

You can't. (But read on)

It's not possible to configure the VerifyJWT policy to reject JWT that have been previously "seen" or handled.

However, it is straightforward to configure a policy flow in Apigee Edge to reject previously seen JWT. You can do this with some simple use of the Apigee Edge Cache.

The way it works is this:

  • Verify the JWT
  • Look in the ache for THIS particular JWT. If found, raise a fault.
  • store a unique identifier for the JWT in cache

That's the basic idea. The key to making this work is getting the time-to-live correct. The Cache TTL must be at least as long as the remaining life on the JWT, in order for this to work. Conveniently, the VerifyJWT policy sets a context variable containing the time remaining (jwt.POLICYNAME.seconds_remaining), and the PopulateCache accepts such a variable for the timeout.

For the "unique identifier", it's easiest if the JWT contains an jti claim. This is an identifier that is expected to be unique, and is set by the issuer. If that claim isn't present in the JWT, you can still use this caching approach. To get a unique identifier, though, you will have to compute a hash over the JWT (using one of the static functions available in AssignMessage /AssignVariable, like sha256Hex or md5Hex). The example that follows assumes the jti is present in the JWT.

The entire flow looks like this:

  <Step>
      <Name>VerifyJWT-1</Name>
  </Step>
  <Step>
      <Name>LookupCache-JTI</Name>
  </Step>
  <Step>
    <Condition>(cached_jti != null)</Condition>
    <Name>RaiseFault-Invalid-Authorization</Name>
  </Step>
  <Step>
      <Name>PopulateCache-JTI</Name>
  </Step>

The Lookup would be configured like this:

<LookupCache name='LookupCache-JTI'>
    <CacheResource>cache1</CacheResource>
    <AssignTo>cached_jti</AssignTo>
  <Scope>Application</Scope>
  <CacheKey>
    <KeyFragment ref='jwt.VerifyJT-1.claim.id' />
  </CacheKey>
</LookupCache>

The population would look like this:

<PopulateCache name='CP-1'>
  <CacheResource>cache1</CacheResource>
  <Source>jwt.VerifyJT-1.claim.id</Source>
  <Scope>Application</Scope>
  <CacheKey>
     <-- if id is not present in the JWT, replace the ref in the following
         line with a ref to a variable that contains a computed hash 
         of the JWT. -->
    <KeyFragment ref='jwt.VerifyJT-1.claim.id' />
  </CacheKey>
  <ExpirySettings>
    <TimeoutInSec ref="jwt.VerifyJWT-1.seconds_remaining"/>
  </ExpirySettings>
</PopulateCache>

Note: Expiry is not a required claim on JWT. For better error handling, You will probably want some error checks in there to verify that there actually IS an expiry on the JWT. Something like this:

  <Step>
      <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> 
  <Step>
      <Name>LookupCache-JTI</Name>
  </Step>
  <Step>
    <!-- check for replay -->
    <Condition>(cached_jti != null)</Condition>
    <Name>RaiseFault-Invalid-Authorization</Name>
  </Step>
  <Step>
      <Name>PopulateCache-JTI</Name>
  </Step>

The actual VerifyJWT is nothing special. It's like any VerifyJWT, something like this:

<VerifyJWT name='VerifyJWT-1'>
  <Source>variable_containing_jwt_here</Source>
  <Algorithm>RS256,RS384</Algorithm>
  <PublicKey>
    <Value ref='a_context_variable'/>
  </PublicKey>
  <!-- the following elements optionally specify claims to be verified -->
  <Subject ref='var-name-here'>explicit-string-here</Subject>
  <Issuer ref='var-name-here'>explicit-string-here</Issuer>
  <Audience ref='var-name-here'>explicit-string-here</Audience>
  <TimeAllowance>120s</TimeAllowance> <!-- defaults to 60s -->
</VerifyJWT><br>

You don't need all of Subject, Issuer, Audience... Only include those elements if you want the VerifyJWT to check those claims in the inbound JWT. (Not all applications require checking all claims!)


As for how to compute a unique hash on a JWT that does not have a jti, try something like this:

<AssignMessage name='AM-ComputeJwtHash'>
  <AssignVariable>
   <Name>jwthash</Name>
   <Template>{sha256Hex(variable_containing_jwt_here)}</Template>
  </AssignVariable>
</AssignMessage>

The sha256 hash will be computed - that's a byte array. Then it will be hex-encoded into a string, and placed in the variable jwthash. You can use THAT as the cache key in the Lookup and Populate steps above.

It would be nice if this post showed us what the `VerifyJWT-1` step looks like. It references it but never shows it. And can we use the `generateJWT` policy to automatically get the random JTI?

Hi Dave, I've updated the answer to show an example VerifyJWT policy , and add some text around that.

I also elaborated on the JTI issue.

As for how you generate the jti.... the issuer of the JWT will set the JTI. The JTI is just an id, and it's represented as a claim in the signed payload of the JWT. Often it's a guid/uuid, but that is not required. The spec says it should be "unique" and I suppose that means unique for the given issuer. It is possible, as you imply, to use GenerateJWT to generate a JWT that includes a jti claim. Just use the Id element in the GenerateJWT policy. If you use an empty element, you will get a random uuid.

Because the JTI is part of the signed payload, and because we are relying on the fact that the issuer is assuring us that the jti will be unique, then we can be sure that any time we get a JWT with that JTI, and the signature validates, it's the same JWT.

  • It's not valid for the issuer to issue multiple JWT with the same jti. If the issuer did that, the arrangement i describe above is not vulnerable to any security issue. In fact that flow would reject a "novel" JWT with a non-unique JTI, and that "fails closed" if you get what I mean.
  • It won't be possible for the client to modify the JWT to insert a jti after the signature is generated. The signature verification of the JWT would fail in that case, and once again, the approach described above would "fail closed".

Thanks for the quick reply!

[BTW, I am also a former Transarc Pittsburgh employee. I never worked with you, but I started in 1996, and just recently got laid off by IBM. It was cool to see your name connected to Apigee!]

Darn, I knew I recognized your name! Whoo-hoo! Cool to see you here too! You're now ... with a certain sporting goods company, eh?

Does this token has to be linked to a single transaction? Perhaps something similar to this?