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 y no añade lógica de tokens propia.

Qué hace el middleware

Go (net/http)

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

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.

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.

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:

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:

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:

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.

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