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: anonymousThis 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_SECRETBoth 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.