Skip to content

Express

Verifying the access token on your own API — middleware, permissions, and the three answers a refusal can have.

The other half of every page in this section: the browser has a token, and your API has to decide what it means. Verification is offline — the token is a signed JWT, your API checks it against your issuer's published key set, and nothing is asked of us per request.

npm install @kleora-io/node

One runtime dependency, a JOSE library. Express and Fastify are optional peers, so the package installs with neither.

1. Authenticate, then authorise

import express from 'express'
import { express as kleora, requirePermission } from '@kleora-io/node'

const app = express()

app.use(
  kleora({
    issuer: process.env.KLEORA_ISSUER!,
    audience: 'https://api.example.com',
  }),
)

app.get('/invoices', requirePermission('invoices:read'), (req, res) => {
  res.json({ tenant: req.kleora.tenant })
})

The middleware verifies the token and sets req.kleora to its claims. requirePermission reads req.kleora.permissions — which is in the token already, so the check is a list lookup rather than a call to us.

2. What to pass as audience

The aud claim of an access token is [client_id], or [client_id, api_audience] once you set an API audience on the environment. A fresh environment has none, so pass the client id until you do, and your own API identifier afterwards.

3. The three answers a refusal can have

SituationAnswer
No Authorization header, or a token that did not verify401 unauthenticated problem document
Verified, but the permission is absent403 insufficient-permission
The issuer's key set cannot be read at all503 issuer-unavailable

The 401 body says only that the request was not authenticated. The reason — expired, wrong audience, bad signature — goes to the logger option, which is where it is useful to you and of no use to somebody probing. The 503 matters more than it looks: without it, our own outage would tell every one of your callers that their token is bad and send them back to an issuer that is already down.

Anything else is your service being misconfigured, and the package does not choose a status for it: Express gets next(error) and your own error handler answers.

requirePermission answers 401 rather than 403 when req.kleora is absent entirely — a route that forgot the authentication middleware is not a permissions problem.

4. What the verifier actually checks

The typ header is at+jwt, the ES256 signature against the issuer's key set, iss exactly, aud containing your audience, exp and iat within 60 seconds of tolerance, and that the token's env claim agrees with the issuer you configured — so a sandbox token cannot quietly satisfy a production API.

The key set is fetched once per issuer and cached for as long as its Cache-Control says. A kid the cache has not seen triggers one refetch, rate limited to one a minute; a fetch that fails serves the cached set until twice its TTL and then fails closed. Signing keys are published before they are used, so a rotation mid-flight finds the kid already cached.

Fastify, and everything else

fastify({ issuer, audience }) is the same plugin, decorating request.kleora and giving the same three answers. It is registered un-encapsulated, because registering an authentication plugin at the root never means "only the routes inside this plugin".

For a Python API, kleora does the same verification with the same rules — Installing an SDK has it.