Authentication using JWKUrl

Motivation

To extend support for Authentication with Identity providers that publish JWKs (required for signing JWT) at URL (JWKUrl).

User Impact

Users will now be able to Authenticate with many more Identity Providers such as Firebase and OneGraph.

Implementation

Introduction

As defined by IETF, JSON Web Key (JWK) is a JSON data structure that represents a cryptographic key used for signing/encryption. Below is an example JWK :

{
      "alg": "RS256",
      "e": "AQAB",
      "n": "zk8r6zAVEktzfzrjVbpiWU_rhAs5mFcMrwcT4dVO_bDIw1gViVGk74O5RJMP6MGqHryNigIIvUhVKLYmyqYwk3JGD9whPXfS8RIPsqE3Y2_BtvbXx5yJdAECr_slKvDPfGfrVVcO_8QPw-03JP0WmkFRCBr6yLS7rNgCuZqUr2JQ-aYIxO2PCy64GoxOrgcqtDiVQ8ZluhJoUEGDC4GrDMQLxFNQ9xoj1rQm5L4_-2rIn0BoeI5Ox6n0a1CTjqGNW4PQkRVb_q2wpNzJzwGlqAE0vPsbfvePrwf4MPohnPKu7N6is9sntkltBNq2bFonaOw1t8Jksiz93hwpdFtPbQ",
      "kty": "RSA",
      "kid": "49ad9bc5e8e44793a2109b56e361a23b41880875",
      "use": "sig"
    }

JWKUrl hosts multiple such JWKs which are used in the verification of the JWT.

Implemention Details

Support for the JWKUrl field in Dgraph.Authorization JSON in the GraphQL Schema. Now it will look like

{
  VerificationKey: "",
  Header: "",
  Namespace: "",
  Algo: "",
  JWKURL: "",
  Audience: ""
}

Users will be allowed to give only one of JWKURL or (VerificationKey, Algo) and not both.

  • If (VerificationKey, Algo) is provided then the GraphQL server will verify JWT against this VerificationKey
  • If JWKURL is provided then Server will fetch all the JWKs and verify the token against one of the JWK based on the kid of JWK.

Some Identity Providers (Firebase) share the JWKs among multiple tenants. In this case, it is required for the user to provide the proper value of Audience in Dgraph.Authorization JSON. Failing to do so might be a major security risk.

Handling Rotating Keys

Some Identity Providers rotate the keys periodically, for this the GraphQL server will check the max-age or other similar directives in the Cache-Control Header or Expires headers in the response from the JWKURL when it fetches the Keys for the first time( When the Sever Starts or Schema Gets Updated) . On the basis of it, set up a goroutine to periodically fetch the keys from the JWKURL.
However, if the GraphQL server is unable to parse a valid value from these Headers or the Headers are not present altogether, then it will not re-fetch the keys.

Any Schema Update which changes the Dgraph.Authorization JSON should also re-trigger the fetching of the keys from the JWKURL.

Since we are planning to support three providers Auth0, Firebase, and OneGraph. Can you describe what mechanism each of them provide to support key rotation and expiry?

1- Firebase: They rotate keys regularly. The expiry time is mentioned in max-age in the Cache-Control header in the response of jwk_url. See this jwk_url.

2- Auth0: Not on regular basis(as per my findings). However, users can do it manually through the dashboard or from the API.
Reference You can now Rotate Signing Keys! - Auth0 Community

3- OneGraph: here

1 Like

This looks pretty nice.

The idea around jwk_url is essentially what the discuss tutorial mimics but from the client side - it pulls the jwk_url from a settings file when updating the Slash GraphQL schema.

That experience makes me wonder if we need the key id in the Dgraph.Authorization JSON ?

I wonder if it’s better to grab the max age from the response (will this only work for firebase), or just require it in the settings, which would allow it to work in any devops process.

More generally, I think I need to understand how the keys and key ids are going to work. For this reason:

  • a user logs into an app they have a valid JWT signed with key1
  • while that session is still in process the app rotates it’s keys, so key2 is now the key being used to sign new JWTs
  • somehow Slash knows about the key rotation
  • what happens to the ongoing session with key1 ? That should still be valid unless the key used to sign it gets revoked. this also seems to say that.

So I wonder that the right approach is to read that jwk_url and accept any keys in there, except the revoked ones??

(… more broadly, I wonder if we should be accepting multiple keys all round??? e.g. if I’m doing key rotation without the jwk_url, then I’d want the current and next keys … probably leave this till someone asks though)

This will happen anyway, right? Even if I upload exactly the same schema as is in the system, everything gets refreshed. That would allow me to have a devops procedure that rotates some keys and then pings Slash to let it know to refresh from the jwk_url.

So I think there’s two possible valid processes here: one is refetch based on a timer, the other is refetch on schema being touched.

(Side note: I don’t think we should be supporting any identity provers in particular. I should be a general mechanism that works nicely with the given providers, but also works in all sorts of other curcumstances.)

Thanks for the detailed comment.

It is not necessary to take key id in Dgraph.Authorization JSON. We fetch all the JWK’s from the jwk_url, then decode our JWT token. The decoded JWT token contains kid in its header in the case and we need to verify it against the signing key with the same kid from the fetched list of JWK’s.

This is the example header of the decoded JWT issued from One of the Identity Provider:

{
 alg: "RS256",
 typ: "JWT",
 kid: "kiCD9MttZUa5IZRhmgj_n"
}.

When we just rotate the keys, the previous JWK is also present there, thus fetching JWKs from the jwk_url also fetches the previously used JWK. Any JWT which was signed by the previous key is still valid (as we have one of the JWK with exact same kid as in the decoded JWK). However, if we revoke the key, it gets removed from the jwk_url thereby making any token signed by it invalid.

Yeah, It should happen even if the same schema is being refreshed.

Both of these processes need to be incorporated because there could be two possible cases.
1- If the signing keys are rotated on regular intervals then the user shouldn’t bother about it. He just needs to convey to the server about the rotating nature of the keys.
2- If the user manually wants to change the rotating keys, then updating the schema should trigger that.

Parsing max-age from the response works only for firebase (as per my understanding).
Taking it in the settings is also a Good Idea.

Ah, looks like I missed a trick in my thing.

Yeah, but this does imply that we have to be able to validate the JWT with both the current key and the previous key.

:+1:

Yeah, so I’d avoid having that if it’s so bespoke - at least in a first cut, if loads of users want more firebase support, then go for it, but in a first version, we need the max age in the settings to accomodate more cases, so don’t complicate things.

Yeah, this will be done until the old key is revoked, All the newly minted tokens will be verified against the newly generated key.

We can do the following:

  1. Provide the max-age in settings so that the user can configure it at the time of submitting the schema.
  2. Deal with the Response headers to specifically solve the problem for the firebase case (which a requested feature from some users)

yeah, let’s do this for now. The user can provide an appropriate value which works with their auth provider instead of us supporting particular providers.

Had a chat with Minhaj about this and we found that

max-age for firebase keeps on changing on making requests and user would have to set a low value for the refresh for this to work. This is because say the key was expiring in 6h and the user set the refresh interval to be 1h, then there could be an interval of 1h where we are still working with the old keys and auth requests would fail. So all in all unless we refresh keys when they expire from firebase, we’ll have a time window where auth doesn’t work for the user as expected. Ofcourse the user can get around this by setting a very low value like 1s for the refresh_interval but it seems like we should have some special handling for Firebase to make the experience smooth for the user.

So I saw me keep a refresh_interval setting but do something special for Firebase to make things work well with it.

1 Like