Some types and fields can contain data that is specific to a certain user. Cached query results that contain those types (or fields) should not be returned to any other user.

In order to handle this scenario, you can define "scopes" in your service which let you scope (hence the name) cached query results to specific headers and/or cookies.

Every service has a special PUBLIC scope it uses by default. This scope returns the same cached results for all users.

Basics

You can define scopes for your service based on the header and/or cookie your users use to authenticate. In our TypeScript configuration file you can add the following to do so.

"scopes": {
  "AUTHORIZATION_HEADER": "header:authorization",
  "SESSION_COOKIE": "cookie:session"
}

🚧

Header names are case-insensitive, cookie names are case-sensitive.

You can also combine multiple headers (or cookies, or a combination of both), into a single scope. This makes it easier to configure your cache rules. However, for a request to be served from the cache, subsequent requests need to match all defined headers and/or cookies. This includes the absence of specific headers or cookies as well.

"scopes": {
  "AUTHENTICATED": "header:Authorization|cookie:session",
}

With the above scope configuration and assuming the Authorization header for a user would be set to abcd, and the value of the session cookie would be 1234, you could have the following buckets for the AUTHENTICATED scope:

  • Authorization header present and set to abcd, and session cookie present and set to 1234
  • Authorization header present and set to abcd, session cookie not set
  • Authorization header not set, session cookie present and set to 1234
  • Authorizationheader not set and session cookie not set

Different values for either the Authorization header, or the session cookie would add additional cache buckets.

Using scopes

To set the scope a certain query result should be cached by, create a cache rule for the types you want to cache separately per-user (see Cache Rules for more information):

{
  "types": [
    "User"
  ],
  "maxAge": 900,
  "swr": 900,
  "scope": "AUTHENTICATED",
  "description": "Cache Users"
}

Now any query result that contains any User will be cached with the corresponding cookie and/or header of the AUTHENTICATED scope and will only be returned for the same requester. Users that aren't authenticated will still get the same cached results.

JWT Based Scopes

"scopes": {
    // To support JWT scopes we modified our config format so that you may
  // pass `definition` and the optional `jwt` option separately.
  "AUTHENTICATED": {
    "definition": "header:authorization",
    
    // Setting this marks the scope as "the value of the definition contains
    // a JWT". If you pass multiple headers and/or cookies in the definition,
    // we'll take the first existing value we can find.
    "jwt": {
        // Pass the claim by which the cache should be scoped
      "claim": "sub",
      
      // Pass the algorithm you use to sign your JWTs
      "algorithm": "HS256",
        
      // Pass the secret you use for signing (or the public key when using
      // an asymmetric algorithm)
      "secret": ":a_very_secret_passphrase"
      // or
      "secret": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\nkd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\ncKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\nmwIDAQAB\n-----END PUBLIC KEY-----\n"
    }
  }
  // Sidenote: The shorthand for passing just a string here still works!
  "ANOTHER_SCOPE": "cookie:session"
}
  • We support nested claims using lodash-like dotpath-notation, e.g. by passing user.id to claim.
  • Supported algorithms are: HS256, HS384, and HS512 (symmetric), RS256, RS384, and RS512 (asymmetric), ES256 (asymmetric) as well as PS256, PS384, and PS512 (asymmetric)
  • When using an asymmetric signature, the public key shall be passed as value for secret as shown in the example above.

Limitations with JWT based scopes

  • The authorization header needs to be in the format Bearer {token}, as required by the spec.
  • After pushing the config it takes some time for the changes to actually come into effect globally. This can vary between a couple of seconds and 1-2 minutes. We’re looking into how we can make this faster and/or predictably wait when pushing until the propagation of these changes is complete.
  • The combined size of all secrets / public keys used in JWT scopes must not exceed 6,000 characters.
  • We do not support JSON Web Key Sets (JWKS) at this time. Your configuration will need to refer to the actual public key and not a JWKS URL at this time.
  • If your claim includes dots like e.g. Hasura does with their default configuration, you will need to escape those dots with a backslash, e.g. to cache based on the user ID for the example configuration shared at https://hasura.io/docs/latest/graphql/core/auth/authentication/jwt/#the-spec
"scopes": {
  "AUTHENTICATED": {
    "definition": "header:authorization",
    "jwt": {
        "claim": "https://hasura\.io/jwt/claims.x-hasura-user-id"
    }
  }
}