Skip to main content

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.

Timeouts

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.

New to JWKS?

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

ClaimValueDescription
kid (header)stringIdentifies 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.
substringThe organisation the token acts for. Scopes every response — a token only ever sees that organisation's tours.
issstringMust match the issuer you registered with us, and is where we look for /.well-known/jwks.json.
audstringThe Noughty Tours API origin you are calling.
iat / expnumberIssued-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.

Testing locally

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

This step is manual, and nothing works until it is done

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:

ValueExampleNotes
Issuerhttps://your-backend.comYour backend's origin. Must match the iss claim on every token you mint, exactly — including scheme, and with no trailing slash.
JWKS endpointhttps://your-backend.com/.well-known/jwks.jsonWhere 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.

Sign the right aud

The 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

Authenticate your token endpoint

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.
A runnable example

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.