#Serialization of a DTO with user.role in request.

43 messages · Page 1 of 1 (latest)

lone garnet
#

The DTO I have looks like this:

export class UpdateUserDto {
  @IsOptional()
  @IsString({ message: MESSAGES.IS_INVALID })
  @Length(3, 20, { message: MESSAGES.LENGTH })
  username?: string;

  @IsOptional()
  @IsEnum(Role, { message: MESSAGES.IS_INVALID })
  role?: Role;

  @IsOptional()
  @IsString({ message: MESSAGES.IS_INVALID })
  @Length(3, 20, { message: MESSAGES.LENGTH })
  firstName?: string;

  @IsOptional()
  @IsString({ message: MESSAGES.IS_INVALID })
  @Length(4, 40, { message: MESSAGES.LENGTH })
  lastName?: string;
}

My goal is to serialize the DTO based on the role of the user.
For example:
The user logged in as an admin, can send the role parameter in the request body, otherwise he cannot.
I tried @Expose, but it didn't work. (I don't have access to the user.role in the request)
How can I do this?

left cargo
lone garnet
#

how can i access to request in @Expose decorator?

left cargo
#

you can just return the Expose() in this custom decorator

#

something i didn't know either
they are actually pretty simple
a custom class validator is actuelly more pain to write
then a class-decorator one

#

does that anser your question? or need some help?

lethal jay
#

I would recommend creating a custom validator and inject the request object like so:

class CustomValidator implements ValidatorConstraintInterface {
  constructor(@Req() private req: Request) {}

  validate(value: string): boolean {
// if role is not admin and a value is passed for role
// then return false, otherwise true
    if (this.req['user'].role !== 'ADMIN' && value) {
      return false;
    }
    return true;
  }

  defaultMessage?(validationArguments?: ValidationArguments): string {
    return 'Invalid request';
  }
}

Then use that custom validator in your dto on top of the role property:

@IsOptional()
@IsEnum(Role, { message: MESSAGES.IS_INVALID })
@Validate(CustomValidator)
role?: Role;

As we can utilize dependency injection in the custom validator, you have access to the request object

#

I've never done that myself to be honest but if it doesn't work, just let us know

left cargo
#

there are more ways to skin a cat

  • you can write a custom class-validator decorator
  • a custom class-transformer
    or even something else then a decorator
    like a guard could work
left cargo
lethal jay
#

Of course it's just preference. I just wanted to show another way that allows him to utilize request object in dto

#

Also returning false inside validate method will return 400. It might be better to throw forbidden exception

left cargo
lethal jay
#

They wont be able to guess admin accounts just with 403 because it'll be thrown when there's no token with role propery attached to the request object. API response just tells the user they are not able to proceed just because they are not an admin user. That gives you no indication as to who might be an admin user

lethal jay
#

And just telling user they need admin access is also not a security issue. Attackers will always try to target admin accounts. Whether you return 403 or not. Just returning 403 without disclosing confidential information is a practice I see in a lot of projects at my work

left cargo
#

it is standard to not give them such info
forging is indeed not really possible
but they will prob search for a admin and do some phishing to gain access
if they get the mail and password they're in
or steal they cookie or something

you basicly tell the attackers that they need to go after a admin or something
dont tell what they did wrong dont acknowledge that role can be send as part of the body, it's better to reject them on everything they send in the body that isn't right

treat all fields the same, if 1 unknown field is send, block them
if 1 field is send they dont have right to, block them

lethal jay
#

Actually yes we can just remove that error message

#

Let me modify it

#

Thanks for the correction

left cargo
#

security is as important
you dont have to tell a attacker what they did wrong
just that they did something wrong
i rarely put any response messages in
only when a user might mess something up

like when they typed a activation code that isn't valid
even something small as this makes a difference 😉

lethal jay
#

You're right. I think creating a constants.ts file for error messages would be also a great practice for that purpose

lone garnet
lone garnet
lone garnet
lethal jay
#

I see what you mean

#

I'd do something like

delete dtoObject.role

Just delete it manually from the object inside your service or controller class. But if you want to do it from DTO using class-transformer, unfortunately I dont know any way to do that

#

Or you can write an interceptor

left cargo
lethal jay
#

That's a cool solution. What I had in mind was something like that (for interceptor solution):

class CustomInterceotor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest<Request>();
    if (request['user'].role !== 'ADMIN' && request.body['role']) {
      delete request.body['role'];
    }
    return next.handle();
  }
}

That way you delete the role propery from the request body in case user is not an admin and just continue on executing the controller with that request

#

And then use

@UseInterceptors(CustomInterceotor)
someControllerMethod()
#

Idk if that's a good fix @lone garnet

#

Let me know if that works out for you

#

Or even create a custom decorator

const CheckRole = () => @UseInterceptors(CustomInterceotor);
lone garnet
lethal jay
lethal jay
lone garnet
left cargo
#

i would rather validate then transform, if you where to ask me, feels more secure
if anything fishy is going trough i just forbid it

transforming can be brute forced and then checking what got removed and what didn't
it is up to you to get the user role passed
can't chew your food for you after all 😉

lethal jay
#

Btw something else just came to my mind @lone garnet

#
class CustomInterceptor implements NestInterceptor {
  constructor(private dtoProperyName: string | symbol) {}

  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<any> | Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest<Request>();
    if (request['user'].role !== 'ADMIN' && request.body[this.dtoProperyName]) {
      delete request.body['role'];
    }
    return next.handle();
  }
}

By using the above approach, you can parameterize the propery name so it's applicable to any route handler. Like this:

@UseInterceptors(new CustomInterceptor('role'))
someRouteHandler() {...}

By replacing 'role' with another property name, you dynamically use it for any dto