Skip to content

Autorización basa en Roles con Guard en Nest.js

Autenticación: Es el proceso de verificar la identidad del usuario. Se asegura de que el usuario sea quien dice ser, generalmente a través de credenciales como nombre de usuario y contraseña. La autenticación se utiliza para conceder acceso a usuarios legítimos y evitar el acceso no autorizado.

Autorización: Es el proceso de determinar qué acciones o recursos un usuario autenticado tiene permitido acceder o realizar. Una vez que un usuario ha sido autenticado, la autorización se encarga de verificar si ese usuario tiene los permisos adecuados para realizar ciertas acciones o acceder a ciertas partes de la aplicación.

Código Github

Ayúdame a seguir creando contenido 😍

Tienes varias jugosas alternativas:

Muchas gracias por su tremendo apoyo 😊

Guard

En Nest.js, los Guards (guardias) son un mecanismo que te permite controlar el acceso a rutas o puntos finales (endpoints) de tu aplicación. Los Guards se ejecutan antes de que se procese una solicitud a una ruta determinada y pueden permitir o denegar el acceso en función de ciertas condiciones o lógica de negocio.

Agregar role en JWT

WARNING

En la entity del user tiene que existir:

ts
@Column({ default: 'user' })
role: string;

Tambien se podría trabajar como un array de roles.

src\auth\auth.service.ts

ts
const payload = { email: user.email, role: user.role };

RequestWithUser

ts
interface RequestWithUser extends Request {
  user: { email: string; role: string };
}

...

@Get('profile')
@UseGuards(AuthGuard)
profile(
  @Request()
  req: RequestWithUser,
) {
  return req.user;
}

@SetMetadata

El decorador @SetMetadata en Nest.js se utiliza para asignar metadatos personalizados a los controladores, controladores de métodos o parámetros de un método.

Los metadatos son información adicional que puedes adjuntar a estos elementos para usarlos en diversos propósitos, como la configuración de autorización, validación, serialización, etc.

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

// Uso del decorador
@SetMetadata(key, value)
  • key: Es el nombre de la clave bajo la cual se almacenarán los metadatos. Puedes elegir cualquier string como clave para identificar los metadatos.
  • value: Es el valor que deseas asignar a la clave como metadato. Puede ser cualquier tipo de dato válido en TypeScript (números, strings, objetos, etc.).

Decorador personalizado

src\auth\decorators\roles.decorator.ts

ts
import { SetMetadata } from "@nestjs/common";

export const Roles = (role) => SetMetadata("roles", role);
ts
@Get('profile')
@Roles('user')
@UseGuards(AuthGuard)
profile(
  @Request()
  req: RequestWithUser,
) {
  return req.user;
}

Reflector y Guard

Para acceder a los metadatos establecidos con el decorador @SetMetadata en el controlador o sus métodos, puedes hacer uso del módulo Reflector proporcionado por Nest.js.

El Reflector te permitirá leer los metadatos adjuntados a los controladores o controladores de métodos en tiempo de ejecución.

nest g guard auth/guard/roles --flat ---no-spec

src\auth\guard\roles.guard.ts

ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride("roles", [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return user.role === requiredRoles;
  }
}

La función getAllAndOverride del Reflector se utiliza para obtener los metadatos de un controlador o controlador de método, pero con la capacidad de sobrescribir esos metadatos si existen en varios niveles de la jerarquía.

El método getAllAndOverride toma dos argumentos:

  1. metadataKey: Este es el nombre de la clave bajo la cual se almacenan los metadatos en el controlador o controlador de método. En este caso, metadataKey es 'roles'.
  2. metatype: Este es un arreglo que contiene los elementos de los cuales se obtendrán los metadatos. En este caso, se pasa un arreglo con dos elementos:
  • context.getHandler(): da como resultado la extracción de los metadatos para el controlador de ruta procesado actualmente.
  • context.getClass(): metadatos del controlador de clase.

La función getAllAndOverride buscará los metadatos en el primer elemento del arreglo (context.getHandler(), que representa los metadatos del controlador de método) y si no los encuentra, buscará en el segundo elemento (context.getClass(), que representa los metadatos del controlador de clase). Si encuentra metadatos en cualquiera de estos elementos, los devolverá y los almacenará en la variable requiredRoles.

ts
@Get('profile')
@Roles('user')
@UseGuards(AuthGuard, RolesGuard)
profile(
  @Request()
  req: RequestWithUser,
) {
  return req.user;
}

Optimizando

src\auth\enums\role.enum.ts

ts
export enum Role {
  ADMIN = "admin",
  USER = "user",
}
ts
@Get('profile')
@Roles(Role.USER)
@UseGuards(AuthGuard, RolesGuard)
profile(
  @Request()
  req: RequestWithUser,
) {
  return req.user;
}

src\auth\decorators\roles.decorator.ts

ts
import { SetMetadata } from "@nestjs/common";
import { Role } from "../enums/role.enum";

export const ROLES_KEY = "roles";
export const Roles = (role: Role) => SetMetadata(ROLES_KEY, role);

src\auth\guard\roles.guard.ts

ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { ROLES_KEY } from "../decorators/roles.decorator";
import { Role } from "../enums/role.enum";

@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(),
    ]);

    console.log(requiredRoles);

    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();

    return user.role === requiredRoles;
  }
}

Decorator composition

src\auth\decorators\auth.decorators.ts

ts
import { applyDecorators, UseGuards } from "@nestjs/common";
import { Role } from "../enums/role.enum";
import { AuthGuard } from "../guard/auth.guard";
import { RolesGuard } from "../guard/roles.guard";
import { Roles } from "./roles.decorator";

export function Auth(role: Role) {
  return applyDecorators(Roles(role), UseGuards(AuthGuard, RolesGuard));
}
ts
@Get('profile2')
@Auth(Role.ADMIN)
profile2(
  @Request()
  req: RequestWithUser,
) {
  return req.user;
}

Parte 2 (continuación)

En esta segunda parte realizaremos refactorizaciones y mejoras en el código de la aplicación. Además relacionaremos la entidad Cat con la entidad User para que cada usuario pueda tener sus propios gatos.

Global Hidden password

src\users\entities\user.entity.ts

ts
@Column({ nullable: false, select: false })
password: string;

src\users\users.service.ts

ts
findOneByEmailWithPassword(email: string) {
  return this.userRepository.findOne({
    where: { email },
    select: ['id', 'name', 'email', 'password', 'role'],
  });
}

src\auth\auth.service.ts

ts
async login({ email, password }: LoginDto) {
    const user = await this.usersService.findOneByEmailWithPassword(email);
    ...
}

Admin role guard

src\auth\guard\roles.guard.ts

ts
// si es administrador lo dejamos hacer lo que sea :D
if (user.role === Role.ADMIN) return true;

Role enum (common)

src\common\enums\role.enum.ts

ts
export enum Role {
  USER = "user",
  ADMIN = "admin",
}

User entity (users)

src\users\entities\user.entity.ts

ts
// import { Role } from '../../common/enums/role.enum';
@Column({ type: 'enum', enum: Role, default: Role.USER })
role: Role;

users controllers

src\users\users.controller.ts

ts
@Controller('users')
@Auth(Role.ADMIN)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
}

Active user decorator and interfaces

Nuestro objetivo es llegar a un decorador personalizado que nos permita obtener el usuario activo en el controlador.

ts
@Get()
findAll(
  @ActiveUser()
  user: ActiveUserInterface,
) {
  return this.catsService.findAll(user);
}
  • custom-decorators
  • createParamDecorator: sirve para inyectar datos adicionales en los parámetros de los controladores.
  • ExecutionContext: En el caso de las aplicaciones basadas en Express, el contexto de ejecución es una instancia de la clase http.Request.

src\common\decorators\active-user.decorator.ts

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

export const ActiveUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  }
);

src\common\interfaces\active-user.interface.ts

ts
export interface ActiveUserInterface {
  email: string;
  role: string;
}

src\cats\cats.controller.ts

ts
@Auth(Role.USER)
@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get()
  findAll(
    @ActiveUser()
    user: ActiveUserInterface,
  ) {
    console.log(user);
    return this.catsService.findAll();
  }
}

users OneToMany cats

JoinColumn

  • JoinColumn: no solo define qué lado de la relación contiene la columna de combinación con una clave externa, sino que también le permite personalizar el nombre de la columna de combinación y el nombre de la columna de referencia.
  • Cuando configuramos @JoinColumn, crea automáticamente una columna en la base de datos llamada propertyName + referencedColumnName.
  • Para no instanciar al usuario en el cat, se puede usar @Column userEmail, además esto solo no carga todo el usuario en el cat, solo el email. Más info aquí

TIP

@OneToMany no puede existir sin @ManyToOne: @ManyToOne es el lado propietario de la relación, por lo que es responsable de actualizar la clave externa en la base de datos.

Sin embargo, no se requiere lo contrario: si solo le importa la relación @ManyToOne, puede definirla sin tener @OneToMany en la entidad relacionada. Donde lo establezca @ManyToOne, su entidad relacionada tendrá "id de relación" y clave externa.

src\cats\entities\cat.entity.ts

ts
@ManyToOne(() => User)
@JoinColumn({ name: 'userEmail', referencedColumnName: 'email',  })
user: User;

@Column()
userEmail: string;

Ayúdame a seguir creando contenido 😍

Tienes varias jugosas alternativas:

Muchas gracias por su tremendo apoyo 😊

Create cat

src\cats\cats.controller.ts

ts
@Post()
create(
  @Body() createCatDto: CreateCatDto,
  @ActiveUser()
  user: ActiveUserInterface,
) {
  return this.catsService.create(createCatDto, user);
}

src\cats\cats.service.ts

ts
async create(createCatDto: CreateCatDto, user: ActiveUserInterface) {
  const breed = await this.validateBreed(createCatDto.breed);

  return await this.catRepository.save({
    ...createCatDto,
    breed,
    userEmail: user.email,
  });
}

private async validateBreed(breed: string) {
  const breedEntity = await this.breedRepository.findOneBy({ name: breed });

  if (!breedEntity) {
    throw new BadRequestException('Breed not found');
  }

  return breedEntity;
}

Find cats by user

src\cats\cats.controller.ts

ts
@Get()
findAll(
  @ActiveUser()
  user: ActiveUserInterface,
) {
  return this.catsService.findAll(user);
}

src\cats\cats.service.ts

ts
async findAll(user: ActiveUserInterface) {
  if (user.role === Role.ADMIN) {
    return await this.catRepository.find();
  }

  return await this.catRepository.find({
    where: { userEmail: user.email },
  });
}

Find cat by id

src\cats\cats.controller.ts

ts
@Get(':id')
findOne(
  @Param('id') id: number,
  @ActiveUser()
  user: ActiveUserInterface,
) {
  return this.catsService.findOne(id, user);
}

src\cats\cats.service.ts

ts
async findOne(id: number, user: ActiveUserInterface) {
  const cat = await this.catRepository.findOneBy({ id });

  if (!cat) {
    throw new BadRequestException('Cat not found');
  }

  this.validateOwnership(cat, user);

  return cat;
}

private validateOwnership(cat: Cat, user: ActiveUserInterface) {
  if (user.role !== Role.ADMIN && cat.userEmail !== user.email) {
    throw new UnauthorizedException();
  }
}

Update cat

src\cats\cats.controller.ts

ts
@Patch(':id')
update(
  @Param('id') id: number,
  @Body() updateCatDto: UpdateCatDto,
  @ActiveUser()
  user: ActiveUserInterface,
) {
  return this.catsService.update(id, updateCatDto, user);
}

src\cats\cats.service.ts

ts
async update(id: number, updateCatDto: UpdateCatDto, user: ActiveUserInterface) {
  await this.findOne(id, user );
  return await this.catRepository.update(id, {
    ...updateCatDto,
    breed: updateCatDto.breed ? await this.validateBreed(updateCatDto.breed) : undefined,
    userEmail: user.email,
  })
}

Delete cat

src\cats\cats.controller.ts

ts
@Delete(':id')
remove(
  @Param('id') id: number,
  @ActiveUser()
  user: ActiveUserInterface,
) {
  return this.catsService.remove(id, user);
}

src\cats\cats.service.ts

ts
async remove(id: number, user: ActiveUserInterface) {
  await this.findOne(id, user);
  return await this.catRepository.softDelete({ id });
}

Conclusiones

Si estos videos y tutoriales te gustan no olvides apoyar suscribiendote al canal aquí y dando like al video. Si tienes alguna duda o sugerencia puedes dejarla en los comentarios. Nos vemos en el próximo video.