Skip to main content
Version: Draft

PostGraphile JWT/JWK Verification Quickstart

This guide adapts Auth0’s Node (Express) quickstart so that the verified JWT payload is forwarded to PostgreSQL through pgSettings. It assumes you are running PostGraphile using the Express adaptor (postgraphile/grafserv/express/v4), but similar will apply for alternative servers.

Terms:

Processing a JWT is a middleware concern

Your web framework middleware will handle verification, refresh tokens, revocation lists, and other security concerns - the logic here is not specific to PostGraphile. Once you have a set of trusted claims you can expose them to PostgreSQL through PostGraphile's pgSettings function. See the JWT guide for the bigger picture and the PostgreSQL JWT specification for how claims map onto PostgreSQL session settings.

Dependencies

This walkthrough uses Express and the Auth0-maintained helpers:

Install them with the package manager of your choice:

npm install --save express express-jwt jwks-rsa

Configure express-jwt

express-jwt verifies the Bearer token and populates req.auth with the JWT payload. Configure it with your Auth0 details and JWKS endpoint:

auth0.ts
import jwt from "express-jwt";
import jwksRsa from "jwks-rsa";

export const checkJwt = jwt({
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://YOUR_DOMAIN/.well-known/jwks.json`,
}),
audience: "YOUR_API_IDENTIFIER",
issuer: `https://YOUR_DOMAIN/`,
algorithms: ["RS256"],
});

Surface trusted claims to PostgreSQL

Inside preset.grafast.context copy whichever details you need into pgSettings. Only forward the fields your policies rely on.

graphile.config.ts
import { PostGraphileAmberPreset } from "postgraphile/presets/amber";

const preset: GraphileConfig.Preset = {
extends: [PostGraphileAmberPreset],

grafast: {
async context(requestContext, args) {
const req = requestContext.expressv4?.req;
const pgSettings = {
...args.contextValue?.pgSettings,
} as Record<string, string>;

const claims = req?.auth;
if (claims && typeof claims === "object") {
if (typeof claims.scope === "string") {
pgSettings["jwt.claims.scope"] = claims.scope;
}
if (typeof claims.sub === "string") {
pgSettings["jwt.claims.sub"] = claims.sub;
}
if (Array.isArray(claims.permissions)) {
pgSettings["jwt.claims.permissions"] = claims.permissions.join(" ");
}
}

return {
...args.contextValue,
pgSettings,
};
},
},
};

export default preset;

You can use alternative Postgres settings names, but ensure that you include at least one and at most two . characters, and that the text before the first . is not a scope reserved by PostgreSQL. Historically no-one has raised an issue with the jwt.claims prefix, so it seems to be a good choice.

Wire Express to Grafserv

Use the Auth0 middleware before adding PostGraphile to your express app.

server.ts
import express from "express";
import { createServer } from "node:http";
import { grafserv } from "postgraphile/grafserv/express/v4";
import { postgraphile } from "postgraphile";

import preset from "./graphile.config.ts";
import { checkJwt } from "./auth0.ts";
import { authErrors } from "./auth-errors.ts";

const app = express();
const server = createServer(app);
server.on("error", (e) => void console.error(e));
app.use("/graphql", checkJwt, authErrors);

const pgl = postgraphile(preset);
const serv = pgl.createServ(grafserv);
serv
.addTo(app, server)
.then(() => {
const port = preset.grafserv?.port ?? 5678;
server.listen(port);
console.log(`GraphQL running at http://localhost:${port}/graphql`);
})
.catch((e) => {
console.error(e);
process.exit(1);
});

Error handling

By default express-jwt sends HTML-formatted error pages. To keep errors inside the GraphQL response surface the UnauthorizedError as JSON:

auth-errors.ts
import type { Request, Response, NextFunction } from "express";

export function authErrors(
err: any,
_req: Request,
res: Response,
next: NextFunction,
) {
if (err?.name === "UnauthorizedError") {
res.status(err.status ?? 401).json({ errors: [{ message: err.message }] });
return;
}
next(err);
}

Using the claims in PostgreSQL

Anything placed in pgSettings is available inside PostgreSQL via current_setting(...). For example:

create function current_subject() returns text as $$
select nullif(current_setting('jwt.claims.sub', true), '');
$$ language sql stable;

You can reference the helper inside Row Level Security policies or use it in functions. Adjust the key names to match whatever you added to pgSettings.

Once everything is wired up, PostGraphile simply consumes the claims you provide and enforces your database policies accordingly.


This article was originally written by BR, but has since been heavily modified and updated.