Saltar al contenido principal

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:

Clic para ampliar

En las etapas tempranas de un prototipo, es común que la creación de usuarios almacene contraseñas en texto plano:

src/users/users.service.ts (Inseguro)
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);
}
Riesgo Crítico de Seguridad

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.

Clic para ampliar

¿Por qué utilizar bcrypt?

  1. Función Unidireccional: No existe algoritmo matemático viable para transformar el hash resultante nuevamente en la contraseña original.
  2. 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.
  3. Factor de Costo Adaptable (Salt Rounds): bcrypt está diseñado para ser deliberadamente lento en términos computacionales. El parámetro de costo kk define el número de iteraciones (2k2^k, donde por ejemplo un valor de 10 representa 210=10242^{10} = 1024 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:

Terminal
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:

.env
SALT_ROUNDS=10
Equilibrio entre Seguridad y Rendimiento

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:

src/users/users.service.ts
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;
}
}
Impacto en Pruebas Unitarias

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.

Fundamentos de Seguridad: Autenticación vs Autorización

Tabla Comparativa de Conceptos

DimensiónAutenticación (AuthN)Autorización (AuthZ)
Pregunta Central¿Quién es el usuario?¿Qué permisos tiene el usuario sobre este recurso?
ObjetivoVerificar la identidad declarada por el cliente.Regular las acciones permitidas según roles y privilegios.
Mecanismo TípicoCredenciales (email/password), JWT, OAuth2, SAML.RBAC (roles), PBAC (permisos), Guards, Policies.
Momento de EjecuciónAl 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 Fallo401 Unauthorized403 Forbidden
Estado en NestJSGestionado comúnmente por Passport y JwtStrategy.Gestionado por Guards (CanActivate) y Reflector.

Flujo de Vida de una Petición Protegida

Clic para ampliar
Diferencia Semántica: 401 vs 403
  • 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.

Cuestionario de Autoevaluación

Cargando cuestionario...