Volver a la lista

Implementación de autenticación multiusuario

Aprenda paso a paso cómo implementar el registro de usuarios, el hashing de contraseñas (bcrypt) y el control de acceso basado en roles.

Intermedio
|
12min
|
Verificado (2026-07)
múltiples usuariosregistro de usuariobcrypthashing de contraseñasimplementación de autenticación
Progreso0/55 (0%)

Implementación de la autenticación multiusuario

Al finalizar este tema

Podrás aplicar un hash seguro a las contraseñas durante el registro, comprender los principios de bcrypt e implementar el control de acceso basado en roles (RBAC).


Por qué no se deben almacenar las contraseñas en texto plano

javascript
// NEVER do this
const users = [
  { username: 'alice', password: 'mySecret123' }
];

Si se filtra la base de datos, las contraseñas de todos los usuarios quedarán expuestas. El hackeo de LinkedIn en 2012 y el de Adobe en 2013 mostraron que cientos de millones de contraseñas se almacenaban en texto plano (o con hashes débiles).

Nunca almacene contraseñas en texto plano. Almacénelas mediante hashing.


¿Qué es el hashing?

El hashing es una transformación unidireccional. Es posible pasar del original al hash, pero no del hash al original.

text
"mySecret123" → hashing → "$2b$10$X7kG..."  (posible)
"$2b$10$X7kG..." → ???  → "mySecret123"   (imposible)

Al iniciar sesión, se aplica un hash a la contraseña ingresada y se compara con el hash almacenado.

text
Entrada del usuario: "mySecret123"
Hash almacenado: "$2b$10$X7kG..."

"mySecret123" → hashing → "$2b$10$X7kG..."  (coincidencia → autenticación exitosa)
"wrongPass"   → hashing → "$2b$10$Qm1Y..."  (no coincidencia → autenticación fallida)

bcrypt: el estándar para el hashing de contraseñas

bash
npm install bcrypt

Razones por las que bcrypt es más adecuado para contraseñas que las funciones hash genéricas como SHA-256:

  1. Diseñado para ser lento: Evita que un atacante intente miles de millones de hashes por segundo.
  2. Incluye salt automáticamente: Genera un hash diferente cada vez, incluso para la misma contraseña.
  3. Costo ajustable: Permite ajustar el equilibrio entre velocidad y seguridad mediante el valor de rounds.
javascript
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;

// hashing (durante el registro)
const hash = await bcrypt.hash('mySecret123', SALT_ROUNDS);
console.log(hash);
// $2b$10$X7kG.fDlUqVnPq5RvWEYq.ZM3Y4Fs8qD1vSrKLhwEcFjnp2fW9Xm.

// comparación (durante el inicio de sesión)
const isMatch = await bcrypt.compare('mySecret123', hash);
console.log(isMatch);  // true

const isWrong = await bcrypt.compare('wrongPass', hash);
console.log(isWrong);  // false

SALT_ROUNDS = 10 indica que la operación se repite 210=1,0242^{10} = 1,024 veces. Un valor más alto es más seguro, pero más lento. El rango recomendado actualmente es de 10 a 12.


Implementación del registro de usuarios

javascript
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());

const users = [];  // en la práctica se usa una base de datos

app.post('/register', async (req, res) => {
  const { username, password, displayName } = req.body;

  // validación de entrada
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password required' });
  }
  if (password.length < 8) {
    return res.status(400).json({ error: 'Password must be at least 8 characters' });
  }

  // Verificación de duplicados
  if (users.find(u => u.username === username)) {
    return res.status(409).json({ error: 'Username already exists' });
  }

  // Hashing de contraseña
  const hashedPassword = await bcrypt.hash(password, 10);

  // Guardar
  const newUser = {
    id: users.length + 1,
    username,
    password: hashedPassword,
    displayName: displayName || username,
    role: 'user',
    createdAt: new Date().toISOString()
  };
  users.push(newUser);

  res.status(201).json({
    message: 'Registration successful',
    user: { id: newUser.id, username: newUser.username }
  });
});

Nunca incluya contraseñas (incluyendo hashes) en las respuestas.


Integración con Passport

javascript
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy(
  async (username, password, done) => {
    const user = users.find(u => u.username === username);
    if (!user) {
      return done(null, false, { message: 'User not found' });
    }

    // Comparación con bcrypt
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) {
      return done(null, false, { message: 'Wrong password' });
    }

    return done(null, user);
  }
));

passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => {
  const user = users.find(u => u.id === id);
  done(null, user);
});

La única diferencia con la Estrategia del tema anterior es que se utiliza bcrypt.compare() en lugar de user.password !== password.


Control de acceso basado en roles (RBAC)

javascript
function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.isAuthenticated()) {
      return res.status(401).json({ error: 'Login required' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// Todos los usuarios logueados
app.get('/profile', requireRole('user', 'admin'), (req, res) => {
  res.json({ user: req.user.displayName });
});

// Solo administradores
app.get('/admin/users', requireRole('admin'), (req, res) => {
  const safeUsers = users.map(u => ({
    id: u.id, username: u.username, role: u.role
  }));
  res.json(safeUsers);
});

// Administrador — Cambio de rol
app.patch('/admin/users/:id/role', requireRole('admin'), (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'User not found' });

  user.role = req.body.role;
  res.json({ message: `${user.username} role changed to ${user.role}` });
});

requireRole(...roles) es una función de orden superior (una función que devuelve otra función). Recibe como argumento el rol permitido y crea un middleware que devuelve un error 403 si el rol no coincide.


Lista de verificación de seguridad

ElementoDescripción
Hash de contraseñasbcrypt, SALT_ROUNDS 10+
Longitud mínima8 caracteres o más (recomendación de NIST)
No incluir el hash en la respuestaNunca incluir el campo "password" en las respuestas de la API
Generalización de mensajes de error"Nombre de usuario o contraseña incorrectos" (no indicar cuál de los dos es el error)
Limitación de tasa (Rate limiting)Limitar el número de intentos de inicio de sesión (para prevenir ataques de fuerza bruta)
HTTPSPara evitar que la contraseña se transmita en texto plano por la red

Razón para generalizar los mensajes de error: si se indica "El nombre de usuario no existe", un atacante podría identificar cuentas existentes. Esto se conoce como enumeración de usuarios (User Enumeration).


Patrón práctico — Eliminación de cuenta

javascript
app.delete('/account', requireRole('user', 'admin'), async (req, res) => {
  const valid = await bcrypt.compare(req.body.password, req.user.password);
  if (!valid) {
    return res.status(401).json({ error: 'Password confirmation failed' });
  }

  const index = users.findIndex(u => u.id === req.user.id);
  users.splice(index, 1);

  req.logout((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.json({ message: 'Account deleted' });
  });
});

La eliminación de cuentas es una operación crítica, por lo que se requiere una reconfirmación de la contraseña. Tras la eliminación, es imprescindible limpiar la sesión mediante req.logout().


Resumen clave

ConceptoResumen
HashingTransformación unidireccional. De original a hash es posible; de hash a original no.
bcryptHash específico para contraseñas. Es lento e incluye salt.
SaltValor aleatorio que genera hashes distintos incluso para la misma contraseña.
RBACControl de acceso basado en roles.
bcrypt.hash()Durante el registro: contraseña \rightarrow hash.
bcrypt.compare()Durante el inicio de sesión: comparación entre la entrada y el hash almacenado.

La autenticación va más allá de verificar si "la contraseña es correcta"; la clave reside en "cómo se gestiona la contraseña de forma segura". El almacenamiento en texto plano es un riesgo, los hashes débiles (MD5, SHA-1) son inútiles y bcrypt es el estándar actual.


💬 Preguntas y comentarios

0 comentarios

Puedes publicar sin iniciar sesión. Los comentarios de invitados no pueden editarse ni eliminarse después.

0/2000

Cargando...