
# Protección de tu API con middleware

Cada SDK de servidor incluye un middleware ligero que protege tu API con tokens de acceso de KeenAuth: `net/http` en Go, Express y Fastify en Node.js, y Django, Flask y FastAPI en Python. Se apoya en el [verificador de tokens](/docs/sdk/token-verification) y no añade lógica de tokens propia.

## Qué hace el middleware

- **Lee el token** del encabezado `Authorization` con el esquema `Bearer` o `DPoP`. Un token en la query de la URL (`?access_token=`) siempre se rechaza, porque las URL terminan en los registros y en los encabezados `Referer`. También se rechaza una solicitud con más de un encabezado `Authorization` o con más de una credencial.
- **Comprueba DPoP.** Con el esquema `DPoP`, el encabezado de prueba `DPoP`, el método de la solicitud y la URL absoluta de la solicitud (la URL base pública configurada seguida de la ruta de la solicitud) se pasan al verificador. Un token vinculado a DPoP enviado con el esquema `Bearer` se rechaza, y también el esquema `DPoP` con un token que no está vinculado a DPoP.
- **Comprueba los requisitos de la ruta:** los scopes requeridos (todos) y, opcionalmente, step-up: valores `acr` aceptables y una antigüedad máxima del inicio de sesión (RFC 9470).
- **Consulta la caché de revocación**, si configuras una, para saber si el token fue revocado.
- **Adjunta las claims verificadas** a la solicitud: `protect.ClaimsFrom(ctx)` en Go, `req.auth` en Express y Fastify, `request.auth` en Django, `g.auth` en Flask y el valor devuelto por la dependencia en FastAPI.
- **Responde a los fallos** con el desafío estándar `WWW-Authenticate` y un pequeño cuerpo `application/problem+json`. La respuesta nunca contiene el token ni ningún valor de claim, y el middleware nunca los registra.

## Go (net/http)

El paquete `protect` solo usa la biblioteca estándar y funciona con cualquier router que acepte un `http.Handler`.

```go
import (
	"github.com/keenauth/keenauth-go/protect"
	"github.com/keenauth/keenauth-go/verify"
)

v, err := verify.New(verify.Config{
	Issuer:   "https://acme.eu.keenauth.app",
	Audience: "https://api.acme.example",
})
if err != nil {
	return err
}
m, err := protect.New(v, protect.Options{
	PublicBaseURL:       "https://api.acme.example",
	ResourceMetadataURL: "https://api.acme.example/.well-known/oauth-protected-resource",
})
if err != nil {
	return err
}

mux := http.NewServeMux()
mux.Handle("GET /orders", m.Require(protect.Requirement{Scopes: []string{"orders:read"}})(http.HandlerFunc(
	func(w http.ResponseWriter, r *http.Request) {
		claims, _ := protect.ClaimsFrom(r.Context())
		fmt.Fprintln(w, claims.Subject)
	})))

age := 10 * time.Minute
mux.Handle("POST /payouts", m.Require(protect.Requirement{
	Scopes:        []string{"payouts:write"},
	AcceptableACR: []string{"urn:idp:acr:mfa"},
	MaxAuthAge:    &age,
})(payoutsHandler))
```

`m.Handler(next)` protege un handler con un token válido y sin requisitos adicionales. `Require` provoca un pánico al arrancar si un scope no es un token de scope válido, como hace `http.ServeMux` con un patrón no válido.

## Node.js (Express y Fastify)

El middleware está en el subpath `@keenauth/node/middleware`. Express y Fastify no son dependencias del SDK.

```ts
import { createVerifier } from "@keenauth/node";
import { createProtector, expressAuth, fastifyAuth } from "@keenauth/node/middleware";

const verifier = createVerifier({ issuer: "https://acme.eu.keenauth.app", audience: "https://api.acme.example" });
const protector = createProtector(verifier, { publicBaseUrl: "https://api.acme.example" });

// Express
app.get("/orders", expressAuth(protector, { scopes: ["orders:read"] }), (req, res) => {
  res.json({ sub: req.auth.subject });
});

// Fastify
fastify.decorateRequest("auth", null);
fastify.get("/orders", { preHandler: fastifyAuth(protector, { scopes: ["orders:read"] }) }, async (request) => {
  return { sub: request.auth.subject };
});
```

Step-up usa `{ acceptableAcr: ["urn:idp:acr:mfa"], maxAuthAgeSeconds: 600 }`. En TypeScript, tipa la solicitud como `AuthenticatedRequest` (exportado por el subpath) para leer `req.auth`.

## Python (Django, Flask, FastAPI)

Instala el extra de tu framework: `pip install "keenauth-python[django]"`, `"keenauth-python[flask]"` o `"keenauth-python[fastapi]"`. El framework solo se importa en su propio módulo adaptador.

```python
from keenauth.middleware import Protector
from keenauth.verify import Verifier, VerifierConfig

verifier = Verifier(VerifierConfig(issuer="https://acme.eu.keenauth.app", audience="https://api.acme.example"))
protector = Protector(verifier, public_base_url="https://api.acme.example")
```

Django:

```python
from django.http import JsonResponse
from keenauth.middleware.django import protect

@protect(protector, scopes=["orders:read"])
def orders(request):
    return JsonResponse({"sub": request.auth.subject})
```

Flask:

```python
from flask import g
from keenauth.middleware.flask import protect

@app.get("/orders")
@protect(protector, scopes=["orders:read"])
def orders():
    return {"sub": g.auth.subject}
```

FastAPI:

```python
from fastapi import Depends, FastAPI
from keenauth.middleware.fastapi import RequireAuth, install
from keenauth.verify import AccessClaims

app = FastAPI()
install(app)

@app.get("/orders")
def orders(claims: AccessClaims = Depends(RequireAuth(protector, scopes=["orders:read"]))):
    return {"sub": claims.subject}
```

`install(app)` registra el handler que escribe el cuerpo del problema y un encabezado `WWW-Authenticate` por cada desafío. Step-up usa `acceptable_acr=["urn:idp:acr:mfa"], max_auth_age=timedelta(minutes=10)`.

## URL base pública (obligatoria)

Una prueba DPoP indica la URL a la que llamó el cliente (`htu`). El middleware la compara con la URL base pública que configuras (`PublicBaseURL`, `publicBaseUrl`, `public_base_url`) seguida de la ruta de la solicitud. Configúrala con la dirección que usan los clientes, por ejemplo `https://api.acme.example` o `https://acme.example/api`. Es obligatoria: crear el middleware sin ella falla al arrancar con un error de configuración, incluso si tus clientes envían solo tokens Bearer.

El middleware nunca construye esta URL a partir del encabezado `Host` de la solicitud ni de encabezados `X-Forwarded-*`, y en Python no usa la URL de solicitud propia del framework, aunque el framework o el servidor estén configurados para confiar en los encabezados reenviados. Los clientes eligen estos valores: si tu API se sirve bajo dos nombres de host, una prueba creada para uno de ellos podría reutilizarse en el otro. Detrás de un balanceador de carga o de un proxy inverso, la URL base pública también es lo que indica al middleware el esquema, el host y el prefijo de ruta que ven los clientes. La URL base pública debe usar `https`, salvo para `localhost`, `127.0.0.1` y `[::1]` (RFC 6750 §5.3); el esquema y el host se comparan sin distinguir mayúsculas y minúsculas, y se ignora un puerto por defecto (`:443`, `:80`), tanto en la URL base como en el `htu` de la prueba.

## Respuestas

| Situación | Estado | `WWW-Authenticate` | `code` |
|---|---|---|---|
| No hay token, o un esquema no compatible | `401` | `Bearer` y `DPoP algs="…"`, sin error | `missing_token` |
| Token en la query, `Authorization` repetido o mal formado | `400` | `Bearer error="invalid_request"` | `invalid_request` |
| Token no válido, caducado o revocado | `401` | `error="invalid_token"` | `invalid_token` |
| Prueba DPoP ausente, repetida o no válida | `401` | `DPoP error="invalid_dpop_proof"` | `invalid_dpop_proof` |
| Falta un scope requerido | `403` | `error="insufficient_scope", scope="…"` | `insufficient_scope` |
| Se necesita step-up | `401` | `error="insufficient_user_authentication", acr_values="…", max_age="…"` | `insufficient_user_authentication` |
| Claves de firma o estado de revocación no disponibles | `503` | ninguno | `jwks_unavailable`, `revocation_unavailable` |

El desafío usa el esquema que usó el cliente. Cuando configuras la URL de metadatos del recurso, cada desafío `401` incluye `resource_metadata` (RFC 9728). El cuerpo tiene `type`, `title`, `status`, `code` y un `detail` fijo; nunca indica por qué un token no era válido. Para tus registros, pasa un callback de error (`OnError`, `onError`, `on_error`): recibe el error del verificador, que no contiene ningún token ni valor de claim. No registres tú mismo los encabezados `Authorization` ni `DPoP`.

## Caché de revocación (`token.revoked`)

Un token de acceso JWT sigue siendo válido hasta que caduca. Para rechazar antes los tokens revocados, gestiona el webhook `token.revoked` y guarda su selector en una caché de revocación que consulta el middleware. Los SDK incluyen una caché en memoria: `protect.NewMemoryRevocationCache` (Go), `createMemoryRevocationCache` (Node.js) y `MemoryRevocationCache` (Python).

Un selector identifica un ID de token (`jti`), una sesión (`sid`), un usuario (`sub`), una organización (`org_id`), una aplicación (`client_id`), o los pares organización + usuario y aplicación + usuario. Un selector `jti` revoca ese token. Los demás selectores revocan todos los tokens emitidos antes del `not_before` del evento; los tokens emitidos después se aceptan. Los marcadores se conservan 24 horas y 1 minuto por defecto, es decir, la vida útil máxima posible de un token de acceso más la desviación del reloj.

```go
cache := protect.NewMemoryRevocationCache(0, nil)
m, _ := protect.New(v, protect.Options{PublicBaseURL: "https://api.acme.example", Revocation: cache})

// In your verified token.revoked webhook handler:
err := cache.Revoke(protect.Selector{SessionID: "ses_…"}, notBefore)
```

La caché en memoria pertenece a un solo proceso. Si tu API se ejecuta en varias instancias, implementa la interfaz del verificador de revocación sobre un almacén compartido. Si la caché no puede responder, el middleware rechaza la solicitud con `503`.

## Pruebas

Usa el [kit de pruebas sin conexión](/docs/sdk/testing) para generar tokens en las pruebas de tu middleware. El kit no incluye un helper de pruebas DPoP; firma una prueba en tu test con la biblioteca criptográfica de tu lenguaje.
