Protecting your API
Define an API, request tokens for it and verify them: locally, with live revocation (hybrid) or by introspection.
Your API receives access tokens in the Authorization: Bearer … header and decides whether to accept them. 9in gives you three ways to check them, trading speed for how quickly revocations take effect.
#Define the API
APIs & Scopes → Create API:
- Identifier: a URL-like name, usually the API's base URL (
https://api.acme.com). Apps request tokens for it withaudience=https://api.acme.com, and tokens carry it asaud. - Scopes: what the API lets callers do, such as
orders:readandorders:write. Apps request them inscope; users approve them on the consent screen when the app isn't first-party. - Optional: token format (JWT or opaque), signing key, access token lifetime, and whether refresh tokens (
offline_access) may be issued for it.
#Verify tokens
#Local (fastest)
Check the signature against the tenant's public keys and the claims. No call to 9in per request; the keys are fetched once and cached.
import { verifyAccessToken } from "@9in-io/sdk/server";
const claims = await verifyAccessToken(bearer, {
issuer: "https://acme.9in.io",
audience: "https://api.acme.com",
scope: "orders:read", // every scope listed must be present
});
// claims.sub — the user (or the client ID for machine tokens)
// claims.scope, claims.client_id, claims.sid, claims.jti, claims.expA token that was revoked (sign-out, a blocked user, /oauth/revoke) still passes until it expires. Keep access tokens short-lived (the default is 1 hour) if you rely on this alone.
#Hybrid (recommended)
A local check, then one introspection per token, cached. 9in's token.revoked, session.revoked and user.* webhooks drop the cached answers, so revocations apply within seconds while most requests stay local.
import { createTokenVerifier, verifyWebhook } from "@9in-io/sdk/server";
const verifier = createTokenVerifier({
issuer: "https://acme.9in.io",
audience: "https://api.acme.com",
clientId: process.env.NINE_CLIENT_ID, // any confidential application of the tenant
clientSecret: process.env.NINE_CLIENT_SECRET,
// mode: "hybrid" (default) | "local" | "introspect"
// cacheTtl: 300 seconds an answer is trusted without news from 9in
});
const claims = await verifier.verify(bearer, { scope: "orders:read" });
// your webhook endpoint, subscribed to token.revoked, session.revoked, user.sessions_revoked, user.updated and user.deleted
verifier.handleEvent(await verifyWebhook(process.env.NINE_WEBHOOK_SECRET!, request));The cache lives in memory, per process: with several processes, each needs the webhook events (or keep cacheTtl short).
#Introspection
mode: "introspect" asks 9in about every token (POST <issuer>/oauth/introspect, authenticated as a confidential application). Always current, one network call per request. Opaque tokens are always introspected, and cached in hybrid mode.
#Without the SDK
Any JWT library works. Verify:
- The header:
typisat+jwt, andalgis the algorithm of the key named bykidin<issuer>/.well-known/jwks.json. Don't accept a fixed algorithm list that includes others, and nevernone. - The signature, with that key.
issequals your issuer exactly;audcontains your API identifier;expis in the future (allow a few seconds of clock skew).scope(space-separated) contains what the route needs.
# Python with PyJWT
import jwt
jwks = jwt.PyJWKClient("https://acme.9in.io/.well-known/jwks.json")
key = jwks.get_signing_key_from_jwt(token)
claims = jwt.decode(token, key.key, algorithms=[key.algorithm_name], audience="https://api.acme.com",
issuer="https://acme.9in.io", options={"require": ["exp", "iss", "aud"]})
assert jwt.get_unverified_header(token)["typ"] == "at+jwt"Cache the JWKS, and fetch it again when a token names a kid you don't know (keys rotate).
#Shared-secret (HMAC) tokens
An API can use an HS256/384/512 key instead of a public key. Its secret is shown once in Tokens & Keys; the API verifies with it:
const claims = await verifyAccessToken(bearer, { issuer, audience: "https://api.acme.com", secret: process.env.NINE_API_SECRET });HMAC keys never appear in the JWKS, and a token signed with one is only accepted for the API (or application) that uses that key, so a leaked secret can't mint tokens for other audiences. Prefer asymmetric keys unless your platform requires HMAC.
#Opaque tokens
With Opaque as the API's token format, access tokens are random strings starting with 9in_at_. They carry nothing, can't be verified locally, and are checked with introspection. Use them when tokens must not reveal anything to the client, or when every request should see revocations at once.
#Machines vs users
A client-credentials token has sub equal to client_id and no sid. Check for that when a route is meant for users only.