Table of Contents
PostGraphile JWT/JWK Verification Quickstart
This guide is an adaption of the official quickstart tutorial for Node (Express) provided by Auth0. The code illustrates how to intercept and verify a JWT Access Token via a JWKS (JSON Web Key Set) using Auth0.
Although this code should work, we make no claims as to its validity or fit for production use. We disclaim all liability.
Dependencies
This guide uses the express
HTTP
framework and supporting Node packages authored and maintained by Auth0:
express-jwt
- Middleware that validates a JWT and copies its contents toreq.auth
jwks-rsa
- A library to retrieve RSA public keys from a JWKS (JSON Web Key Set) endpoint
yarn add express express-jwt jwks-rsa
# Or:
npm install --save express express-jwt jwks-rsa
Prior Knowledge & Context
As a developer, the three essential aspects of Auth0 are:
- APIs and Applications
- JWT types (e.g. ID Token vs. Access Token)
- Authentication and Authorization Flows
To keep it simple, in this guide we will be dealing with an Access Token granted by an API which we will need to verify.
Getting Started
You will need two values from your Auth0 configuration: The Auth0 tenant domain name, and the API identifier.
const jwt = require("express-jwt");const jwksRsa = require("jwks-rsa");
// ...
// Authentication middleware. When used, the
// Access Token must exist and be verified against
// the Auth0 JSON Web Key Set.
// On successful verification, the payload of the
// decrypted Access Token is appended to the
// request (`req`) as a `user` parameter.
const checkJwt = jwt({
// Dynamically provide a signing key
// based on the `kid` in the header and
// the signing keys provided by the JWKS endpoint.
secret: jwksRsa.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `https://YOUR_DOMAIN/.well-known/jwks.json`, }),
// Validate the audience and the issuer.
audience: "YOUR_API_IDENTIFIER", issuer: `https://YOUR_DOMAIN/`, algorithms: ["RS256"],
});
(note: if we were processing an ID Token instead of an Access Token, the audience would be the Client ID instead)
Remember that a JWT has
three period-separated sections: header,
payload, and signature. On successful verification, the payload will be
available for us to save inside the PostGraphile request via the
pgSettings
function.
Let's look at an example payload:
{
"iss": "https://YOUR_DOMAIN/",
"sub": "CLIENT_ID@clients",
"aud": "YOUR_API_IDENTIFIER",
"iat": 1555808706,
"exp": 1555895106,
"azp": "CLIENT_ID",
"scope": "read:schema", // scopes a.k.a. permissions "gty": "client-credentials"
}
In this example payload, we can see that the only scope the API has made
available is read:schema
. Our user can perform no mutations, nor can they
perform any queries, they are limited to fetching the schema. Not all tokens
will have such simple payloads, but, in this example, the only meaningful data
is in the scope
value.
Now let's make use of the checkJwt
middleware function:
const express = require("express");
const { postgraphile } = require("postgraphile");
const jwt = require("express-jwt");
const jwksRsa = require("jwks-rsa");
// ...
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"],
});
const app = express();
// Apply checkJwt to our graphql endpointapp.use("/graphql", checkJwt);
app.use(
postgraphile(process.env.DATABASE_URL, process.env.DB_SCHEMA, {
pgSettings: req => { const settings = {}; if (req.auth) { settings["user.permissions"] = req.auth.scopes; } return settings; }, // any other PostGraphile options go here }));
PostGraphile applies everything returned by
pgSettings to the
current session
with set_config($key, $value, true)
. So inside Postgres we can read the
current value of user.permissions
by
select current_setting('user.permissions', true)::text;
.
Basic Error Handling
By default, if there is an error in the JWT verification process, the
express-jwt
package will send a 401 status with an HTML-formatted error
message as a response. Instead, we want to follow the pattern of PostGraphile
and return errors properly formatted in a
GraphQL-compliant
JSON response.
Let's create a basic Express middleware for handling the errors which our
checkJwt
function will throw:
const authErrors = (err, req, res, next) => {
if (err.name === "UnauthorizedError") {
console.log(err); // You will still want to log the error...
// but we don't want to send back internal operation details
// like a stack trace to the client!
res.status(err.status).json({ errors: [{ message: err.message }] });
res.end();
}
};
// Apply error handling to the graphql endpoint
app.use("/graphql", authErrors);
So, now, for example, if someone tries to connect to our GraphQL service without any token at all, we still get a 401 status, but with the appropriate and succinct response:
{
"errors": [
{
"message": "No authorization token was found"
}
]
}
This article was written by BR.