Menu
CoddyTech

Verificar tokens

Parte de la sección Autenticación y seguridad del Journey de Node.js de Coddy. Lección 16 de 24.

Un cliente de API envía su token en un encabezado en cada solicitud: Authorization: Bearer <token>. Leerlo con decodeJwt aceptaría cualquier token que alguien escribiera. jwtVerify(token, key) vuelve a calcular la firma con tu secreto y genera un error cuando no coincide, cuando el token ha caducado o cuando ni siquiera es un JWT. Solo después de que devuelve un resultado puedes confiar en payload.

async function requireDevice(req, res, next) {
    const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
    try {
        const { payload } = await jwtVerify(token, deviceKey);
        req.device = payload.sub;
    } catch {
        return res.status(401).json({ error: 'Unknown device' });
    }
    next();
}
challenge icon

Desafío

Fácil

Ahora acepta los tokens que firmas. Una aplicación envía su token en un encabezado en cada solicitud, y el servidor comprueba la firma antes de confiar en cualquier dato que contenga.

  1. En server.js, importa jwtVerify desde jose.
  2. Escribe un middleware requireUser(req, res, next). Lee el encabezado Authorization. Ese encabezado contiene la palabra Bearer, un espacio y luego el token. Extrae el token.
  3. Comprueba el token con jwtVerify, usando la misma clave con la que firmas. jwtVerify lanza un error cuando la firma no coincide, cuando el token ha caducado o cuando el texto ni siquiera es un token. En todos esos casos, y cuando no hay encabezado, responde con el estado 401 y {"error": "Log in first"}.
  4. Cuando el token sea válido, establece req.user como un objeto con id, obtenido de la reclamación sub del token, y username, obtenido de su reclamación username. Después, llama a next().
  5. Añade GET /me con requireUser. Devuelve req.user: {"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.

Pruébalo: Pulsa Ejecutar. Envía GET /me: estado 401 y {"error": "Log in first"}. Envía POST /signup y luego POST /token, ambos con {"username": "ada", "password": "correct horse"}, y copia el token. En el campo de encabezados, añade un encabezado llamado Authorization cuyo valor sea Bearer, un espacio y tu token. Envía GET /me: estado 200, con tu id y "username": "ada". Ahora cambia una letra de la parte central del token, entre los dos puntos, y vuelve a enviar GET /me: estado 401, porque la firma ya no coincide.

Pruébalo tú mismo

import express from 'express';
import { SignJWT, decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

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 });
});

app.post('/token', 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' });
    }
    const token = await new SignJWT({ username: user.username })
        .setProtectedHeader({ alg: 'HS256' })
        .setSubject(user._id.toHexString())
        .setIssuedAt()
        .setExpirationTime('1h')
        .sign(secret);
    res.json({ token });
});

// Muestra el contenido de un token: GET /inspect?token=...
app.get('/inspect', (req, res) => {
    try {
        res.json(decodeJwt(String(req.query.token ?? '')));
    } catch {
        res.status(400).json({ error: 'Not a JWT' });
    }
});

// La página de administración. No se puede confiar en ninguna afirmación antes de verificar su firma.
app.get('/admin', (req, res) => {
    res.status(403).json({ error: 'Admins only' });
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});
quiz iconPonte a prueba

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