flowchart LR R["Request<br/>GET /tasks"] --> M1["logging<br/><i>registra</i>"] --> M2["auth<br/><i>verifica JWT,<br/>busca user</i>"] M2 -->|válido| H["handler<br/><i>ya sabe quién eres</i>"] M2 -->|inválido| E["401 — el handler<br/>NUNCA se ejecuta"] H --> Res["Response 200"]
25 16. Necesitamos saber quién eres en cada request
¿Copiar la verificación del JWT en 20 endpoints? No.
Veinte endpoints protegidos no pueden llevar veinte copias de “verifica el token”. El patrón que lo resuelve es middleware: una función que se ejecuta antes de tu handler, resuelve quién eres, y te entrega el usuario listo — o corta el request con un 401. En este capítulo get_current_user nace y protege Taskflow sin duplicar una línea.
25.1 El problema
Cada endpoint privado necesita lo mismo: leer la cookie, verificar la firma, extraer sub, buscar el usuario, responder 401 si algo falla. Copiarlo es más que feo — es inconsistente: el endpoint donde olvides pegarlo queda público, y el día que cambie la verificación hay que tocar 20 archivos. La verificación de auth es un cross-cutting concern: una capa que atraviesa todo, no parte de ningún handler.
Una firma (HMAC) prueba que un mensaje es auténtico y no fue tocado: se calcula con un secreto sobre el contenido. Los webhooks la usan para que verifiques “esto realmente vino de Stripe”.
La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.
Una cookie es un dato que el servidor pone en tu navegador y el navegador devuelve en cada request. httpOnly = JavaScript no puede leerla (el XSS no la roba); Secure = solo viaja por HTTPS.
25.2 Cómo lo resuelve un equipo
El patrón universal: la lógica compartida se envuelve alrededor del handler, no dentro. Cada stack tiene su dialecto:
- Express:
app.use(auth)— función que recibenext()y decide si la cadena continúa. - FastAPI:
Depends(get_current_user)— declaras la dependencia en la firma; el framework la resuelve y inyecta el resultado.
Un framework es un esqueleto de aplicación ya decidido: te da la estructura (rutas, validación, errores) y tú llenas la lógica. Diferencia con librería: la librería la llamas tú; el framework te llama a ti.
- Go:
auth(handler)— una función que devuelve un handler envuelto: composición explícita, sin magia.
Y la pregunta gemela: una vez verificado, ¿dónde vive el usuario para que el handler lo use sin otra query? Cada stack tiene su canal: res.locals (Express), el retorno del Depends (FastAPI), el context.Context del request (Go).
Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.
25.3 Conceptos nuevos
- U El patrón middleware/pipeline: código que corre antes del handler y decide si este existe
- TS
app.use(auth)— la función que envuelve la siguiente - Py
Depends(...)— inyección declarativa: pides el usuario en la firma - Go
middleware(handler)— composición: handler que devuelve handler - U Propagar el usuario:
res.localsvs retorno de DI vscontext.Context - U El orden del pipeline: auth antes del handler, logging antes de todo
25.4 La explicación visual
El pipeline completo de un request protegido:
El middleware es una aduana: el handler detrás puede asumir “quien llegó aquí ya mostró su identificación”.
25.5 Implementación
El mismo patrón en tres dialectos — verifica el token, carga el usuario, lo entrega al handler:
Un token es una credencial portable: una cadena que dice quién eres y hasta cuándo. El servidor la emite tras el login; el cliente la presenta en cada request en vez de la contraseña.
// auth.middleware.ts — the customs officer
export async function auth(req: Request, res: Response, next: NextFunction) {
try {
const { payload } = await jwtVerify(req.cookies.token, secret);
const user = await getUserById(Number(payload.sub));
if (!user) throw new Error("no user");
res.locals.user = user; // where the user lives for this request
next(); // proceed to the handler
} catch {
throw new UnauthorizedError("invalid or expired token"); // 401, handler skipped
}
}
// routes.ts — one word protects the whole group
app.use("/tasks", auth);
app.get("/tasks", async (req, res) => {
const user = res.locals.user; // already verified, already loaded
res.json(await listTasks(user.id));
});res.locals es el bolsillo del request: vive lo que el middleware dejó. El handler nunca toca el JWT — solo pregunta “¿quién soy?”.
# deps.py — the dependency FastAPI resolves for you
def get_current_user(request: Request) -> User:
token = request.cookies.get("token")
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
user = get_user_by_id(int(payload["sub"]))
except Exception:
raise UnauthorizedError("invalid or expired token")
if not user:
raise UnauthorizedError("invalid or expired token")
return user # whatever you return gets INJECTED
# routes.py — asking for it in the signature IS the protection
@app.get("/tasks")
def list_tasks(user: User = Depends(get_current_user)):
return list_tasks_service(user.id) # user already verified + loadedEsto es inyección de dependencias: Depends(get_current_user) dice “para ejecutar este handler, primero corre esa función y dame su resultado”. FastAPI la llama; si lanza UnauthorizedError, el handler ni se ejecuta. La protección está en la firma — imposible olvidarla.
// auth.go — a function that wraps a handler
func auth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("token")
if err != nil {
writeError(w, 401, "invalid or expired token")
return // handler never runs
}
claims := jwt.MapClaims{}
_, err = jwt.ParseWithClaims(cookie.Value, claims,
func(t *jwt.Token) (any, error) { return []byte(os.Getenv("JWT_SECRET")), nil })
if err != nil {
writeError(w, 401, "invalid or expired token")
return
}
sub, _ := strconv.ParseInt(claims["sub"].(string), 10, 64)
user, err := getUserByID(r.Context(), sub)
if err != nil || user == nil {
writeError(w, 401, "invalid or expired token")
return
}
ctx := context.WithValue(r.Context(), userKey, user) // user travels in ctx
next(w, r.WithContext(ctx)) // proceed to the handler
}
}
// routes.go — wrap it, one word
mux.HandleFunc("GET /tasks", auth(listTasks))
func listTasks(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(userKey).(*User) // already verified
writeJSON(w, 200, mustListTasks(r.Context(), user.ID))
}Go no tiene app.use ni Depends — tiene funciones que devuelven funciones. auth(listTasks) es el handler: uno que hace aduana y delega. Todo explícito — nada se esconde.
El patrón en una línea: la verificación vive en un solo lugar, y proteger un endpoint nuevo cuesta una palabra (Depends, app.use, auth(...)). Cap. 17 le da argumentos — middlewares con parámetros.
El parámetro es el hueco declarado en la función (def f(x) — x es parámetro); el argumento es el valor concreto que le pasas (f(42) — 42 es argumento). Mismo dato, dos momentos: declaración vs uso.
Implementa el middleware de autenticación para mi API
[Express / FastAPI / Go]. Requisitos: verifica el JWT de la cookie
'token' (firma HS256 con JWT_SECRET de env), carga el usuario completo
desde la BD por su sub, lo propaga al handler por el mecanismo idiomático
(res.locals / Depends / context.Context), y responde 401 genérico sin
ejecutar el handler si falla. Debe proteger un grupo de rutas con UNA
línea (app.use / Depends / wrapper). Explícame el orden del pipeline y
dónde pondrías logging y rate-limit relativos a auth.
25.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: los tres resuelven el mismo problema — “código antes del handler con usuario resuelto” — con el mecanismo que su lenguaje hace natural: Express encadena funciones, FastAPI declara dependencias, Go compone handlers. Si entiendes el patrón, cada dialecto se aprende en una tarde; si solo memorizas el dialecto, cada framework parece magia nueva.
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
Una dependencia es código de terceros que tu proyecto usa: npm install, pip install, go get las traen. Cada una es deuda — ahora funciona, pero hay que mantenerla, actualizarla y confiar en ella.
request → logging → CORS → rate-limit → auth → handler
loggingprimero: quieres registrar todo, incluso los 401.rate-limitantes deauth: no gastesbcrypt.compare/JWT verify en requests que vas a rechazar de todas formas (cap. 18).authjusto antes del handler protegido: lo público (/health,/auth/login) queda fuera.- Los errores de cualquier capa caen al mapper del cap. 6.
En Express el orden es literal (el orden de app.use); en FastAPI es el orden de los Depends/middlewares; en Go es cómo anidas los wrappers: logging(cors(rateLimit(auth(handler)))) — se lee de afuera hacia adentro.
25.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| Verificación copiada por endpoint | “Solo era un endpoint más” | Un middleware; el olvido cuesta un endpoint público |
| Auth que consulta la BD… por cada request… en endpoints que no la necesitan | El middleware carga el user aunque el handler no lo use | OK si es barato; si no, resuelve sub en middleware y carga el user en el servicio que lo necesita |
| Middleware registrado DESPUÉS de las rutas | Orden del pipeline al revés | app.use(auth) antes de las rutas protegidas |
| El user viaja en variable global | let currentUser = ... → requests concurrentes se mezclan |
res.locals/Depends/ctx — por request, nunca global |
| Rutas públicas detrás del auth | /health pidiendo JWT |
Agrupa: público primero, use(auth) solo en el grupo privado |
25.8 Buenas prácticas
- El middleware resuelve identidad, el handler decide permisos — separa autenticación (quién eres, cap. 16) de autorización (qué puedes, cap. 17).
La autorización responde “¿qué puedes hacer?”: eres usuario válido (autenticado), pero ¿puedes borrar esta tarea? Se decide por rol o por ownership — y se verifica en cada request, no se recuerda.
- El usuario propagado es el mínimo necesario: id + lo que el handler consulta siempre — no la fila entera si no hace falta.
- 401 temprano: falla rápido, antes de tocar la BD si la firma ya es inválida.
- Nombra la dependencia por lo que devuelve (
get_current_user), no por lo que hace (check_token) — el handler que la pide se lee solo.
25.9 Ejercicio
- Dibuja el pipeline de
POST /tasksen tu stack, marcando dónde se puede cortar con 401. - Escribe la línea exacta que protege
GET /projectsen tu stack. - ¿Por qué
currentUseren variable global es un bug de concurrencia?
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
POST /tasks→ logging → rate-limit → auth (aquí muere con 401 si el token falta/expira) → validación del body (422) → handler → servicio → BD.- TS:
app.get("/projects", auth, listProjects)(oapp.use("/projects", auth)para todo el grupo). Py:def list_projects(user: User = Depends(get_current_user)). Go:mux.HandleFunc("GET /projects", auth(listProjects)). - Porque el servidor atiende muchos requests a la vez: mientras la request de Ana está en el handler, la de Bruno sobreescribe
currentUser— y Ana ejecuta acciones como Bruno. El usuario es estado del request: vive enres.locals, el retorno delDependso elcontext.Context, que son por-request por diseño.
25.10 Mini reto
Proteger un endpoint nuevo en una sola línea — y entender por qué funciona.
Go:
mux.HandleFunc("DELETE /tasks/{id}", auth(deleteTask))Funciona porque auth es una función que devuelve un handler: el handler resultante hace la aduana primero y, solo si pasa, llama a deleteTask. La línea parece declarativa (“este endpoint requiere auth”) pero es composición real de funciones — auth(deleteTask) es un http.HandlerFunc válido. La misma línea en FastAPI es el Depends(get_current_user) en la firma; en Express, poner auth entre la ruta y el handler. Una palabra, toda la protección.
25.11 Vocabulario técnico del capítulo
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Un string es texto entre comillas: "hola". El nombre viene de “cadena de caracteres” — una secuencia de letras. Todo lo que llega de un formulario o una URL llega como string, aunque parezca número.
null (TS), None (Py), nil (Go) = “aquí no hay valor”. Es la respuesta a “¿qué devuelvo cuando no hay nada?” — y la fuente del bug más famoso de la historia (su inventor lo llamó “el error del billón de dólares”). Por eso el código revisa if x is not None.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
Asíncrono = empezar algo sin esperar sentado a que termine: pides la pizza (async) y sigues trabajando; cuando llega, te avisan. Lo opuesto a síncrono (esperar parado). Vital cuando la espera es larga: red, disco, bases de datos.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
Un JWT (JSON Web Token) es un token firmado: el servidor lo genera con su secreto y puede verificarlo sin consultar la BD. Ojo: está firmado, no cifrado — cualquiera puede leer su contenido, pero no modificarlo.
Un hash es una función de un solo sentido: contraseña →$2b\(10\)…`. No se puede revertir — por eso las contraseñas se hashean, no se cifran. bcrypt es lento a propósito: fuerza bruta cara para el atacante.