
# Proteggi un'API Express

Questa guida rapida protegge un'API Express 5 con access token di KeenAuth. Aggiungi il middleware `expressAuth` dell'SDK per Node.js, richiedi uno scope su una route e testi l'API offline con il test kit dell'SDK. Express non è una dipendenza dell'SDK; il middleware funziona anche con Express 4.

L'API ha tre endpoint:

| Endpoint | Richiede |
|---|---|
| `GET /api/public` | nulla |
| `GET /api/private` | un access token valido per la tua API |
| `GET /api/messages` | un access token valido con lo scope `messages:read` |

## Prima di iniziare

- Un ambiente KeenAuth. Il suo issuer ha il formato `https://your-environment.eu.keenauth.app`.
- Un'API registrata nell'ambiente ([Crea un'API](/docs/api/management/apis/create)). Il suo `identifier`, per esempio `https://api.example.com`, è l'audience dei token che la tua API accetta.
- Lo scope `messages:read` su quell'API, concesso all'applicazione che la chiama.
- Node.js 22 o successivo.

## 1. Installa l'SDK

```sh
npm install @keenauth/node express@5.2.1
```

L'SDK non è ancora pubblicato. Finché non lo sarà, l'esempio rimanda al codice sorgente dell'SDK nel suo `package.json`.

## 2. Configura

L'API legge la configurazione solo da variabili d'ambiente. Inseriscile in un file `.env`, che `npm start` carica con `node --env-file-if-exists=.env`, e non committare mai quel file.

| Variabile | Obbligatoria | Significato |
|---|---|---|
| `KEENAUTH_ISSUER` | sì | L'issuer del tuo ambiente |
| `KEENAUTH_AUDIENCE` | sì | L'identificatore della tua API |
| `PUBLIC_BASE_URL` | sì | L'URL che i client chiamano (vedi [Dietro un proxy](#dietro-un-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | consigliata | Il segreto di firma `idp_whsec_` dell'endpoint webhook che consegna `token.revoked` (due, separati da virgola, durante la rotazione). Senza di essa l'API non viene mai a sapere dei token revocati |
| `PORT` | no | Porta di ascolto, predefinita `8080` |

## 3. Proteggi le tue route

`createApp` crea il protector e le route. `expressAuth(protector)` richiede un token valido; con `{ scopes }` richiede anche degli scope. L'handler legge i claim verificati da `req.auth`.

```js
export function createApp({ verifier, publicBaseUrl, revocation, webhookSecrets = [] }) {
  const protector = createProtector(verifier, {
    publicBaseUrl,
    revocation,
    // The error never contains the token or claim values; never log the Authorization or DPoP header.
    onError: (err) => console.warn(`rejected: ${err.message}`),
  });

  const app = express();
  app.disable("x-powered-by");

  // No token needed.
  app.get("/api/public", (req, res) => {
    res.json({ message: "Hello from a public endpoint." });
  });

  // Any valid access token for this API.
  app.get("/api/private", expressAuth(protector), (req, res) => {
    res.json({ message: "Hello from a private endpoint.", sub: req.auth.subject });
  });

  // A valid access token that also carries the messages:read scope.
  app.get("/api/messages", expressAuth(protector, { scopes: ["messages:read"] }), (req, res) => {
    res.json({ messages: ["Hello from a scoped endpoint."] });
  });

  // token.revoked deliveries from KeenAuth; the handler reads the raw body itself, so no JSON parser runs before it.
  if (webhookSecrets.length > 0) {
    app.post(WEBHOOK_PATH, ...createWebhookHandler({ secrets: webhookSecrets, revocation }));
  } else {
    console.warn("KEENAUTH_WEBHOOK_SECRET is not set: token.revoked events are not received, so revoked tokens stay valid until they expire");
  }

  return app;
}
```

`server.js` costruisce il verifier reale a partire dalla configurazione. Il verifier recupera le chiavi di firma dal tuo ambiente la prima volta che vede un token.

```js
const issuer = process.env.KEENAUTH_ISSUER;
const audience = process.env.KEENAUTH_AUDIENCE;
// The URL clients call: DPoP proofs are checked against it, never against the Host header.
const publicBaseUrl = process.env.PUBLIC_BASE_URL;
if (!issuer || !audience || !publicBaseUrl) {
  console.error("KEENAUTH_ISSUER, KEENAUTH_AUDIENCE and PUBLIC_BASE_URL are required");
  process.exit(1);
}

// createVerifier refuses an issuer under the reserved .invalid domain, so a test token can never pass here.
const verifier = createVerifier({ issuer, audience });
const revocation = createMemoryRevocationCache();
// The signing secret (idp_whsec_...) of the webhook endpoint that delivers token.revoked; comma separated while rotating.
const webhookSecrets = (process.env.KEENAUTH_WEBHOOK_SECRET ?? "").split(",").map((s) => s.trim()).filter(Boolean);
const app = createApp({ verifier, publicBaseUrl, revocation, webhookSecrets });

const port = Number(process.env.PORT || 8080);
app.listen(port, () => console.log(`listening on port ${port}`));
```

## 4. Avvia e chiama l'API

```sh
npm start
```

Per una prima chiamata, ottieni un token con il grant client credentials di un'applicazione machine-to-machine che ha lo scope `messages:read` sulla tua API:

```sh
ACCESS_TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=https://api.example.com \
  -d scope=messages:read \
  https://your-environment.eu.keenauth.app/oauth2/token | jq -r .access_token)

curl -i http://localhost:8080/api/messages -H "Authorization: Bearer $ACCESS_TOKEN"
```

## 5. Testa senza rete

`@keenauth/node/testing` genera token firmati con una chiave di test pubblica, e `createTestVerifier` accetta solo quei token. I test passano quel verifier a `createApp` e chiamano l'app su una porta locale, quindi non richiedono rete né un account KeenAuth. Vedi [Test kit offline](/docs/sdk/testing).

```js
test("scope check: 403 insufficient_scope without messages:read, 200 with it", async () => {
  await expectProblem(await get("/api/messages", bearer(mintAccessToken(AUDIENCE))), 403, "insufficient_scope", /scope="messages:read"/);
  const ok = await get("/api/messages", bearer(mintAccessToken(AUDIENCE, { claims: { scope: "profile messages:read" } })));
  assert.equal(ok.status, 200);
});
```

```sh
npm test
```

I test dell'esempio verificano che un token valido venga accettato e che un token mancante, scaduto o con audience errata, uno scope mancante, un token nella query dell'URL e un token revocato vengano rifiutati con gli stati indicati sotto. Inviano anche un token legato a DPoP con una proof per `PUBLIC_BASE_URL`, e verificano che `server. The tests also send signed `token.revoked` deliveries: a valid one revokes the token, a forged, tampered or stale one gets `400` and revokes nothing, and a repeated one has one effect.js` rifiuti di avviarsi con l'issuer di test.

## Risposte

| Richiesta | Stato | `code` nel body |
|---|---|---|
| Nessun header `Authorization` | `401` | `missing_token` |
| Token scaduto, token per un'altra API, firma non valida, token revocato | `401` | `invalid_token` |
| Token senza lo scope richiesto | `403` | `insufficient_scope` |
| Token nella query dell'URL (`?access_token=`) | `400` | `invalid_request` |

Il body è `application/problem+json` e l'header `WWW-Authenticate` contiene la challenge standard. La tabella completa è in [Proteggere la tua API con il middleware](/docs/sdk/protect-api#risposte).

## Dietro un proxy

Se i client usano DPoP, il middleware verifica che ogni proof sia stata creata per l'URL chiamato dal client. Imposta `PUBLIC_BASE_URL` sull'URL esterno che i client chiamano, per esempio `https://api.example.com`, anche dietro un proxy o un load balancer. È obbligatorio e deve usare `https` (`http` solo per `localhost`, `127.0.0.1` o `[::1]`); l'app rifiuta di avviarsi senza di esso. Il middleware non si fida mai degli header `X-Forwarded-*`, qualunque sia l'impostazione `trust proxy` di Express.

## Token revocati

Un access token resta valido fino alla scadenza, quindi KeenAuth comunica alla tua API quando i token vengono revocati. L'esempio passa una cache di revoca in memoria al protector e serve un webhook `token.revoked` su `POST /webhooks/keenauth` (`webhook.js`) che la alimenta.

1. Nella dashboard, crea un endpoint webhook con l'URL `https://<your host>/webhooks/keenauth` che si iscrive a `token.revoked`, ed esporta il suo segreto di firma come `KEENAUTH_WEBHOOK_SECRET`.
2. L'handler verifica la firma con l'SDK sul body grezzo e risponde `400` quando non è valida o quando il timestamp differe di più di cinque minuti. Associa il selettore dell'evento (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) a un marcatore di revoca e risponde `204`; il middleware rifiuta poi ogni token corrispondente emesso prima di `not_before`.
3. KeenAuth consegna almeno una volta, quindi l'handler applica ogni `webhook-id` una sola volta. Se il marcatore non può essere salvato, risponde `503` e KeenAuth consegna di nuovo l'evento. Ignora gli altri tipi di evento e i selettori che nessun access token può corrispondere (`refresh_family`, `api`).

L'handler registra l'id dell'evento, il tipo di selettore e il motivo, mai il body, una firma o il segreto. Ogni processo ha la propria cache; con più istanze, inserisci i marcatori in uno store condiviso dietro lo stesso metodo `revoke`. Vedi [Cache di revoca](/docs/sdk/protect-api#cache-di-revoca-tokenrevoked).

## Prossimi passi

- [Proteggere la tua API con il middleware](/docs/sdk/protect-api): Fastify, requisiti di step-up, DPoP e tutte le opzioni.
- [Verifica dei token](/docs/sdk/token-verification): il verifier alla base del middleware.
- [Test kit offline](/docs/sdk/testing).
