En esta página

Autenticación con JWT y Passport

15 min lectura TextoCap. 4 — Seguridad y comunicación

Autenticación en APIs REST

La autenticación es el proceso de verificar la identidad de quien hace una petición. En APIs REST stateless, el mecanismo más común es JWT (JSON Web Token): el servidor emite un token firmado al hacer login, y el cliente lo incluye en cada petición posterior en el header Authorization: Bearer <token>.

Instalando las dependencias

npm install @nestjs/passport @nestjs/jwt passport passport-local passport-jwt bcrypt
npm install --save-dev @types/passport-local @types/passport-jwt @types/bcrypt

La entidad Usuario con manejo seguro de contraseñas

// usuarios/entities/usuario.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity('usuarios')
export class Usuario {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ length: 100 })
  nombre: string;

  @Column({ unique: true, length: 255 })
  email: string;

  @Column({ name: 'password_hash', select: false }) // nunca se incluye por defecto
  passwordHash: string;

  @Column({ type: 'text', array: true, default: ['usuario'] })
  roles: string[];

  @Column({ default: true })
  activo: boolean;

  @CreateDateColumn()
  creadoEn: Date;
}

DTOs de autenticación

// auth/dto/registro.dto.ts
import { IsEmail, IsString, MinLength, MaxLength, Matches } from 'class-validator';
import { Transform } from 'class-transformer';

export class RegistroDto {
  @IsString()
  @MinLength(2)
  @MaxLength(100)
  nombre: string;

  @IsEmail()
  @Transform(({ value }: { value: string }) => value.toLowerCase().trim())
  email: string;

  @IsString()
  @MinLength(8)
  @Matches(/^(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])/, {
    message: 'La contraseña debe tener una mayúscula, un número y un símbolo especial',
  })
  password: string;
}

// auth/dto/login.dto.ts
export class LoginDto {
  @IsEmail()
  @Transform(({ value }: { value: string }) => value.toLowerCase())
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}

El controlador de autenticación

// auth/auth.controller.ts
import {
  Controller, Post, Body, UseGuards, Get, HttpCode, HttpStatus
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { RegistroDto } from './dto/registro.dto';
import { Public } from './decorators/public.decorator';
import { UsuarioActual } from './decorators/usuario-actual.decorator';
import { JwtPayload } from './strategies/jwt.strategy';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('registro')
  @Public()
  registro(@Body() registroDto: RegistroDto) {
    return this.authService.registro(registroDto);
  }

  @Post('login')
  @Public()
  @HttpCode(HttpStatus.OK)
  @UseGuards(AuthGuard('local')) // ejecuta LocalStrategy.validate()
  login(@UsuarioActual() usuario: UsuarioPayload) {
    return this.authService.login(usuario);
  }

  @Get('perfil')
  // El JwtAuthGuard global protege esta ruta automáticamente
  getPerfil(@UsuarioActual() usuario: JwtPayload) {
    return usuario;
  }

  @Post('logout')
  @HttpCode(HttpStatus.NO_CONTENT)
  logout() {
    // En JWT stateless no hay logout real en el servidor
    // El cliente debe eliminar el token del almacenamiento local
    // Si tienes refresh tokens, invalida el refresh token aquí
    return;
  }
}

Implementando refresh tokens

Para un sistema de autenticación más robusto con refresh tokens:

// En la entidad Usuario, añade:
@Column({ name: 'refresh_token_hash', nullable: true, select: false })
refreshTokenHash: string | null;

// En AuthService:
async generarTokens(usuario: Usuario): Promise<{ accessToken: string; refreshToken: string }> {
  const payload: JwtPayload = {
    sub: usuario.id,
    email: usuario.email,
    roles: usuario.roles,
  };

  const [accessToken, refreshToken] = await Promise.all([
    this.jwtService.signAsync(payload, { expiresIn: '15m' }),
    this.jwtService.signAsync(
      { sub: usuario.id },
      { secret: process.env['JWT_REFRESH_SECRET'], expiresIn: '7d' }
    ),
  ]);

  // Guardar hash del refresh token en la BD
  const refreshTokenHash = await bcrypt.hash(refreshToken, 10);
  await this.usuarioRepo.update(usuario.id, { refreshTokenHash });

  return { accessToken, refreshToken };
}

async refrescarToken(usuarioId: string, refreshToken: string): Promise<{ accessToken: string }> {
  const usuario = await this.usuarioRepo.findOne({
    where: { id: usuarioId },
    select: ['id', 'email', 'roles', 'refreshTokenHash', 'activo'],
  });

  if (!usuario || !usuario.activo || !usuario.refreshTokenHash) {
    throw new UnauthorizedException('Sesión inválida');
  }

  const tokenValido = await bcrypt.compare(refreshToken, usuario.refreshTokenHash);
  if (!tokenValido) {
    // Posible robo de token — invalida todos los tokens del usuario
    await this.usuarioRepo.update(usuarioId, { refreshTokenHash: null });
    throw new UnauthorizedException('Refresh token inválido');
  }

  const accessToken = this.generarToken(usuario);
  return { accessToken };
}

Protección de rutas con el patrón global

La forma más limpia de proteger toda la aplicación es registrar el JwtAuthGuard globalmente y usar el decorador @Public() para las excepciones:

// app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/guards/jwt-auth.guard';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard,
    },
  ],
})
export class AppModule {}

Variables de entorno necesarias

Crea un archivo .env con:

DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASS=secreto
DB_NAME=mi_api
JWT_SECRET=tu_secret_muy_largo_y_aleatorio_aqui
JWT_EXPIRES_IN=15m
JWT_REFRESH_SECRET=otro_secret_diferente_para_refresh
NODE_ENV=development

Con el sistema de autenticación JWT implementado, tu API tiene una capa de seguridad sólida. En la próxima lección aprenderemos sobre interceptores y middleware —herramientas para transformar respuestas, agregar logging y gestionar el ciclo completo de la petición.

Nunca guardes el JWT secret en el código fuente
El secret del JWT debe ser una cadena larga y aleatoria (mínimo 256 bits) almacenada únicamente en variables de entorno. En producción, usa un gestor de secretos como AWS Secrets Manager, GCP Secret Manager o HashiCorp Vault. Puedes generarlo con: `node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"`
Implementa refresh tokens para UX mejorada
Los access tokens de corta duración (15 minutos) son más seguros, pero requieren refresh tokens para no forzar al usuario a hacer login frecuentemente. Guarda el refresh token en una cookie HttpOnly (no en localStorage) y crea un endpoint `/auth/refresh` que verifique el refresh token y emita un nuevo access token.