Qué es un JWT
Parte de la sección Autenticación y seguridad del Journey de Node.js de Coddy. Lección 14 de 24.
Una sesión necesita consultar la base de datos en cada solicitud. Un JWT (JSON Web Token) lleva los datos en sí mismo: tres partes unidas por puntos. Cada parte se escribe en base64url, una forma de escribir bytes con letras, dígitos, - y _. El encabezado indica el algoritmo, la carga útil contiene las afirmaciones (sub indica de quién es el token, exp indica cuándo caduca) y la firma demuestra que lo generó el servidor.
Base64url es una codificación, no un cifrado. Cualquiera que tenga un token puede leer su carga útil:
const payload = token.split('.')[1];
JSON.parse(Buffer.from(payload, 'base64url').toString());
// { sub: '42', name: 'Lin', exp: 1767225600 }decodeJwt(token) de jose, el paquete para JWT, hace lo mismo. Lee la carga útil sin comprobar la firma y genera un error cuando el texto no es un JWT. Por eso, nunca pongas un secreto en una carga útil ni tomes decisiones basándote en una afirmación que te haya proporcionado decodeJwt: cualquiera puede escribir cualquier afirmación.
Desafío
FácilEste capítulo añade una segunda forma de iniciar sesión, mediante tokens, para las aplicaciones que llaman a tu API. Parte de una copia más pequeña del servidor, con registro e inicio de sesión, pero sin sesiones ni notas. Un JWT lleva sus datos en su interior y cualquiera puede leerlos. Crea una ruta que muestre lo que dice un token y cierra una página de administración que confía en cualquier afirmación de un token.
- En
server.js, escribeGET /inspect. Lee un token de la cadena de consulta, como enGET /inspect?token=eyJ..., y devuelve las declaraciones que contiene como JSON. Léelas condecodeJwt, que ya está importado. Para un token cuya carga útil es{"sub": "lin", "plan": "free"}, la ruta devuelve exactamente{"sub": "lin", "plan": "free"}. decodeJwtlanza un error cuando el texto no es un JWT. Captúralo y responde con el estado 400 y{"error": "Not a JWT"}. Por ejemplo,GET /inspect?token=hellorecibe ese estado 400.- Fíjate en
GET /admin. Lee el token condecodeJwty muestra un secreto cuando el token indica"admin": true. PerodecodeJwtnunca comprueba la firma, así que cualquiera puede escribir un token que indique"admin": true. Este servidor todavía no puede comprobar firmas. Así que haz queGET /adminresponda con el estado 403 y{"error": "Admins only"}a todas las solicitudes, sea cual sea el token que reciba. 403 Forbidden significa «no puedes ver esto». En el capítulo 4 se volverá a abrir una página de administración, esta vez solo para administradores reales.
Pruébalo: Pulsa Ejecutar. La barra de solicitudes empieza con GET /admin?token=, seguido de este token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJtYWxsb3J5IiwiYWRtaW4iOnRydWV9.c2lnbmF0dXJl Un atacante llamado mallory lo escribió a mano, sin ninguna clave secreta. Envíalo antes de cambiar nada: la versión inicial devuelve {"secret": "The vault key is 42"}. Después de hacer el cambio, vuelve a pulsar Ejecutar y envía la misma solicitud: estado 403 y {"error": "Admins only"}. Envía GET /inspect?token= seguido del mismo token: {"sub": "mallory", "admin": true}, leído sin ninguna clave. Envía GET /inspect?token=hello: estado 400 y {"error": "Not a JWT"}.
Pruébalo tú mismo
import express from 'express';
import { decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
res.json({ username: user.username });
});
// Muestra lo que dice un token: GET /inspect?token=...
app.get('/inspect', (req, res) => {
res.status(501).json({ error: 'Not written yet' });
});
// La página de administración.
app.get('/admin', (req, res) => {
const claims = decodeJwt(String(req.query.token ?? ''));
if (claims.admin === true) {
return res.json({ secret: 'The vault key is 42' });
}
res.status(403).json({ error: 'Admins only' });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Autenticación y seguridad
Practica por tu cuenta: Playground de Express