Last updated: November 2026
Hasura authorizes GraphQL requests using a JWT that must contain a special https://hasura.io/jwt/claims namespace with x-hasura-* keys, at minimum x-hasura-default-role, x-hasura-allowed-roles, and x-hasura-user-id. With Keycloak you configure Hasura’s JWT mode to trust your realm’s JWKS endpoint, then bridge the gap between Keycloak’s flat claim model and Hasura’s nested claims namespace. The cleanest approach is Hasura’s claims_map feature, which lets Hasura read standard Keycloak token fields directly and avoids the fragility of Keycloak script mappers.
Combining Keycloak with Hasura puts a production-grade identity layer in front of an auto-generated GraphQL API. Keycloak handles authentication, token issuance, and role management; Hasura enforces row-level permissions in PostgreSQL using the identity information inside each JWT. The details of how Hasura reads JWT claims are easy to get wrong, this guide covers both configuration options with enough detail to avoid the most common pitfalls.
How Hasura JWT Mode Works
Hasura’s JWT authentication mode is enabled by setting the HASURA_GRAPHQL_JWT_SECRET environment variable to a JSON object that tells Hasura how to validate incoming bearer tokens. The minimum viable configuration points at your Keycloak realm’s JWKS (JSON Web Key Set) endpoint:
{
"type": "RS256",
"jwk_url": "https://auth.example.com/realms/myrealm/protocol/openid-connect/certs"
}
Replace auth.example.com with your Keycloak host and myrealm with your realm name. In Keycloak 26.x the path is always /realms/{realm}/protocol/openid-connect/certs, the legacy /auth/realms/ prefix is no longer used.
When a client sends a GraphQL request, it includes a bearer token in the Authorization header:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Hasura fetches the public key from the JWKS URL, validates the token signature and expiry, then reads the authorization claims from the token payload. Those claims must tell Hasura which roles the user is allowed to assume and which role to use by default.
The x-hasura-* Claims Requirement
Hasura expects the following claims inside the JWT payload:
| Claim | Required | Description |
|---|---|---|
x-hasura-default-role |
Yes | The role Hasura uses when the client does not specify one |
x-hasura-allowed-roles |
Yes | Array of all roles the user may request (must include the default role) |
x-hasura-user-id |
Recommended | Uniquely identifies the user; used in row-level permission rules |
x-hasura-org-id |
Optional | Useful for multi-tenant row filtering |
x-hasura-* (custom) |
Optional | Any additional session variable for permission rules |
By default, Hasura looks for all of these inside a single nested object keyed by the namespace https://hasura.io/jwt/claims:
{
"sub": "9f43a8b2-1234-5678-abcd-ef0123456789",
"iat": 1748000000,
"exp": 1748003600,
"https://hasura.io/jwt/claims": {
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": ["user", "editor"],
"x-hasura-user-id": "9f43a8b2-1234-5678-abcd-ef0123456789"
}
}
Keycloak does not emit this structure out of the box. Its protocol mappers produce flat, top-level claims or simple string/array values, not a nested object under a long URL-shaped key. There are two ways to bridge this gap.
Option A: Keycloak Protocol Mapper That Emits the Nested Object
One approach is to configure Keycloak to put the entire https://hasura.io/jwt/claims object into the token directly. This requires a mapper that can emit a JSON object as a claim value. Keycloak 26.x ships with a “Hardcoded claim” mapper that sets a fixed string, and it also supports Script mappers (disabled by default since Keycloak 18 for security reasons and re-enabled only by adding -Dkeycloak.profile.feature.scripts=enabled to the server arguments or enabling the feature in keycloak.conf).
With scripts enabled, you can write a JavaScript mapper that inspects the user’s realm roles and builds the nested object:
Admin Console path: Clients > {your-client} > Client scopes > {client-name}-dedicated > Add mapper > By configuration > Script Mapper
// Script Mapper — evaluate on token generation
var roles = user.getRoleMappings()
.getRealmMappingsStream()
.map(function(r) { return r.getName(); })
.collect(java.util.stream.Collectors.toList());
var hasuraRoles = roles.filter(function(r) {
return r.startsWith("hasura-");
}).map(function(r) {
return r.replace("hasura-", "");
});
var defaultRole = hasuraRoles.length > 0 ? hasuraRoles[0] : "user";
token.setOtherClaims("https://hasura.io/jwt/claims", {
"x-hasura-default-role": defaultRole,
"x-hasura-allowed-roles": hasuraRoles.length > 0 ? hasuraRoles : ["user"],
"x-hasura-user-id": user.getId()
});
This works but has real downsides: script mappers require a server feature flag, they are harder to audit, and they depend on role-naming conventions. If you can avoid script mappers, you should.
Option B: Hasura claims_map (Recommended)
The cleaner approach is to leave Keycloak tokens as they are, with standard flat claims, and configure Hasura to read those flat claims and map them to x-hasura-* session variables. This is done through the claims_map key in HASURA_GRAPHQL_JWT_SECRET.
This is the recommended path because:
- No Keycloak script mapper required, script mappers need a non-default feature flag.
- Keycloak protocol mappers emit realm roles as a flat array at
realm_access.roles, a well-documented, stable path. - The
subclaim (user ID) is always present in standard OIDC tokens. - The mapping is fully version-controlled inside your Hasura configuration, not inside Keycloak.
Step 1: Configure the Keycloak Client to Emit Roles
Keycloak emits realm roles inside the token under realm_access.roles by default, but only when the “realm roles” mapper is active on the client scope. Verify this in the Admin Console:
Admin Console path: Clients > {your-client} > Client scopes > {client-name}-dedicated > Mappers
Look for a mapper named “realm roles” with mapper type “User Realm Role”. If it is absent, add it:
- Click Add mapper > By configuration
- Select User Realm Role
- Set Token Claim Name to
realm_access.roles - Set Claim JSON Type to
String - Enable Add to access token and Multivalued
- Save
After this mapper is active, an access token for a user who has the realm role editor will contain:
{
"sub": "9f43a8b2-1234-5678-abcd-ef0123456789",
"realm_access": {
"roles": ["default-roles-myrealm", "editor", "offline_access", "uma_authorization"]
}
}
You can decode a live token using the JWT Token Analyzer to confirm the roles are appearing correctly before moving to the Hasura side.
For a deeper look at how Keycloak role mappers and custom attributes interact, see using custom user attributes in Keycloak OIDC tokens.
Step 2: Configure Hasura claims_map
Set HASURA_GRAPHQL_JWT_SECRET to a JSON object that uses claims_map to translate Keycloak’s standard claims into x-hasura-* session variables:
{
"type": "RS256",
"jwk_url": "https://auth.example.com/realms/myrealm/protocol/openid-connect/certs",
"claims_map": {
"x-hasura-allowed-roles": {
"path": "$.realm_access.roles"
},
"x-hasura-default-role": {
"path": "$.realm_access.roles[0]",
"default": "user"
},
"x-hasura-user-id": {
"path": "$.sub"
}
}
}
The path values use JSONPath syntax. $.realm_access.roles extracts the full roles array and maps it to x-hasura-allowed-roles. $.realm_access.roles[0] takes the first role as the default, you may want to be more deliberate here (see the role ordering note below). $.sub extracts the OIDC subject identifier and maps it to x-hasura-user-id.
When claims_map is present, Hasura does not look for the https://hasura.io/jwt/claims namespace at all. The two configurations are mutually exclusive.
Role Ordering Consideration
Using $.realm_access.roles[0] as the default role is convenient but fragile: Keycloak does not guarantee the order of roles in the array, and default realm roles (default-roles-myrealm, offline_access, uma_authorization) will often appear before your application roles.
A safer approach is to define a dedicated Keycloak role, for example hasura-default, and map only that role to x-hasura-default-role using a hardcoded mapper or an additional mapper that filters to a single role. Alternatively, use the default key in claims_map to set a fallback:
"x-hasura-default-role": {
"path": "$.app_default_role",
"default": "user"
}
Then add a simple Keycloak “Hardcoded claim” mapper on the client that sets app_default_role to the string user for all users, and override it with a user attribute for elevated roles. This is covered in detail in the guide to client scopes vs roles in Keycloak.
Mapping Keycloak Roles to Hasura Roles
With claims_map active, every Keycloak realm role that appears in realm_access.roles becomes a Hasura allowed role. This means Keycloak realm roles and Hasura roles share the same namespace. There are two reasonable conventions:
Convention 1, Direct mapping: Name Keycloak realm roles to match your Hasura role names exactly. If Hasura has roles user, editor, and admin, create those exact roles in Keycloak and assign them to users. The token will contain ["user", "editor"] and Hasura will read them directly.
Convention 2, Prefixed Keycloak roles: Prefix Keycloak roles with app- or a similar namespace to avoid collisions with other applications sharing the same realm. Then add a Keycloak mapper or a Hasura claims_map path transform to strip the prefix. Currently Hasura’s JSONPath support does not include string transforms, so for this convention you need to pre-process the claim on the Keycloak side using a script mapper or a dedicated client scope.
Convention 1 is simpler and works well when the Keycloak realm is dedicated to a single application or tightly coupled set of applications. For shared realms, see the discussion of realm isolation in fine-grained authorization in Keycloak explained.
Role Assignment in Keycloak Admin Console
To assign a realm role to a user:
- Go to Users > {user} > Role mapping
- Click Assign role
- Filter by Filter by realm roles
- Select the role (e.g.,
editor) and click Assign
To assign a realm role to all users by default, add it to the default-roles-{realm} composite role:
- Go to Realm settings > User registration > Default roles
- Click Assign role and select the role
Somewhere to run this that stays patched
Everything above works the same on Skycloak, because it is real upstream Keycloak rather than a fork. What changes is who handles the upgrades, backups and security patches afterwards.
Using x-hasura-user-id in Row-Level Permissions
Once x-hasura-user-id is populated from the sub claim, you can use it in Hasura table permissions to enforce row-level isolation. In the Hasura Console:
- Go to Data > {schema} > {table} > Permissions
- Select the role (e.g.,
user) and click the row insert/select/update/delete cell - Set Row select permissions to a custom check:
{
"owner_id": {
"_eq": "X-Hasura-User-Id"
}
}
This ensures users can only query rows where owner_id matches their Keycloak sub value. The session variable X-Hasura-User-Id is automatically populated from the JWT claim mapped by claims_map.
For multi-tenant scenarios where you need to filter by organization, add an x-hasura-org-id mapping to your claims_map pointing at a custom Keycloak user attribute:
"x-hasura-org-id": {
"path": "$.org_id",
"default": ""
}
Then configure a Keycloak “User Attribute” mapper to emit org_id from the user’s profile. The guide on using custom user attributes in Keycloak OIDC tokens covers the mapper configuration in detail.
Testing the Integration End to End
Decode the Token
Before wiring up Hasura, confirm the token contains the expected claims. Obtain a token from Keycloak using the Resource Owner Password Credentials grant (acceptable in test environments):
curl -s -X POST \
"https://auth.example.com/realms/myrealm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=hasura-client" \
-d "username=testuser" \
-d "password=testpass" \
| jq -r '.access_token'
Paste the resulting token into the JWT Token Analyzer to inspect the payload. Confirm that realm_access.roles contains your expected application roles and that sub is a non-empty UUID.
Send an Authenticated GraphQL Request
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -s -X POST \
"https://hasura.example.com/v1/graphql" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query": "{ __typename }"}'
A successful response returns {"data":{"__typename":"query_root"}}. If you see invalid-jwt or jwt-missing-role-claims, check whether claims_map is correctly set and whether the JWKS URL is reachable from the Hasura container.
Validate Token Verification for APIs
The same JWKS-based validation pattern that Hasura uses applies to any service in your stack that needs to verify Keycloak tokens. See Keycloak token validation for APIs for implementation patterns in Node.js, Go, and Python.
GraphQL and Keycloak Architecture Patterns
Hasura with Keycloak is a common stack for building GraphQL backends with minimal custom authorization code. If you are also using Apollo Client or Apollo Server in your frontend or BFF layer, the integration patterns described in Keycloak GraphQL Apollo authentication apply directly, specifically the pattern of injecting the bearer token into the Apollo authLink and handling token refresh before expiry.
Frequently Asked Questions
How do I use Keycloak JWTs with Hasura?
Set the HASURA_GRAPHQL_JWT_SECRET environment variable to a JSON object with jwk_url pointing at your Keycloak realm’s JWKS endpoint (/realms/{realm}/protocol/openid-connect/certs). Hasura will validate the token signature automatically on every request. You also need either the https://hasura.io/jwt/claims namespace in the token or a claims_map in the JWT secret config to tell Hasura where to find role and user-ID information.
How do I add x-hasura claims in Keycloak?
The recommended approach is to use Hasura’s claims_map rather than modifying the Keycloak token. Add claims_map to HASURA_GRAPHQL_JWT_SECRET and map x-hasura-allowed-roles to $.realm_access.roles, x-hasura-default-role to a dedicated flat claim, and x-hasura-user-id to $.sub. If you must embed the nested https://hasura.io/jwt/claims object in the token itself, use a Keycloak Script mapper, but note that requires enabling the scripts feature in keycloak.conf.
How do I map Keycloak roles to Hasura roles?
Assign realm roles to users in Users > {user} > Role mapping in the Keycloak Admin Console. With claims_map, every role in realm_access.roles becomes an entry in x-hasura-allowed-roles. Name your Keycloak realm roles to match your Hasura role names exactly (user, editor, admin) and they map through without transformation. System roles (offline_access, uma_authorization) also appear in the array, filter them out with a dedicated Keycloak client scope or ensure Hasura’s default permissions deny unknown roles.
How do I filter out Keycloak system roles from x-hasura-allowed-roles?
Create a dedicated Keycloak client scope for Hasura with a “User Realm Role” mapper scoped to an allow-list of application roles, then attach that scope only to the Hasura client. This prevents offline_access and uma_authorization from appearing. Alternatively, leave them in place and configure Hasura so unrecognized roles have no table permissions.
How do I use x-hasura-user-id in row-level permissions?
Open the Permissions tab for a table in the Hasura Console and set a row select check for the user role: {"owner_id": {"_eq": "X-Hasura-User-Id"}}. The owner_id column must store the Keycloak sub UUID when rows are created. Add a column preset on insert (owner_id = X-Hasura-User-Id) so Hasura fills it automatically, preventing users from spoofing each other’s records.
Conclusion
Securing Hasura with Keycloak requires two things: pointing Hasura at your realm’s JWKS endpoint and bridging the x-hasura-* claims it expects. The claims_map approach, mapping $.realm_access.roles to x-hasura-allowed-roles and $.sub to x-hasura-user-id, is simpler and more maintainable than script mappers. Once in place, Keycloak roles drive Hasura table permissions with no custom authorization middleware.
If you are evaluating a managed Keycloak setup to simplify this kind of integration, Skycloak provides a production-ready Keycloak cluster with JWKS endpoints ready to use, removing the operational overhead of running Keycloak yourself.

