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). Su
identifier, por ejemplohttps://api.example.com, es la audiencia de los tokens que acepta su API. - El scope
messages:readen esa API, concedido a la aplicación que la llama. - Node.js 22 o posterior.
1. Instalar el SDK
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) |
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.
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.
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
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:
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.
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);
});
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.revokeddeliveries: a valid one revokes the token, a forged, tampered or stale one gets400 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.
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.
- En el panel, cree un endpoint de webhook con la URL
https://<your host>/webhooks/keenauthque se suscriba atoken.revoked, y exporte su secreto de firma comoKEENAUTH_WEBHOOK_SECRET. - El manejador verifica la firma con el SDK sobre el cuerpo sin procesar y responde
400cuando 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 responde204; el middleware rechaza entonces todos los tokens coincidentes emitidos antes denot_before. - KeenAuth entrega al menos una vez, así que el manejador aplica cada
webhook-iduna sola vez. Si no se puede guardar el marcador, responde503y 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.
Próximos pasos
- Proteger su API con middleware: Fastify, requisitos de step-up, DPoP y todas las opciones.
- Verificar tokens: el verificador que hay bajo el middleware.
- Kit de pruebas sin conexión.