GL
Seguridad 20 de mayo de 2025 15 min de lectura Por Gustavo Leyva

Autenticación segura con JWT

Implementación empresarial de autenticación con JSON Web Tokens, incluyendo refresh tokens, revocación y mejores prácticas de seguridad.

#JWT #Seguridad #Autenticación #Backend #OAuth
A

¿Qué es JWT?

JSON Web Token (JWT) es un estándar abierto (RFC 7519) que define una forma compacta y autónoma de transmitir información de forma segura entre partes como un objeto JSON. Empresas como Auth0, Firebase, AWS Cognito y Okta utilizan JWT como base de sus sistemas de autenticación.

Estructura de un JWT

Un JWT consta de tres partes separadas por puntos:

header.payload.signature

1. Header

{
  "alg": "HS256",
  "typ": "JWT"
}

2. Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "role": "admin"
}

3. Signature

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

Implementación en Node.js

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Generar access token (corta duración)
function generateAccessToken(user) {
  const payload = {
    id: user.id,
    email: user.email,
    role: user.role
  };
  
  return jwt.sign(payload, process.env.JWT_SECRET, {
    expiresIn: '15m',
    issuer: 'mi-app',
    audience: 'mi-app-users'
  });
}

// Generar refresh token (larga duración)
function generateRefreshToken(user) {
  const payload = {
    id: user.id,
    tokenVersion: user.tokenVersion // Para revocación
  };
  
  return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
    expiresIn: '7d',
    issuer: 'mi-app'
  });
}

// Verificar token
function verifyToken(token, secret) {
  try {
    return jwt.verify(token, secret, {
      issuer: 'mi-app',
      audience: 'mi-app-users'
    });
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      throw new Error('Token expirado');
    }
    throw new Error('Token inválido');
  }
}

// Login endpoint
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  
  // Buscar usuario
  const user = await User.findOne({ email });
  if (!user) {
    return res.status(401).json({ error: 'Credenciales inválidas' });
  }
  
  // Verificar contraseña
  const validPassword = await bcrypt.compare(password, user.password);
  if (!validPassword) {
    return res.status(401).json({ error: 'Credenciales inválidas' });
  }
  
  // Generar tokens
  const accessToken = generateAccessToken(user);
  const refreshToken = generateRefreshToken(user);
  
  // Guardar refresh token en base de datos
  await RefreshToken.create({
    userId: user.id,
    token: refreshToken,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  });
  
  // Enviar refresh token como httpOnly cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 días
  });
  
  res.json({ accessToken });
});

Middleware de autenticación

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).json({ error: 'Token no proporcionado' });
  }
  
  const token = authHeader.split(' ')[1]; // Bearer TOKEN
  
  try {
    const decoded = verifyToken(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: error.message });
  }
}

// Middleware de autorización por rol
function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'No autenticado' });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'No autorizado' });
    }
    
    next();
  };
}

// Uso
app.get('/api/profile', authMiddleware, (req, res) => {
  res.json({ user: req.user });
});

app.delete('/api/users/:id', authMiddleware, authorize('admin'), (req, res) => {
  // Solo admins pueden eliminar usuarios
});

Refresh Token Rotation (Patrón empresarial)

Este patrón es usado por Auth0 y OAuth 2.0 para máxima seguridad.

app.post('/api/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token no proporcionado' });
  }
  
  try {
    // Verificar refresh token
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
    
    // Buscar token en base de datos
    const storedToken = await RefreshToken.findOne({
      userId: decoded.id,
      token: refreshToken
    });
    
    if (!storedToken) {
      // Posible ataque de reutilización de token
      await RefreshToken.deleteMany({ userId: decoded.id });
      return res.status(401).json({ error: 'Token inválido' });
    }
    
    // Buscar usuario
    const user = await User.findById(decoded.id);
    
    // Verificar versión del token (para revocación)
    if (user.tokenVersion !== decoded.tokenVersion) {
      return res.status(401).json({ error: 'Token revocado' });
    }
    
    // Eliminar refresh token antiguo
    await RefreshToken.deleteOne({ _id: storedToken._id });
    
    // Generar nuevos tokens (rotation)
    const newAccessToken = generateAccessToken(user);
    const newRefreshToken = generateRefreshToken(user);
    
    // Guardar nuevo refresh token
    await RefreshToken.create({
      userId: user.id,
      token: newRefreshToken,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    });
    
    // Actualizar cookie
    res.cookie('refreshToken', newRefreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000
    });
    
    res.json({ accessToken: newAccessToken });
    
  } catch (error) {
    return res.status(401).json({ error: 'Token inválido' });
  }
});

Revocación de tokens

// Logout: revocar refresh token
app.post('/api/logout', authMiddleware, async (req, res) => {
  const { refreshToken } = req.cookies;
  
  // Eliminar refresh token de la base de datos
  await RefreshToken.deleteOne({ token: refreshToken });
  
  // Limpiar cookie
  res.clearCookie('refreshToken');
  
  res.json({ message: 'Logout exitoso' });
});

// Logout de todos los dispositivos
app.post('/api/logout-all', authMiddleware, async (req, res) => {
  // Incrementar versión del token para invalidar todos los tokens existentes
  await User.findByIdAndUpdate(req.user.id, {
    $inc: { tokenVersion: 1 }
  });
  
  // Eliminar todos los refresh tokens del usuario
  await RefreshToken.deleteMany({ userId: req.user.id });
  
  res.json({ message: 'Sesiones cerradas en todos los dispositivos' });
});

Vulnerabilidades de seguridad y prevención

1. Algoritmo “none” (CVE-2015-9235)

// ❌ VULNERABLE: Acepta cualquier algoritmo
jwt.verify(token, secret);

// ✅ SEGURO: Especifica algoritmo permitido
jwt.verify(token, secret, { algorithms: ['HS256'] });

2. Secret débil

// ❌ MALO: Secret predecible
const secret = 'mi-secreto-123';

// ✅ BUENO: Secret fuerte y aleatorio
const secret = crypto.randomBytes(64).toString('hex');
// Almacenar en variable de entorno

3. Información sensible en payload

// ❌ NUNCA incluyas información sensible
const payload = {
  id: user.id,
  password: user.password, // ¡NO!
  ssn: user.ssn // ¡NO!
};

// ✅ Solo información no sensible
const payload = {
  id: user.id,
  email: user.email,
  role: user.role
};

Mejores prácticas empresariales

1. Usar HTTPS siempre

Siempre transmite tokens sobre HTTPS para prevenir interceptación (man-in-the-middle attacks).

2. Tiempo de expiración corto para access tokens

// Access token: 15 minutos
jwt.sign(payload, secret, { expiresIn: '15m' });

// Refresh token: 7 días
jwt.sign(payload, refreshSecret, { expiresIn: '7d' });

3. Almacenamiento seguro

Backend:

  • Variables de entorno para secrets
  • Nunca en código fuente
  • Usar servicios como AWS Secrets Manager, HashiCorp Vault

Frontend:

  • Access token: memoria (variable JavaScript)
  • Refresh token: httpOnly cookie (no accesible desde JavaScript)
  • Nunca en localStorage (vulnerable a XSS)

4. Rate limiting

const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // 5 intentos
  message: 'Demasiados intentos de login, intenta más tarde'
});

app.post('/api/login', loginLimiter, async (req, res) => {
  // ...
});

5. Validación de claims

function verifyToken(token) {
  const decoded = jwt.verify(token, secret, {
    algorithms: ['HS256'],
    issuer: 'mi-app',
    audience: 'mi-app-users'
  });
  
  // Validar claims adicionales
  if (!decoded.exp || decoded.exp < Date.now() / 1000) {
    throw new Error('Token expirado');
  }
  
  if (!decoded.role || !['user', 'admin'].includes(decoded.role)) {
    throw new Error('Rol inválido');
  }
  
  return decoded;
}

Integración con OAuth 2.0

// Ejemplo con Google OAuth
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);

app.post('/api/auth/google', async (req, res) => {
  const { token } = req.body;
  
  try {
    // Verificar token de Google
    const ticket = await client.verifyIdToken({
      idToken: token,
      audience: process.env.GOOGLE_CLIENT_ID
    });
    
    const payload = ticket.getPayload();
    
    // Buscar o crear usuario
    let user = await User.findOne({ email: payload.email });
    if (!user) {
      user = await User.create({
        email: payload.email,
        name: payload.name,
        googleId: payload.sub,
        emailVerified: payload.email_verified
      });
    }
    
    // Generar nuestros propios tokens JWT
    const accessToken = generateAccessToken(user);
    const refreshToken = generateRefreshToken(user);
    
    res.json({ accessToken, refreshToken });
    
  } catch (error) {
    res.status(401).json({ error: 'Token de Google inválido' });
  }
});

Conclusión

JWT es la solución estándar para autenticación en aplicaciones modernas, utilizada por empresas como Auth0, Firebase, AWS Cognito y Okta. Siguiendo estas mejores prácticas empresariales, puedes implementar un sistema de autenticación:

  • Seguro: Con refresh token rotation y revocación
  • Escalable: Stateless, ideal para microservicios
  • Flexible: Compatible con OAuth 2.0 y SSO
  • Robusto: Protegido contra vulnerabilidades comunes

La clave está en nunca comprometer la seguridad por conveniencia: usa HTTPS, secrets fuertes, tokens de corta duración y almacenamiento seguro.

GL

Gustavo Leyva

Desarrollador de Software

← Volver al blog