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
| Situation | Answer |
|---|---|
No Authorization header, or a token that did not verify | 401 unauthenticated problem document |
| Verified, but the permission is absent | 403 insufficient-permission |
| The issuer's key set cannot be read at all | 503 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.