Authentication
Requests are authenticated with a bearer token that your own backend mints. The SDK never sees your signing key — only the finished token.
getBearerToken() is awaited immediately before each request, and its result is
sent as Authorization: Bearer <token>. Because it runs per call, it is the
natural place to refresh an expired token. Cache the token in your own code if
you do not want a round trip per call.
Requests abort after 15 seconds and reject with an ApiError whose code is
TIMEOUT and statusCode is 408.
You sign tokens with your own private key and publish the public half as a JWK Set. We verify a token by fetching that key set from your server, so the only thing you register with us is a URL — no shared secret, and nothing to rotate on our side when you rotate yours.
The SDK drives the browser half for you: getBearerToken() is where you call
your own token endpoint. The rest of this page is the backend half.
What Are JSON Web Key Sets (JWKS) and How Do They Work?
is a good primer on the mechanics this page assumes — how kid selects a key,
how rotation works, and how a verifier fetches a key set.
Minting a token
Any JWT library will do; the examples use jose.
Sign with ES256 and put the key's kid in the header — that is what lets us
pick the right key out of your JWKS.
import { SignJWT } from "jose";
const TTL_SECONDS = 900;
/**
* Signs a short-lived token for one organisation.
* `key` is your ES256 private key; `kid` identifies it within your JWKS.
*/
export async function mintToken({ key, kid, organisationId }) {
return new SignJWT({})
.setProtectedHeader({ alg: "ES256", kid, typ: "JWT" })
.setSubject(organisationId)
.setIssuer("https://your-backend.com")
.setAudience("https://api.dev.sdk.thenoughtyfox.com")
.setIssuedAt()
.setExpirationTime(Math.floor(Date.now() / 1000) + TTL_SECONDS)
.sign(key);
}
Expose it on an endpoint the browser can reach, behind your own session check:
app.post("/api/auth-token", requireSession, async (req, res) => {
const { key, kid } = await getSigningKey();
const token = await mintToken({
key,
kid,
// Taken from the authenticated session, never from the request body.
organisationId: req.session.organisationId
});
res.json({
access_token: token,
token_type: "Bearer",
expires_in: TTL_SECONDS
});
});
Claims we verify
| Claim | Value | Description |
|---|---|---|
kid (header) | string | Identifies the signing key within your JWKS. Required — without it we cannot tell which key to verify against. |
alg (header) | "ES256" | Asymmetric algorithms only. HS256 and other shared-secret algorithms are rejected. |
sub | string | The organisation the token acts for. Scopes every response — a token only ever sees that organisation's tours. |
iss | string | Must match the issuer you registered with us, and is where we look for /.well-known/jwks.json. |
aud | string | The Noughty Tours API origin you are calling. |
iat / exp | number | Issued-at and expiry, in seconds. Keep the window short — it is the only revocation you have. |
Publishing your JWKS
Serve the public half of every key that might still have live tokens signed
against it, at /.well-known/jwks.json on your issuer origin. It must be
reachable from the public internet — we fetch it server-side, so localhost
will not resolve.
import { exportJWK, calculateJwkThumbprint } from "jose";
app.get("/.well-known/jwks.json", async (_req, res) => {
const jwk = await exportJWK(publicKey);
// A thumbprint makes a stable `kid` that is derived from the key itself.
const kid = await calculateJwkThumbprint(jwk);
res.set("Cache-Control", "public, max-age=300");
res.json({ keys: [{ ...jwk, kid, alg: "ES256", use: "sig" }] });
});
To rotate without downtime, serve both keys, start signing with the new kid,
and drop the old key once every token signed with it has expired. We refetch your
key set as soon as we see a kid we do not recognise, so no coordination is
needed.
We fetch your JWKS server-side, so a local backend needs a tunnel. Run something
like ngrok http 5050, set your iss to the tunnel URL, and confirm it serves:
curl https://<your-tunnel>/.well-known/jwks.json.
Registering your issuer
We only fetch key sets from issuers we know about, so a correctly signed token from an unregistered issuer is still rejected.
Send the Noughty Tours team two values once your key set is live:
| Value | Example | Notes |
|---|---|---|
| Issuer | https://your-backend.com | Your backend's origin. Must match the iss claim on every token you mint, exactly — including scheme, and with no trailing slash. |
| JWKS endpoint | https://your-backend.com/.well-known/jwks.json | Where we fetch your public keys. Reachable from the public internet, over HTTPS. |
Tell us before you change either value. Rotating a key needs no coordination —
that is what kid is for — but moving your issuer or JWKS URL does, and every
token breaks until we have the new one.
audThe audience you sign into a token has to be the Noughty Tours API origin the
SDK calls, https://api.dev.sdk.thenoughtyfox.com. It is set in your backend
code, nowhere near the frontend that consumes it, so it is easy to get wrong —
and every token is rejected when it is.
Before you go live
Whoever can call it can mint a token, and the token is what scopes our responses.
Put your existing session or API-key check in front of it, and read sub from
that session — never from a request parameter.
- Persist the signing key in a KMS or secrets manager. Generating one per boot invalidates every token already in flight.
- Cache the token in your frontend until it nears expiry.
getBearerToken()runs before every SDK call, so an uncached implementation costs a round trip to your backend each time. - Keep the TTL short — minutes, not days.
- Serve your JWKS over HTTPS with a sane cache header, and make sure it stays up. If we cannot fetch it, we cannot verify your tokens.
noughty-tours-web-backend-demo is a small NestJS service that does exactly the above — mints tokens and publishes a JWK Set. It pairs with noughty-tours-web-demo on the frontend, so the two together are a complete working integration.