DocsAuthentication

Authentication

Secure your API with JWT authentication and PostgreSQL Row-Level Security.

JWT Authentication

Postrust validates JWT tokens and extracts role claims to determine database access.

JWT Payload
{
  "role": "authenticated_user",
  "sub": "user_123",
  "email": "user@example.com",
  "exp": 1704067200
}
Request with JWT
curl http://localhost:3000/users \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

How the claims are checked

A claim that is absent is not a claim that is wrong: a token carrying no exp does not expire, and one carrying no aud is for anybody. Only a claim that is present and is not the kind of thing that claim is gets rejected.

  • expChecked to the second. Forgiving an expiry keeps a session alive past the moment its issuer said it ended, so no slack is given here.
  • nbf, iatThirty seconds of clock skew allowed. These describe a token that is not valid yet, and a client whose clock runs slightly fast mints one through nobody's fault; refusing it makes a working deployment fail intermittently.
  • roleMust be a string. A role claim of any other shape, or "" or none, names no role and falls back to the anonymous role, rather than asking PostgreSQL for a role of that spelling.
  • issNot looked at unless PGRST_JWT_ISS is set. Then it must be present and equal it exactly: where an identity provider’s keys are shared across tenants, the issuer is what tells one tenant’s tokens from another’s.

A 401 names which refusal in its WWW-Authenticate challenge, so a client can tell “rotate your key” from “fix your clock” without reading the body.

Signing keys

PGRST_JWT_SECRET is read as PostgREST reads jwt-secret. A plain string is an HMAC secret and admits HS256 tokens. A JSON Web Key, or a JSON Web Key Set, is a set of public keys: how an identity provider’s keys are given.

Environment
# A shared HMAC secret (HS256)
PGRST_JWT_SECRET="your-secret-key-at-least-32-characters"

# A JWKS, as an identity provider publishes it
PGRST_JWT_SECRET='{"keys":[{"kid":"2024-07","kty":"RSA","alg":"RS256","n":"0vx7...","e":"AQAB"}]}'
ktyAlgorithms
RSARS256/384/512, PS256/384/512 (modulus of at least 2048 bits)
ECES256 (P-256), ES384 (P-384)
OKPEdDSA (Ed25519)
octHS256/384/512

The algorithm comes from the key, never from the token, so neitheralg: none nor an HS256 token “signed” with an RSA public key gets through. A token with a kid is checked against the key with that kid only; one without is tried against every key that admits its algorithm. Keys that cannot verify (encryption keys, unsupported curves) are skipped with a warning; a malformed key stops the server from starting.

Keys from an identity provider

Give PGRST_JWT_JWKS_URL the provider’s jwks_uri and Postrust fetches the key set from there and keeps it current, so a key the provider rotates in is picked up without a restart and a key it withdraws stops being accepted.

Environment
# Supabase Auth, on asymmetric signing keys
PGRST_JWT_JWKS_URL="https://<project>.supabase.co/auth/v1/.well-known/jwks.json"
# Auth0
PGRST_JWT_JWKS_URL="https://<tenant>.auth0.com/.well-known/jwks.json"
# Keycloak
PGRST_JWT_JWKS_URL="https://<host>/realms/<realm>/protocol/openid-connect/certs"

# Optional: only accept tokens this issuer minted
PGRST_JWT_ISS="https://<tenant>.auth0.com/"
  • Fetched at start-up, then again after the response’s Cache-Control: max-age(held to between 60 seconds and a day), or every PGRST_JWT_JWKS_REFRESH seconds (default 300).
  • A token naming a kid the set does not hold triggers one more fetch first — a key just rotated in — at most once every 30 seconds.
  • https only (http just for loopback), redirects only to the same host, 5 seconds and 1 MiB per fetch.
  • Every change of the set empties the token cache, so a withdrawn key’s tokens are refused from the next request on.
  • If the provider is down, the last set that loaded is kept. If none ever loaded, tokens are refused (fail closed) and /_/ready answers 503 with"jwks": false.
  • With PGRST_JWT_SECRET set as well, a token either one verifies is accepted: a backend’s service tokens beside the provider’s user tokens.

Configuration

PGRST_JWT_SECRET

An HMAC secret (HS256), or a JWK or JWKS of public keys (RS, PS, ES, EdDSA)

PGRST_JWT_SECRET_IS_BASE64

Set true if secret is base64 encoded

PGRST_JWT_JWKS_URL

Fetch the keys from an identity provider's JWKS URL and keep them current

PGRST_JWT_JWKS_REFRESH

Seconds between fetches when the response has no max-age (default: 300)

PGRST_JWT_AUD

Required audience claim (optional)

PGRST_JWT_ISS

Required issuer: a token's iss must equal it, and must be present (optional)

PGRST_JWT_ROLE_CLAIM_KEY

Claim key containing role (default: role)

Row-Level Security

PostgreSQL RLS policies are enforced on every request. JWT claims are available as session variables.

SQL - RLS Policy
-- Enable RLS on table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy: users can only see their own orders
CREATE POLICY user_orders ON orders
  FOR ALL
  USING (
    user_id = current_setting('request.jwt.claims')::json->>'sub'
  );

-- Policy: admins can see all orders
CREATE POLICY admin_orders ON orders
  FOR ALL
  USING (
    current_setting('request.jwt.claims')::json->>'role' = 'admin'
  );

Accessing JWT Claims in SQL

-- Get full claims object
current_setting('request.jwt.claims')::json

-- Get specific claim
current_setting('request.jwt.claims')::json->>'sub'
current_setting('request.jwt.claims')::json->>'email'
current_setting('request.jwt.claims')::json->>'role'

-- Use in function
CREATE FUNCTION get_current_user_id() RETURNS TEXT AS $$
  SELECT current_setting('request.jwt.claims')::json->>'sub';
$$ LANGUAGE SQL STABLE;

The GraphQL surface

Everything above is how the REST surface authenticates. The GraphQL surface at /v1/graphql reads Hasura’s contract instead — an admin secret, x-hasura-* session variables, and a role that decides which schema answers before it decides anything about a row.

The two meet at the token: a verified JWT names the role either way, and on the GraphQL side an X-Hasura-Role header may select any role the token’s x-hasura-allowed-roles lists. See Configuration for the settings and GraphQL for the surface itself.

Either way the verified claims reach the database, so one RLS policy serves both: request.jwt.claims is set in every GraphQL transaction as it is in every REST one. That includes subscriptions. A WebSocket is authenticated once, from the connection_init payload — {"headers": {"Authorization": "Bearer <token>"}}, since a browser cannot set headers on a WebSocket — and every live query on it re-reads as that identity.