Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 067 — Token de acceso#

⬅️ 066 · 📚 Parte 5 · 🎓 Clases · 068 ➡️ Parte 5 — Identidad y seguridad · Nivel 🟡 intermedio · Pista backendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Firmar y verificar una credencial sin estado en el servidor. La clase 066 guardó la sesión en el servidor; esta la mete entera en el token, firmada [rfc7519]. El servidor no recuerda nada entre peticiones — y ese es a la vez el atractivo y el precio.

🧩 La situación#

POST /token con credenciales entrega un JWT. GET /informe lo exige en Authorization: Bearer … y lo verifica sin consultar nada: la única prueba de autenticidad es la firma. El contrato ataca esa firma por los cuatro costados conocidos.

🧮 El contrato#

Petición Respuesta
GET /informe sin token 401
POST /token con credenciales malas 401
POST /token con credenciales buenas 200 · {token, tipo: "Bearer"}
GET /informe con el token recién emitido 200 · {"usuario": "ana"}
el mismo token con la firma alterada 401
un token caducado con firma buena 401
un token firmado con otra clave 401
el ataque alg: none — sin firma 401

Los tres primeros ataques son estáticos: tokens precalculados que viajan en el contrato, porque un token caducado o de otra clave es solo texto. El cuarto es histórico y sigue siendo el mejor examen de una verificación: un token cuya cabecera declara alg: none y no trae firma. Las bibliotecas que dejaban que la cabecera del token eligiera el algoritmo lo aceptaban — y la cabecera la escribe el atacante [hoffman-web-application-security]. Por eso las cuatro implementaciones fijan la lista de algoritmos en el código del verificador, no la leen del token.

Para el caso dinámico, el verificador ganó variables entre casos: el caso que emite declara guardar: {token: "token"} y los siguientes interpolan ${token} — incluida la versión alterada, que es ${token}AA.

📖 Lo que un JWT es y lo que no es#

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiJhbmEiLCJleHAiOjQxMDI0NDQ4MDB9 . BIRKBW…
        cabecera              cuerpo (¡legible!)                    firma

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Token de acceso (JWT) Un dato firmado que el cliente presenta en cada petición y que el servidor verifica sin consultar nada. Su cuerpo va codificado, no cifrado: cualquiera que lo tenga puede leerlo. Y lo que no se guarda no se puede revocar.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
ASP.NET Core framework web de .NET (C#) 2016 MIT Microsoft y .NET Foundation

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-067-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java

🔧 ASP.NET Core#

Reescritura multiplataforma y de código abierto de la pila web de Microsoft. Sus API mínimas trajeron el estilo de los microframeworks al ecosistema .NET.

Preparar sus dependencias, dentro de su directorio:

dotnet build -c Release --nologo -v quiet

Arrancarla suelta, sin el verificador:

PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000

Qué hay dentro de su directorio:

Archivo Qué es
Clase067.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Ninguno de los cuatro frameworks firma tokens por sí mismo. En los cuatro la pieza es una biblioteca externa, y eso ya es el hallazgo: emitir y verificar un JWT no es trabajo del framework web. Lo que sí cambia entre ellos es qué te deja hacer mal esa biblioteca.

Express · express/server.mjs — con jsonwebtoken#

  const token = jwt.sign({ sub: usuario }, SECRETO, {
    algorithm: "HS256",
    expiresIn: "1h",
  });
  respuesta.json({ token, tipo: "Bearer", expira_en: 3600 });

El token lleva lo que el servidor necesitará saber sin consultar nada: quién (sub) y hasta cuándo (exp). No lleva secretos — el cuerpo de un JWT va codificado, no cifrado: cualquiera que lo tenga puede leerlo.

    const datos = jwt.verify(token, SECRETO, { algorithms: ["HS256"] });

Esa lista algorithms es la línea que separa esta clase de un titular de seguridad. Sin ella, la biblioteca acepta lo que declare la cabecera del token — y la cabecera la escribe quien ataca. El ataque alg: none fue exactamente eso.

  } catch {
    respuesta.status(401).json({ error: "token-invalido" });
  }

Alterado, caducado, de otra clave o ausente: un solo 401 para todo. Al cliente legítimo le da igual el matiz, y al atacante no hay que dárselo.

FastAPI · fastapi/main.py — con PyJWT#

    token = jwt.encode(
        {"sub": credenciales.usuario, "exp": int(time.time()) + 3600},
        SECRETO,
        algorithm="HS256",
    )

Aquí la caducidad se calcula a mano —int(time.time()) + 3600— en lugar de declararse como en Express. Mismo resultado, un recordatorio menos que el framework te da.

        datos = jwt.decode(token, SECRETO, algorithms=["HS256"])
    except jwt.InvalidTokenError:
        return JSONResponse({"error": "token-invalido"}, status_code=401)

InvalidTokenError es la clase base de toda la jerarquía de errores de PyJWT: firma mala, formato roto y caducidad caen ahí. Es lo que permite el 401 único sin enumerar casos. Y decode verifica exp por omisión.

Spring Boot · spring-boot/…/Aplicacion.java — con jjwt#

    private static final SecretKey CLAVE = Keys.hmacShaKeyFor(
            "clave-de-firma-solo-para-el-laboratorio".getBytes(StandardCharsets.UTF_8));

hmacShaKeyFor exige al menos 256 bits para HS256: una clave corta no arranca. El framework convierte una mala práctica en un error de ejecución, que es la forma más eficaz de documentación que existe.

            Claims datos = Jwts.parser().verifyWith(CLAVE).build()
                    .parseSignedClaims(token).getPayload();

Y aquí está la mejor decisión de diseño del elenco: parseSignedClaims solo acepta tokens firmados. alg: none no es un caso especial que haya que acordarse de bloquear con una lista; es un token no firmado, y se rechaza por tipo. Express y FastAPI lo evitan porque el programador escribió la lista; Spring lo evita porque la API no ofrece la otra opción.

ASP.NET Core · aspnet-core/Program.cs — con Microsoft.IdentityModel#

    var resultado = await manejador.ValidateTokenAsync(token, new TokenValidationParameters
    {
        ValidateIssuer = false,
        ValidateAudience = false,
        IssuerSigningKey = clave,
        ValidAlgorithms = [SecurityAlgorithms.HmacSha256],
        ClockSkew = TimeSpan.Zero,
    });

Un objeto de parámetros en lugar de argumentos sueltos: cada cosa que se valida —o que se decide no validar— queda escrita. ValidateIssuer = false no es descuido, es una decisión declarada, y eso es mejor que un valor por omisión invisible.

Y el detalle que hay que saber antes de escribir el contrato: ClockSkew vale cinco minutos por omisión. Un token caducado hace tres minutos seguiría entrando. Es una tolerancia razonable para relojes desincronizados entre servidores y una trampa para cualquiera que intente medir la caducidad; por eso aquí se pone a cero.

📊 Comparación#

Framework Biblioteca alg: none Caducidad Detalle propio
Express jsonwebtoken rechazado si fijas algorithms verifica exp sin lista de algoritmos, decide el token
FastAPI PyJWT rechazado: algorithms es obligatorio por omisión la API obliga a lo correcto
Spring Boot jjwt rechazado por tipo por omisión exige clave ≥ 256 bits
ASP.NET Core Microsoft.IdentityModel rechazado con ValidAlgorithms margen de 5 min por omisión ClockSkew explícito

⚖️ Sesión o token#

La pregunta de la parte, y no tiene una respuesta única:

Sesión (066) Token (067)
Estado en el servidor no
Revocar al instante no: espera al exp
Verificar sin red no: consulta el almacén : solo la firma
Varios servicios verifican difícil: comparten almacén fácil: comparten clave pública
Robo de la credencial se mata la sesión vale hasta caducar

La regla práctica: aplicación web con su propio backend → sesión; API que consumen terceros o varios servicios → token. Y las arquitecturas reales combinan: sesión con el navegador, tokens entre servicios [rfc9700].

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 067

Los casos están en contrato.json. El verificador ejecuta las implementaciones que encuentre y declara las que omitió.

🧪 Reto de transferencia#

Añade POST /token/renovar: recibe un refresh token opaco (guardado en el servidor, como una sesión), emite un access token nuevo y rota el refresh. Añade al contrato el caso «refresh usado dos veces → 401»: la rotación convierte el robo del refresh en un fallo detectable [rfc9700].

🔗 Enlaces#

Fuentes#