Skip to content

Autorización (Roles) con NestJS

Nos enfocaremos en la autorización, es decir, controlar el acceso a recursos según los roles de usuario.


User Role y JWT Payload

Añadiendo Roles a los Usuarios

Para implementar la autorización basada en roles, primero debemos asociar roles a nuestros usuarios.

Modificar la Entidad User

Agregamos una nueva propiedad role a nuestra entidad User.

users/entities/user.entity.ts

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column()
  password: string;

  @Column({ default: 'user' })
  role: string;
}
  • role: Indica el rol del usuario. Puede ser 'user', 'admin', etc.
  • default: 'user': Establece el rol por defecto como 'user'.

Incluyendo el Rol en el Payload del JWT

Cuando generamos el token JWT, podemos incluir información adicional en el payload.

Modificar el Método login en AuthService

auth/auth.service.ts

async login(loginAuthDto: LoginAuthDto) {
  // ... Validación de credenciales

  const payload = { username: user.username, sub: user.id, role: user.role };
  return {
    access_token: this.jwtService.sign(payload),
  };
}
  • role: user.role: Añadimos el rol del usuario al payload del token.

Actualizar la Estrategia JWT

Necesitamos asegurarnos de que el rol esté disponible en el contexto de la solicitud.

auth/jwt.strategy.ts

async validate(payload: any) {
  return { userId: payload.sub, username: payload.username, role: payload.role };
}
  • Ahora, el objeto req.user incluirá userId, username y role.

Verificación Manual de Rol Admin

Control de Acceso en el Controlador

Podemos verificar el rol del usuario directamente en el controlador para proteger ciertas rutas.

Ejemplo en CatsController

cats/cats.controller.ts

import { Controller, Get, UseGuards, Request, ForbiddenException } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

@Controller('cats')
export class CatsController {
  @UseGuards(JwtAuthGuard)
  @Get('admin')
  findAllAdmin(@Request() req) {
    if (req.user.role !== 'admin') {
      throw new ForbiddenException('Access denied');
    }
    // Código para usuarios con rol admin
    return this.catsService.findAll();
  }
}
  • Verificación del Rol: Comprobamos si req.user.role es 'admin'.
  • Manejo de Excepciones: Si el rol no es 'admin', lanzamos una ForbiddenException.

Limitaciones

  • Repetitivo: Tener que verificar manualmente el rol en cada ruta puede ser tedioso y propenso a errores.
  • Mejoras: Podemos crear un Guard personalizado para manejar esta lógica de manera centralizada.

Decorador Personalizado

Creando un Decorador Roles

Para simplificar la asignación de roles a las rutas, podemos crear un decorador personalizado.

Definir el Decorador Roles

auth/roles.decorator.ts

import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
  • SetMetadata: Función que asigna metadatos a una ruta.
  • ROLES_KEY: Clave utilizada para acceder a los metadatos.
  • Roles: Decorador que acepta una lista de roles.

Uso del Decorador en el Controlador

cats/cats.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from '../auth/roles.decorator';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

@Controller('cats')
export class CatsController {
  @UseGuards(JwtAuthGuard)
  @Roles('admin')
  @Get('admin')
  findAllAdmin() {
    // Código para usuarios con rol admin
    return this.catsService.findAll();
  }
}
  • @Roles('admin'): Especificamos que esta ruta requiere el rol 'admin'.

Necesidad de un Guard Personalizado

El decorador por sí solo no es suficiente; necesitamos un guard que interprete los metadatos y realice la verificación.


Roles Guard

Implementación del Guard de Roles

Crearemos un guard que verifique los roles antes de permitir el acceso a una ruta.

Definir el Guard RolesGuard

auth/roles.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true; // Si no hay roles definidos, permitir acceso
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.includes(user.role);
  }
}
  • Reflector: Servicio para acceder a los metadatos.
  • canActivate: Método que determina si el usuario tiene acceso.
  • requiredRoles: Obtenemos los roles requeridos de los metadatos.

Aplicación del Guard en el Controlador

cats/cats.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from '../auth/roles.decorator';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';

@Controller('cats')
@UseGuards(JwtAuthGuard, RolesGuard)
export class CatsController {
  @Roles('admin')
  @Get('admin')
  findAllAdmin() {
    return this.catsService.findAll();
  }

  @Roles('user', 'admin')
  @Get()
  findAll() {
    return this.catsService.findAll();
  }
}
  • @UseGuards(JwtAuthGuard, RolesGuard): Aplica ambos guards a todas las rutas del controlador.
  • @Roles(...): Especifica los roles permitidos para cada ruta.

Ventajas

  • Centralización: La lógica de autorización está centralizada en el guard.
  • Reutilización: Podemos aplicar el guard a múltiples controladores o rutas.

Enums en TypeScript

Uso de Enums para Roles

Para evitar errores tipográficos y facilitar la gestión de roles, utilizamos Enums en TypeScript.

Definir el Enum Role

auth/roles.enum.ts

export enum Role {
  User = 'user',
  Admin = 'admin',
}

Actualizar el Decorador y el Guard

Modificar el Decorador Roles

auth/roles.decorator.ts

import { SetMetadata } from '@nestjs/common';
import { Role } from './roles.enum';

export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

Modificar el Guard RolesGuard

El guard permanece igual, pero ahora trabaja con valores del enum.

Actualizar el Controlador

cats/cats.controller.ts

import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from '../auth/roles.decorator';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Role } from '../auth/roles.enum';

@Controller('cats')
@UseGuards(JwtAuthGuard, RolesGuard)
export class CatsController {
  @Roles(Role.Admin)
  @Get('admin')
  findAllAdmin() {
    return this.catsService.findAll();
  }

  @Roles(Role.User, Role.Admin)
  @Get()
  findAll() {
    return this.catsService.findAll();
  }
}
  • Uso del Enum: Utilizamos Role.Admin y Role.User en lugar de cadenas de texto.

Unir Varios Decoradores

Combinación de Decoradores

Para simplificar aún más, podemos crear un decorador que combine la aplicación de los guards y la asignación de roles.

Crear un Decorador Auth

auth/auth.decorator.ts

import { applyDecorators, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';

export function Auth(...roles: Role[]) {
  return applyDecorators(
    Roles(...roles),
    UseGuards(JwtAuthGuard, RolesGuard),
  );
}
  • applyDecorators: Permite combinar múltiples decoradores en uno solo.

Uso del Decorador Auth

cats/cats.controller.ts

import { Controller, Get } from '@nestjs/common';
import { Auth } from '../auth/auth.decorator';
import { Role } from '../auth/roles.enum';

@Controller('cats')
export class CatsController {
  @Auth(Role.Admin)
  @Get('admin')
  findAllAdmin() {
    return this.catsService.findAll();
  }

  @Auth(Role.User, Role.Admin)
  @Get()
  findAll() {
    return this.catsService.findAll();
  }
}
  • Ahora, con un solo decorador, aplicamos la autenticación y autorización.

Ocultar Contraseña

Excluir la Contraseña al Devolver el Usuario

Es una mala práctica devolver la contraseña (aunque esté hasheada) en las respuestas de la API.

Utilizar Exclude de class-transformer

users/entities/user.entity.ts

import { Exclude } from 'class-transformer';

@Entity()
export class User {
  // ...

  @Exclude()
  @Column()
  password: string;
}
  • @Exclude(): Indica que esta propiedad debe excluirse al transformar la entidad en JSON.

Aplicar Transformación en el Servicio

En los métodos que devuelven usuarios, utilizamos class-transformer para aplicar la exclusión.

users/users.service.ts

import { instanceToPlain } from 'class-transformer';

async findOne(username: string): Promise<User | undefined> {
  const user = await this.usersRepository.findOneBy({ username });
  return user ? instanceToPlain(user) : undefined;
}

Enum Roles en Entidad

Definir el Tipo de Datos de role en la Entidad

Podemos indicar que el campo role acepta solo valores definidos en el enum.

users/entities/user.entity.ts

import { Role } from '../../auth/roles.enum';

@Entity()
export class User {
  // ...

  @Column({
    type: 'enum',
    enum: Role,
    default: Role.User,
  })
  role: Role;
}
  • type: 'enum': Indica que el campo es de tipo enum en la base de datos.
  • enum: Role: Especifica el enum a utilizar.

Ventajas

  • Integridad de Datos: La base de datos solo aceptará valores definidos en el enum.
  • Consistencia: Facilita el mantenimiento y reduce errores.

Rol Admin con Privilegios

Implementación de Privilegios para el Rol Admin

Ahora, aseguraremos que solo los usuarios con rol admin puedan acceder a ciertos recursos, como la creación de nuevos roles o la gestión de usuarios.

Proteger Rutas con el Decorador Auth

users/users.controller.ts

import { Controller, Get, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { Auth } from '../auth/auth.decorator';
import { Role } from '../auth/roles.enum';

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Auth(Role.Admin)
  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Auth(Role.Admin)
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}
  • Solo Admin: Solo usuarios con rol admin pueden acceder a estas rutas.

Decorador Request User

Creando un Decorador User

Para acceder al usuario autenticado de manera más sencilla, podemos crear un decorador personalizado.

Definir el Decorador User

auth/user.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);
  • createParamDecorator: Crea un decorador de parámetros.
  • request.user: Accedemos al usuario autenticado.

Uso del Decorador en el Controlador

cats/cats.controller.ts

import { Controller, Get } from '@nestjs/common';
import { Auth } from '../auth/auth.decorator';
import { User } from '../auth/user.decorator';

@Controller('cats')
export class CatsController {
  @Auth()
  @Get('profile')
  getProfile(@User() user) {
    return user;
  }
}
  • @User() user: Inyecta el usuario autenticado en el parámetro user.

Breed Solo Rol Admin

Control de Acceso en Operaciones Específicas

Podemos restringir ciertas operaciones, como la creación de nuevas razas (breed), solo a usuarios con rol admin.

Definir el Módulo Breeds

Supongamos que tenemos un módulo Breeds para gestionar las razas de gatos.

breeds/breeds.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { Auth } from '../auth/auth.decorator';
import { Role } from '../auth/roles.enum';

@Controller('breeds')
export class BreedsController {
  @Auth(Role.Admin)
  @Post()
  create(@Body() createBreedDto: CreateBreedDto) {
    // Lógica para crear una nueva raza
  }
}
  • Restricción a Admin: Solo los administradores pueden crear nuevas razas.

Relación entre Cat y User

Asociar los Gatos (Cat) con el Usuario que los Crea

Queremos que cada gato registrado esté asociado al usuario que lo creó.

Modificar la Entidad Cat

cats/entities/cat.entity.ts

import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
import { User } from '../../users/entities/user.entity';

@Entity()
export class Cat {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  // Otras propiedades

  @ManyToOne(() => User, (user) => user.cats)
  owner: User;
}
  • @ManyToOne: Indica una relación de muchos a uno con User.
  • owner: Propiedad que referencia al usuario propietario del gato.

Modificar la Entidad User

users/entities/user.entity.ts

import { OneToMany } from 'typeorm';
import { Cat } from '../../cats/entities/cat.entity';

@Entity()
export class User {
  // ...

  @OneToMany(() => Cat, (cat) => cat.owner)
  cats: Cat[];
}
  • @OneToMany: Indica que un usuario puede tener muchos gatos.

Actualizar el Método create en CatsService

cats/cats.service.ts

async create(createCatDto: CreateCatDto, user: User): Promise<Cat> {
  const cat = this.catsRepository.create({ ...createCatDto, owner: user });
  return this.catsRepository.save(cat);
}
  • Asociamos el gato creado con el usuario que lo creó.

Actualizar el Controlador

cats/cats.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { Auth } from '../auth/auth.decorator';
import { User } from '../auth/user.decorator';

@Controller('cats')
export class CatsController {
  @Auth()
  @Post()
  create(@Body() createCatDto: CreateCatDto, @User() user: User) {
    return this.catsService.create(createCatDto, user);
  }
}
  • Inyectamos el usuario autenticado y lo pasamos al servicio.

Métodos findAll Cat

Filtrar Gatos por Usuario

Queremos que los usuarios solo puedan ver los gatos que ellos mismos crearon, a menos que sean administradores.

Modificar el Método findAll en CatsService

cats/cats.service.ts

async findAll(user: User): Promise<Cat[]> {
  if (user.role === Role.Admin) {
    return this.catsRepository.find({ relations: ['owner'] });
  }
  return this.catsRepository.find({
    where: { owner: { id: user.id } },
    relations: ['owner'],
  });
}
  • Admin: Si el usuario es admin, devuelve todos los gatos.
  • Usuario Regular: Devuelve solo los gatos que pertenecen al usuario.

Actualizar el Controlador

cats/cats.controller.ts

@Auth()
@Get()
findAll(@User() user: User) {
  return this.catsService.findAll(user);
}

Método findOne Cat

Restringir Acceso al Método findOne

Al obtener un gato por ID, debemos asegurarnos de que el usuario tenga permiso para verlo.

Modificar el Método findOne en CatsService

cats/cats.service.ts

async findOne(id: number, user: User): Promise<Cat> {
  const cat = await this.catsRepository.findOne({
    where: { id },
    relations: ['owner'],
  });
  if (!cat) {
    throw new NotFoundException('Cat not found');
  }
  if (user.role !== Role.Admin && cat.owner.id !== user.id) {
    throw new ForbiddenException('Access denied');
  }
  return cat;
}
  • Verificación de Propiedad: Si el usuario no es el propietario y no es admin, se deniega el acceso.

Actualizar el Controlador

cats/cats.controller.ts

@Auth()
@Get(':id')
findOne(@Param('id') id: number, @User() user: User) {
  return this.catsService.findOne(+id, user);
}

Actualizar Cat

Control de Acceso en Actualización

Solo el propietario o un administrador pueden actualizar un gato.

Modificar el Método update en CatsService

cats/cats.service.ts

async update(id: number, updateCatDto: UpdateCatDto, user: User): Promise<Cat> {
  const cat = await this.findOne(id, user);
  Object.assign(cat, updateCatDto);
  return this.catsRepository.save(cat);
}
  • Reutilizamos findOne: Garantiza que el usuario tiene permiso para actualizar.
  • Actualización: Modificamos los campos y guardamos.

Actualizar el Controlador

cats/cats.controller.ts

@Auth()
@Patch(':id')
update(@Param('id') id: number, @Body() updateCatDto: UpdateCatDto, @User() user: User) {
  return this.catsService.update(+id, updateCatDto, user);
}

Eliminar Cat

Control de Acceso en Eliminación

Solo el propietario o un administrador pueden eliminar un gato.

Modificar el Método remove en CatsService

cats/cats.service.ts

async remove(id: number, user: User): Promise<void> {
  const cat = await this.findOne(id, user);
  await this.catsRepository.remove(cat);
}
  • Verificación de Permisos: Utilizamos findOne para validar.
  • Eliminación: Eliminamos el registro de la base de datos.

Actualizar el Controlador

cats/cats.controller.ts

@Auth()
@Delete(':id')
remove(@Param('id') id: number, @User() user: User) {
  return this.catsService.remove(+id, user);
}