Hasura – Authentication

July 15, 20264 min readUpdated 8/21/2026

Hasura does not log anyone in. It has no user table, no password field and no session store. What it does is verify a token somebody else issued and read a few claims out of it. Getting that division right is most of the work, and misunderstanding it is where the confusion starts.

The division of labour

Your application owns identity: registration, passwords, sessions, password resets, multi-factor. When somebody signs in, your service issues a signed token. From then on Hasura’s only question is who does this token claim to be, and is the signature valid?

In the demo app that means one JWT secret shared between two services. FastAPI signs; Hasura verifies. One login, two APIs, no second session concept.

JWT mode

Set one variable and the engine starts verifying tokens:

HASURA_GRAPHQL_JWT_SECRET: '{"type":"HS256","key":"dev-only-change-me-in-any-real-deployment"}'

HS256 is a shared secret — the same string signs and verifies, so both services must have it. For anything real, prefer RS256 with a public key, or better, point Hasura at your identity provider’s JWKS endpoint so keys rotate without a redeploy:

{"type":"RS256","jwk_url":"https://your-idp.example.com/.well-known/jwks.json"}

The claims Hasura needs

A token is not enough on its own — it has to carry Hasura’s claims, under a namespace key:

{
  "sub": "8f14e45f-ceea-467a-9f0e-b1c8d2c93a10",
  "exp": 1787000000,
  "https://hasura.io/jwt/claims": {
    "x-hasura-default-role": "host",
    "x-hasura-allowed-roles": ["customer", "host"],
    "x-hasura-user-id": "8f14e45f-ceea-467a-9f0e-b1c8d2c93a10",
    "x-hasura-is-host": "true"
  }
}

Three rules, each of which costs somebody an afternoon:

Every claim value must be a string. Including ids and booleans. Hasura compares session variables as text, so a numeric x-hasura-user-id silently fails to match a filter that works everywhere else. Note "true", not true.

allowed_roles is a list because one person is several things. A host is also a guest. The default role applies when the client says nothing; the client can request another with an x-hasura-role header, and Hasura rejects anything not in this list.

The namespace key is exact. https://hasura.io/jwt/claims is the default. It is configurable, but changing it buys nothing and breaks every example you will read.

The role you must not name admin

The demo app calls its staff role staff, and the reason is worth internalising: admin is reserved. It is the role the admin secret grants, it always has unrestricted access, and any attempt to declare a permission for it is rejected with cannot define permission for admin role.

Mint tokens with x-hasura-default-role: admin and you have created users whose permissions can never be written, so every one of their queries is denied. The error names the role but not the fact that the role is special.

A JWT can never carry real admin access either, which is the point: full access requires the secret, not a token.

Webhook mode

The alternative. Instead of verifying a token itself, Hasura forwards the request headers to an endpoint you run, which replies with the session variables:

HASURA_GRAPHQL_AUTH_HOOK: http://auth-service:4000/hasura-auth
{"X-Hasura-User-Id":"8f14e45f-...","X-Hasura-Role":"customer"}

Use it when sessions are opaque — cookies, revocable tokens, anything needing a database lookup to resolve. The cost is a network hop on every request, so cache the response with the Cache-Control header Hasura honours, or the auth service becomes the bottleneck.

Nobody at all

A request with no Authorization header is not automatically rejected. It is assigned a role:

HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous

This is how the demo app serves published listings to visitors. It names a role and grants nothing on its own — permissions are still declared in metadata. Leave it unset and unauthenticated requests are refused outright, which is right for an internal API and wrong for a public site.

The admin secret

HASURA_GRAPHQL_ADMIN_SECRET is not authentication. There is no user behind it. It is a master key that bypasses every rule you write, and it exists for the console, migrations and server-side scripts.

It must never reach a browser. Every frontend in the demo app sends a JWT; grep the source for the secret and you find nothing. If you are tempted to use it in a client “just for now”, you are one deploy away from publishing your database.

Authentication in v3 (DDN)

On the v3 sections. Everything marked v3 (DDN) is taken from the official Hasura DDN documentation as read on 2026-08-21 and was not run locally — it shows configuration, never claimed output. The v2 material was executed against a running engine.

v3 moves this out of environment variables and into metadata, in the globals subgraph:

kind: AuthConfig
version: v3
definition:
  mode:
    jwt:
      claimsConfig:
        namespace:
          claimsFormat: Json
          location: "/claims.jwt.hasura.io"
      key:
        fixed:
          algorithm: HS256
          key:
            valueFromEnv: AUTH_SECRET

Both JWT and webhook modes carry over. Authentication is configured once at supergraph level — every subgraph shares it — while authorization stays per-subgraph. That split is deliberate: one identity, many independently-owned permission sets.

There is no admin secret in v3

The most consequential difference in the whole track. DDN has no HASURA_GRAPHQL_ADMIN_SECRET and no built-in unrestricted role behind it. API access uses a Cloud PAT sent as a cloud_pat header.

Every v2 habit built on that secret needs another answer before you migrate: seeding scripts, console access, CI that applies metadata, the “just use admin” shortcut in a test suite. None of them have a direct translation, and nothing announces this loudly.

Next

Authentication says who you are. Lesson 9 covers what that lets you see — and it is the most important chapter in the track.