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
- Lee el token del encabezado
Authorizationcon el esquemaBeareroDPoP. Un token en la query de la URL (?access_token=) siempre se rechaza, porque las URL terminan en los registros y en los encabezadosReferer. También se rechaza una solicitud con más de un encabezadoAuthorizationo con más de una credencial. - Comprueba DPoP. Con el esquema
DPoP, el encabezado de pruebaDPoP, 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 esquemaBearerse rechaza, y también el esquemaDPoPcon un token que no está vinculado a DPoP. - Comprueba los requisitos de la ruta: los scopes requeridos (todos) y, opcionalmente, step-up: valores
acraceptables 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.authen Express y Fastify,request.authen Django,g.authen Flask y el valor devuelto por la dependencia en FastAPI. - Responde a los fallos con el desafío estándar
WWW-Authenticatey un pequeño cuerpoapplication/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.
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.