When does JWK expire? JWKS rotation policy

9.6k Views Asked by At

I am reading about JWKS and found information about the key rotation concept - https://developer.okta.com/docs/concepts/key-rotation/

Let's assume I use JWKS in my application but I don't fetch them periodically, so just hardcoded. The single key JSON object looks like

{
      "kty": "RSA",
      "e": "xxx",
      "use": "sig",
      "kid": "xxx",
      "x5t": "xx",
      "x5c": [
        "xxx"
      ],
      "n": "xxx

}

The JWKS provides you the public key so you can validate JWT. Now questions.

  1. Is it possible to get information when JWKS expires? For example, can I generate a .cert file using a JWK and open it to check expiration day?
  2. Does the JWKS provider expose information when the key rotation is planned or maybe it is sensitive information?

And please consider the example above, so I have keys in the application and would like to know when I should replace them.

Of course I know that it is bad practice (I should fetched keys directly from JWKS endpoint and feel safe) but this is only an example (if it is a stupid example, please propose a better one just to describe the context).

2

There are 2 best solutions below

1
On BEST ANSWER
  1. JSON Web Key Set (JWKS aka JWK Set) is a list of JSON Web Keys (JWKs). Since JWK Set is simply a container, it contains no metadata such as an expiration date/time.

  2. It does not expose this for at least two reasons:

  • RFC 7517 is the specification that governs the behavior of JWKs and JWK Set. It does not mention or require the provider to publish an expiration date/time. Perhaps this is so due to reason #2:
  • The provider should be able to remove keys for any reason at any time. Possible reason: key has been compromised. (For a private/public keypair, this would mean the private key has been compromised and the corresponding public key published via JWKS should be removed from circulation). This example is an outlier but it does happen and the provider would have to act immediately to fix it.

Emergencies notwithstanding, providers do rotate keys on a regular basis as a matter of good security hygiene. To handle key rotation (be it planned or emergency), your application should adhere to a simple algorithm. It should periodically fetch the keys from JWKS endpoint, build a local replica of all keys and add/remove keys from this replica based on the last fetch. Only keys found in the local replica should be used by your application to perform a cryptographic operation such as verifying a signature on a JWT.

Each JWK has a kid (key id) parameter and this parameter is used to match a specific key. RFC 7517 recommends using kid to choose among a set of keys within a JWK Set during key rollover. When your application does a fetch of keys from JWKS, you'll be comparing the set of keys coming from JWKs to the set of keys in your local replica. The comparison is based on kid. If a key with some kid is present in JWKS but not present in your local replica, you should add this key to your replica. Vice versa, if a key with some kid is present in your local replica but not present in JWKS, you should remove this key from your local replica.

How frequently should your application fetch the keys from JWKS? This is up to you, it depends on the risk tolerance of your app and/or your organization. Some apps fetch every minute, others do it hourly or daily.

Let's say your app never does this fetch, the key is hardcoded in your app. This will work until the key is removed by the provider. (We're assuming that we're talking about a public key here. A JWK could represent a private key...and that you will not want to embed into your app). Some providers don't rotate keys or do so once in a very long while. If you're dealing with a well-known (to you) provider and they guarantee to you that they won't rotate keys, your risk of embedding a key into your app is low.

In general, embedding a public key into the app is not a good idea. If you're going to be using a JWKS endpoint, implement a simple fetch + update solution as outlined above.

0
On

For practicality, you should rotate your keys based on the impact it has on the security and the user experience. It also depends on your implementation.

The actors are

  • client
  • resource provider (RP)
  • identity provider (IP) implementation detail... there's no reason an IP and RP cannot be the same entity, that's how it was before all these OIDC stuff. but it is still good to think about it in those terms in case you want to scale or use alternate OIDC providers)

Let's assume your implementation is that your access tokens are Json Web Signature (JWS) content of your JWT claims. The JWT in access token approach primarily replaces the notion of JSESSIONID along with the storage of such information in the backend and forwarding that storage responsibility to the client.

From the client perspective:

  1. If the client has no OAuth token it does some process (e.g. OIDC) to get the OAuth token

  2. If the client has an OAuth token it sends the access_token as the bearer to the RP

  3. If the RP responds with 401 or the client thinks the RP will respond with a 401.

    • the client will use the refresh token endpoint to get a new token from the IP
    • if the IP responds in error, the refresh process failed and the user is logged out
    • else continue
  4. Else just standard RP response

The 3rd step specifies the refresh token process. This needs to be noted as that also needs to be factored in when determining the rotation policy.

Given the scenario in the quoted section, there's only one party that would really need to care about the signature, the one that is consuming the JWT for the purpose of identifying the user and authorization level data, that's the RP

The JWT in the bearer token would be associated to a specific JWS, and the JWT can be reused until the time expires. Since this happens often the RP needs to have some layer that does this validation quickly. It does this by having the public key of the signer, that is provided by the IP. On the OpenID Connect (OIDC) standard it is referenced by the jwks_uri of OpenID Discovery. Regardless on how the RP retrieves it, the RP must have a trusted way of getting the key from the IP.

Let's put in some variables now

  • exp access token expiration TIME not duration
  • jwke JSON web key expiration TIME not duration
  • njwk number of active JWKs
  • maxjwke JSON web key expiration DURATION
  • maxexp access web key expiration DURATION

Given that jwke >= exp otherwise the client will get a 401 unexpectedly because the key is no longer valid for their access token.

Now for simplicity you can have a JWK per access token, that would work, and relatively secure, but will kill your backend as it's just creating crypto tokens all the time. But that's a minimum.

So at the very least I would have a reasonable set of JWKs as a pool, could be 10 could be 100 it depends on your tolerance.

Now what's the maximum? Well that depends on your tolerance and cost of creating the tokens.

But regardless of your tolerance values the one thing you need to ensure is for a given JWK it should NOT expire before the access time.

IP Implementation notes

Now let's say you're implementing in REDIS which has two key limitations:

  1. You cannot SCAN across cluster nodes
  2. Inability to expire hash elements

You can implement it as follows

  • create hash pairs of njwk elements containing the public JWK and private key (stored as encoded bytes)
  • each hash pair will be keyed based on some time block say day of the week assuming that maxexp will not go past maxjwke.
  • each hash pair will have a default expiration of maxjwke
  • when key is used for an access token, it will extend the expiration ONLY OF THE PUBLIC KEY such that it is maxjwke + remaining access token expiration time. (this bit ensures that the access token will work within the access period it has. The private key is is still set to expire.
  • when providing the list of JWKS it should provide current and last set so long as it hasn't expired.
  • when choosing the key to sign with it should choose from a the current bucket which has not expired.