Control de Roles y Permisos con JSON Web Tokens en TypeScript

Clase 15 de 22Curso de NestJS: Autenticación con Passport y JWT

Resumen

¿Cómo controlar los roles en una aplicación?

En la gestión de aplicaciones, el control de roles es fundamental para garantizar la seguridad y la personalización de la experiencia del usuario. Aquí exploraremos cómo implementar un sistema de roles y permisos utilizando JSON Web Tokens (JWT) y decoradores en aplicaciones TypeScript. Este enfoque permite definir qué operaciones puede realizar un usuario dependiendo de su rol.

¿Cómo empezamos a definir roles?

Lo primero es definir claramente los roles en la aplicación. Para esto, utilizamos las capacidades de TypeScript, como los enumerators o "enums", los cuales son conjuntos de valores predefinidos.

// models/rules.models.ts
enum Role {
  Customer = 'customer',
  Admin = 'admin',
}

Con estos enums, podemos evitar errores comunes como mal escribir el rol y garantizar que solo los roles predefinidos sean utilizados.

¿Cómo se implementa un decorador para roles?

Después de definir los roles, podemos crear un decorador que nos permita asignar roles específicos a las rutas de nuestra aplicación. Esto se hace añadiendo metadata al endpoint.

// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from '../models/rules.models';

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

Mediante este decorador, podemos definir qué roles tienen acceso a cada endpoint de la siguiente manera:

import { Controller, Post, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
import { Roles } from './decorators/roles.decorator';
import { Role } from './models/rules.models';

@Controller('api')
@UseGuards(JwtAuthGuard, RolesGuard)
export class ApiController {
  @Post('create')
  @Roles(Role.Admin)
  create() {
    // lógica del endpoint
  }
}

¿Cómo funciona el guardián de roles?

El guardián actúa como un guardián de acceso, verificando si el usuario tiene los permisos adecuados para realizar una acción específica. Este proceso incluye verificar si el rol del usuario coincide con el permitido en el decorador.

Para obtener y verificar la metadata, utilizamos el reflección:

// guards/roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '../models/rules.models';

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.role?.includes(role));
  }
}

¿Cómo se asegura el acceso adecuado en endpoints?

Aquí es donde las piezas se unen. Al definir nuestro decorador y guardián de roles, los configuramos en los controladores. Esto permite que solo los usuarios con el rol correcto accedan a determinados endpoints.

Por ejemplo, al utilizar Postman o Insomnia para probar el acceso, podrías realizar una solicitud para crear un producto. Si el usuario es un administrador, la operación se permite; de lo contrario, se deniega.

¿Qué pasa con los endpoints sin roles definidos?

Si hay endpoints que no requieren verificación de roles, simplemente establecemos una condición para dejarlos pasar si no hay metadata de roles:

canActivate(context: ExecutionContext): boolean {
  const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
    context.getHandler(),
    context.getClass(),
  ]);
  if (!requiredRoles) {
    return true;
  }
  // Procedemos con la lógica de verificación de roles
}

El manejo de roles es clave en aplicaciones modernas para mantener la seguridad y personalización de usuario. Si tu aplicación maneja roles dinámicos, podrías ampliar esta lógica integrando consultas a una base de datos para roles y permisos. Siempre es genial ver cómo las diferentes piezas del código trabajan en conjunto para crear soluciones eficientes y robustas, así que sigue perfeccionando tus habilidades y explorando nuevas posibilidades en el mundo del desarrollo.