
# Proteger una API de Express

Esta guía de inicio rápido protege una API de Express 5 con tokens de acceso de KeenAuth. Añada el middleware `expressAuth` del SDK de Node.js, requiera un scope en una ruta y pruebe la API sin conexión con el kit de pruebas del SDK. Express no es una dependencia del SDK; el middleware también funciona con Express 4.

La API tiene tres endpoints:

| Endpoint | Requiere |
|---|---|
| `GET /api/public` | nada |
| `GET /api/private` | un token de acceso válido para su API |
| `GET /api/messages` | un token de acceso válido con el scope `messages:read` |

## Antes de empezar

- Un entorno de KeenAuth. Su emisor tiene el formato `https://your-environment.eu.keenauth.app`.
- Una API registrada en el entorno ([Crear una API](/docs/api/management/apis/create)). Su `identifier`, por ejemplo `https://api.example.com`, es la audiencia de los tokens que acepta su API.
- El scope `messages:read` en esa API, concedido a la aplicación que la llama.
- Node.js 22 o posterior.

## 1. Instalar el SDK

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

El SDK todavía no está publicado. Mientras tanto, el ejemplo enlaza con el código fuente del SDK en su `package.json`.

## 2. Configurar

La API lee su configuración solo desde variables de entorno. Colóquelas en un archivo `.env`, que `npm start` carga con `node --env-file-if-exists=.env`, y nunca suba ese archivo al control de versiones.

| Variable | Obligatoria | Significado |
|---|---|---|
| `KEENAUTH_ISSUER` | sí | El emisor de su entorno |
| `KEENAUTH_AUDIENCE` | sí | El identificador de su API |
| `PUBLIC_BASE_URL` | sí | La URL que llaman los clientes (consulte [Detrás de un proxy](#detrás-de-un-proxy)) |
| `KEENAUTH_WEBHOOK_SECRET` | recomendada | El secreto de firma `idp_whsec_` del endpoint de webhook que entrega `token.revoked` (dos, separados por comas, mientras los rota). Sin él, la API nunca se entera de los tokens revocados |
| `PORT` | no | Puerto de escucha, por defecto `8080` |

## 3. Proteger sus rutas

`createApp` crea el protector y las rutas. `expressAuth(protector)` requiere un token válido; con `{ scopes }` también requiere scopes. El manejador lee los claims verificados de `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` construye el verificador real a partir de la configuración. El verificador obtiene las claves de firma de su entorno la primera vez que ve 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. Ejecutar y llamar a la API

```sh
npm start
```

Para una primera llamada, obtenga un token con el flujo de client credentials de una aplicación máquina a máquina que tenga el scope `messages:read` en su 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. Probar sin red

`@keenauth/node/testing` emite tokens firmados con una clave de prueba pública, y `createTestVerifier` acepta solo esos tokens. Las pruebas pasan ese verificador a `createApp` y llaman a la aplicación en un puerto local, así que no necesitan red ni una cuenta de KeenAuth. Consulte [Kit de pruebas sin conexión](/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
```

Las pruebas del ejemplo comprueban que se acepta un token válido y que un token ausente, caducado o con audiencia incorrecta, un scope ausente, un token en la query de la URL y un token revocado se rechazan con los estados indicados a continuación. También envían un token vinculado con DPoP con una prueba para `PUBLIC_BASE_URL`, y comprueban que `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` se niega a arrancar con el emisor de prueba.

## Respuestas

| Solicitud | Estado | `code` en el cuerpo |
|---|---|---|
| Sin cabecera `Authorization` | `401` | `missing_token` |
| Token caducado, token de otra API, firma incorrecta, token revocado | `401` | `invalid_token` |
| Token sin el scope requerido | `403` | `insufficient_scope` |
| Token en la query de la URL (`?access_token=`) | `400` | `invalid_request` |

El cuerpo es `application/problem+json` y la cabecera `WWW-Authenticate` incluye el desafío estándar. La tabla completa está en [Proteger su API con middleware](/docs/sdk/protect-api#respuestas).

## Detrás de un proxy

Si los clientes usan DPoP, el middleware comprueba que cada prueba se hizo para la URL que llamó el cliente. Establezca `PUBLIC_BASE_URL` en la URL externa que llaman los clientes, por ejemplo `https://api.example.com`, incluso detrás de un proxy o balanceador de carga. Es obligatoria y debe usar `https` (`http` solo para `localhost`, `127.0.0.1` o `[::1]`); la aplicación se niega a arrancar sin ella. El middleware nunca confía en las cabeceras `X-Forwarded-*`, sea cual sea la configuración de `trust proxy` de Express.

## Tokens revocados

Un token de acceso sigue siendo válido hasta que caduca, así que KeenAuth informa a su API cuando se revocan tokens. El ejemplo pasa una caché de revocación en memoria al protector y sirve un webhook `token.revoked` en `POST /webhooks/keenauth` (`webhook.js`) que la alimenta.

1. En el panel, cree un endpoint de webhook con la URL `https://<your host>/webhooks/keenauth` que se suscriba a `token.revoked`, y exporte su secreto de firma como `KEENAUTH_WEBHOOK_SECRET`.
2. El manejador verifica la firma con el SDK sobre el cuerpo sin procesar y responde `400` cuando no es válida o cuando la marca de tiempo difiere más de cinco minutos. Asigna el selector del evento (`access_token`, `session`, `user`, `client`, `org`, `org_user`, `client_user`) a un marcador de revocación y responde `204`; el middleware rechaza entonces todos los tokens coincidentes emitidos antes de `not_before`.
3. KeenAuth entrega al menos una vez, así que el manejador aplica cada `webhook-id` una sola vez. Si no se puede guardar el marcador, responde `503` y KeenAuth vuelve a entregar el evento. Ignora otros tipos de evento y selectores que ningún token de acceso puede cumplir (`refresh_family`, `api`).

El manejador registra el id del evento, el tipo de selector y el motivo, nunca el cuerpo, una firma ni el secreto. Cada proceso tiene su propia caché; con varias instancias, guarde los marcadores en un almacén compartido detrás del mismo método `revoke`. Consulte [Caché de revocación](/docs/sdk/protect-api#caché-de-revocación-tokenrevoked).

## Próximos pasos

- [Proteger su API con middleware](/docs/sdk/protect-api): Fastify, requisitos de step-up, DPoP y todas las opciones.
- [Verificar tokens](/docs/sdk/token-verification): el verificador que hay bajo el middleware.
- [Kit de pruebas sin conexión](/docs/sdk/testing).
