Custom JWT Claims with Ory Kratos

2 July 2024

JSON Web Tokens (JWT) are data structures that allow the holder of the token to assert claims that are able to be cryptographically verified. An example would be that a claim could hold a user claim role like administrator that if proven to be unaltered (using a shared public key) then could be used to provide access to certain administrator functions.

Tools like PostgREST, Hasura and Supabase (a managed PostgREST) rely on JWTs to carry a user’s claims where the role claim is used to set a Postgres role to enforce role-based access control. Tools like Auth0 and Supabase Auth are tools that are commonly used to generate the JWT and can be configured to create tokens that contain the correct claims format for the target system.

Ory Kratos

Ory Kratos is part of a suite of products that provide authentication, identity management and permissions with Kratos focused on the login/authentication side. Kratos supports modern login solutions like Passkey alongside the traditional methods, can be self-hosted, is open-source, supports many databases and can be easily packaged as a Docker container. These benefits mean it is possible to easily start with a SQLite/Litestream instance, for example, and migrate to an Ory managed instance if required.

By default, upon login, a user will be issued an Ory Session object that can be verified by the server (by calling /session/whoami) against the Kratos instance to validate the user and apply authentication and authorization. To convert this Session object into a JWT with the correct claims Kratos provides a not-so-well documented feature:

Converting a Session to JWT

Kratos is configured using a kratos.yml file that contains the configuration for converting the Session to JWT. Hidden away in this pull request is how to configure it. The idea is that /session/whoami can be called with an optional tokenize_as parameter thats allows the caller to specify a target token format.

In the below example tokenize_as could be set to either postgrest or hasura. The three values that must be provided are ttl, which controls the expiry of the token and jwks_url and claims_mapper_url which are described below. Due to the difficulty revoking JWTs it is a good idea to keep the ttl as low as practical.

session:
  whoami:
    tokenizer:
      templates:
        postgrest:
          ttl: 1h
          jwks_url: file:///etc/config/kratos/jwk.eddsa.json
          claims_mapper_url: file:///etc/config/kratos/postgrest.claims.jsonnet
        hasura:
          ttl: 1h
          jwks_url: file:///etc/config/kratos/jwk.eddsa.json
          claims_mapper_url: file:///etc/config/kratos/hasura.claims.jsonnet

jwks_url

To sign the token Kratos must be provided a key as a JSON Web Key. There are many ways to create such a key but an easy one is to download another Ory product, Oathkeeper (binaries are available on github) and run the command:

oathkeeper credentials generate --alg EdDSA

A value like below will be returned which can be saved as /etc/config/kratos/jwk.eddsa.json and accessed via the Kratos instance.

The JWK can easily to passed into PostgREST via environment variable PGRST_JWT_SECRET or Hasura via HASURA_GRAPHQL_JWT_SECRET to allow them to verify the JWT.

{
  "keys": [
    {
      "use": "sig",
      "kty": "OKP",
      "kid": "10dd524d-decb-48ff-b84d-7d389c10f2f7",
      "crv": "Ed25519",
      "alg": "EdDSA",
      "x": "58zYrqRbmsXkyg2t50S9_RoZQb2cVXnSfxUU4aPhDAk",
      "d": "1F_Thkzgc43ZiI_qqnf1xuNHqN6EPJYgpX5NXm15bb0"
    }
  ]
}

claims_mapper_url

The claims mapper is where a custom configuration can be defined using the Jsonnet configuration language. The claims for a Hasura endpoint can be configured like:

local session = std.extVar('session');

{
  "claims": {
    "https://hasura.io/jwt/claims": {
      "x-hasura-default-role": "user",
      "x-hasura-allowed-roles": ["user"],
      "x-hasura-user-id": session.identity.id,
    }
  }
}

The much simplier configuration for PostgREST may look like:

local session = std.extVar('session');

{
  "claims": {
    "role": "user",
    "userId": session.identity.id
  }
}
If you find an error please raise a pull request.