# Protecting your API

> Define an API, request tokens for it and verify them: locally, with live revocation (hybrid) or by introspection.

Source: https://9in.io/docs/protecting-apis/

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 with `audience=https://api.acme.com`, and tokens carry it as `aud`.
- **Scopes:** what the API lets callers do, such as `orders:read` and `orders:write`. Apps request them in `scope`; 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.

```ts
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.exp
```

A 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](https://9in.io/docs/webhooks/) drop the cached answers, so revocations apply within seconds while most requests stay local.

```ts
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:

1. The header: `typ` is `at+jwt`, and `alg` is the algorithm of the key named by `kid` in `<issuer>/.well-known/jwks.json`. Don't accept a fixed algorithm list that includes others, and never `none`.
2. The signature, with that key.
3. `iss` equals your issuer exactly; `aud` contains your API identifier; `exp` is in the future (allow a few seconds of clock skew).
4. `scope` (space-separated) contains what the route needs.

```python
# 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:

```ts
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.
