Prerrequisitos y Conceptos de Seguridad
La seguridad es uno de los pilares fundamentales en el desarrollo de arquitecturas backend modernas. En aplicaciones empresariales y APIs RESTful, no basta con implementar las funcionalidades de negocio; es obligatorio garantizar que cada petición provenga de una identidad legítima y que las operaciones ejecutadas respeten estrictamente las políticas de acceso del sistema.
En esta guía abordaremos los fundamentos conceptuales de la seguridad web, el almacenamiento protegido de credenciales mediante funciones de derivación criptográfica (hashing) y la distinción arquitectónica entre autenticación y autorización.
1. Prerrequisitos de la Arquitectura
Antes de implementar mecanismos de seguridad perimetral y protección de rutas en NestJS, el proyecto debe contar con una estructura de persistencia para usuarios, roles y permisos (también conocidos como authorities o privilegios).
Una arquitectura de control de acceso robusta se fundamenta en un modelo relacional normalizado:
En las etapas tempranas de un prototipo, es común que la creación de usuarios almacene contraseñas en texto plano:
async create(createUserDto: CreateUserDto) {
const role = await this.roleService.findByName(createUserDto.roleName);
if (!role) {
throw new NotFoundException('Role not found');
}
// Peligro: Almacenamiento en texto plano
const newUser = this.userRepository.create({
...createUserDto,
role,
});
return await this.userRepository.save(newUser);
}
Almacenar contraseñas en texto plano es una de las vulnerabilidades más severas según el estándar OWASP Top 10 (Cryptographic Failures). Si la base de datos se ve comprometida mediante inyección SQL, copias de seguridad expuestas o accesos no autorizados, todas las credenciales de los usuarios quedan expuestas de inmediato.
2. Hashing de Contraseñas con bcrypt
Para proteger las credenciales de los usuarios, se emplean funciones de hash criptográficas unidireccionales combinadas con técnicas de salting. A diferencia del cifrado simétrico o asimétrico (que puede ser revertido con una clave), un hash seguro no puede revertirse a su texto original.
¿Por qué utilizar bcrypt?
- Función Unidireccional: No existe algoritmo matemático viable para transformar el hash resultante nuevamente en la contraseña original.
- Salting Automático: Cada hash incluye un valor aleatorio (salt) generado dinámicamente. Esto previene ataques mediante Rainbow Tables (tablas de búsqueda precomputadas) y garantiza que dos usuarios con la misma contraseña tengan hashes almacenados completamente diferentes.
- Factor de Costo Adaptable (Salt Rounds): bcrypt está diseñado para ser deliberadamente lento en términos computacionales. El parámetro de costo define el número de iteraciones (, donde por ejemplo un valor de 10 representa rondas internas de derivación), mitigando de forma efectiva los ataques de fuerza bruta basados en aceleración por GPU.
Paso 1: Instalación de Dependencias
Ejecuta en la terminal de tu proyecto NestJS:
npm install bcrypt
npm install -D @types/bcrypt
Paso 2: Configuración de Variables de Entorno
Define el factor de costo en tu archivo .env. Un valor estándar recomendado en la industria para entornos de producción es 10:
SALT_ROUNDS=10
Cada incremento de 1 en SALT_ROUNDS duplica el tiempo de cómputo requerido para procesar el hash. Un valor entre 10 y 12 ofrece una excelente protección frente a fuerza bruta sin degradar la latencia del registro o inicio de sesión en tu servidor.
Paso 3: Modificación del Servicio de Usuarios
Inyecta ConfigService en tu servicio y procesa la contraseña antes de persistir la entidad:
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from './entities/user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { RolesService } from '../roles/roles.service';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private readonly roleService: RolesService,
private readonly configService: ConfigService,
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const role = await this.roleService.findByName(createUserDto.roleName);
if (!role) {
throw new NotFoundException(`El rol '${createUserDto.roleName}' no existe`);
}
if (!createUserDto.password) {
throw new BadRequestException('La contraseña es un campo obligatorio');
}
// Leemos las rondas de sal desde las variables de entorno
const saltRounds = parseInt(
this.configService.get<string>('SALT_ROUNDS') ?? '10',
10,
);
// Generamos el hash criptográfico
const passwordHash = await bcrypt.hash(createUserDto.password, saltRounds);
const newUser = this.userRepository.create({
email: createUserDto.email,
passwordHash,
role,
});
const savedUser = await this.userRepository.save(newUser);
// Omitimos el hash de la contraseña en la respuesta
const { passwordHash: _, ...userWithoutPassword } = savedUser;
return userWithoutPassword as User;
}
}
Si disponías de suites de pruebas unitarias o de integración para UsersService, es muy probable que fallen tras este cambio, ya que los mocks esperarán una llamada a bcrypt.hash y la propiedad almacenada será passwordHash en lugar de una propiedad en texto plano. Asegúrate de actualizar los mocks de pruebas inyectando ConfigService.
3. Autenticación vs. Autorización
Comprender la frontera conceptual y operativa entre autenticación y autorización es el requisito más importante para diseñar la seguridad en cualquier API.
Tabla Comparativa de Conceptos
| Dimensión | Autenticación (AuthN) | Autorización (AuthZ) |
|---|---|---|
| Pregunta Central | ¿Quién es el usuario? | ¿Qué permisos tiene el usuario sobre este recurso? |
| Objetivo | Verificar la identidad declarada por el cliente. | Regular las acciones permitidas según roles y privilegios. |
| Mecanismo Típico | Credenciales (email/password), JWT, OAuth2, SAML. | RBAC (roles), PBAC (permisos), Guards, Policies. |
| Momento de Ejecución | Al inicio de la sesión o al descodificar el token de la petición. | Inmediatamente antes de ejecutar el manejador del controlador. |
| Código HTTP de Fallo | 401 Unauthorized | 403 Forbidden |
| Estado en NestJS | Gestionado comúnmente por Passport y JwtStrategy. | Gestionado por Guards (CanActivate) y Reflector. |
Flujo de Vida de una Petición Protegida
401 Unauthorized: El cliente no se ha identificado o su token expiró/es inválido. El cliente puede reintentar la solicitud suministrando credenciales correctas.403 Forbidden: El servidor reconoció con éxito quién es el usuario, pero sus privilegios son insuficientes para ejecutar la acción solicitada. Reintentar con las mismas credenciales siempre resultará en el mismo rechazo.