45

Today, I am trying to figure out how to validate a Sign Up form in the backend side (NestJS) of the app. I am just wondering if exists a way to validate password and passwordConfirm matching, using class-validator package to build up a custom validator or exploit provided ones. I am thinking about a class validator, not a field one.

// Maybe validator here
export class SignUpDto {
    @IsString()
    @MinLength(4)
    @MaxLength(20)
    username: string;

    @IsString()
    @MinLength(4)
    @MaxLength(20)
    @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, {message: 'password too weak'})
    password: string;

    @IsString()
    @MinLength(4)
    @MaxLength(20)
    passwordConfirm: string;
}

What do you suggest?

12
  • Don't think it supports this yet: github.com/typestack/class-validator/issues/486 Commented Feb 28, 2020 at 12:14
  • @AndreiTătar What a pity! Thanks for your answer! Commented Feb 29, 2020 at 10:55
  • 2
    @piero: It's not supported yet as mentioned. But here's an example decorator (@IsLongerThan): github.com/typestack/class-validator/tree/master/sample/… .... it checks if a property is longer than another one. So it's possible to compare one property against another. You can use this example to create a decorator that does what you want. Commented Mar 1, 2020 at 10:52
  • 1
    @PieroMacaluso I would not set a maxlength on passwords, see: stackoverflow.com/questions/98768/… i also hope you hash the passwords Commented Sep 1, 2020 at 14:35
  • 2
    Late to the game but why even send the password confirmation to the service? The entire (somewhat misguided) reason for the password confirmation is to ensure that the user has entered the password they intended to. That validation can take place entirely on the UI-side and only one password property needs to be sent to the service. Commented Oct 26, 2023 at 0:32

12 Answers 12

73

Finally I managed to solve the password matching problem thanks to the suggestion of @ChristopheGeers in the comments of my question:

@piero: It's not supported yet as mentioned. But here's an example decorator (@IsLongerThan): LINK .... it checks if a property is longer than another one. So it's possible to compare one property against another. You can use this example to create a decorator that does what you want.

Here it is the solution I propose:

sign-up.dto.ts

export class SignUpDto {
    @IsString()
    @MinLength(4)
    @MaxLength(20)
    username: string;

    @IsString()
    @MinLength(4)
    @MaxLength(20)
    @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, {message: 'password too weak'})
    password: string;

    @IsString()
    @MinLength(4)
    @MaxLength(20)
    @Match('password')
    passwordConfirm: string;
}

match.decorator.ts

import {registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface} from 'class-validator';

export function Match(property: string, validationOptions?: ValidationOptions) {
    return (object: any, propertyName: string) => {
        registerDecorator({
            target: object.constructor,
            propertyName,
            options: validationOptions,
            constraints: [property],
            validator: MatchConstraint,
        });
    };
}

@ValidatorConstraint({name: 'Match'})
export class MatchConstraint implements ValidatorConstraintInterface {

    validate(value: any, args: ValidationArguments) {
        const [relatedPropertyName] = args.constraints;
        const relatedValue = (args.object as any)[relatedPropertyName];
        return value === relatedValue;
    }

}
Sign up to request clarification or add additional context in comments.

3 Comments

It seems a very good solution, but it has a lot of problem with the TypeScript linting.
You can add this method to MatchConstraint class for error message: defaultMessage(args: ValidationArguments){ return args.property + " must match " + args.constraints[0]; }
But password is stored in db in hashed form, so there will be issues with validation. Take even length
30

For the validating password, I suggest using @IsStrongPassword from class-validator

It could be like this

 @IsStrongPassword({
    minLength: 8,
    minLowercase: 1,
    minNumbers: 1,
    minSymbols: 1,
    minUppercase: 1
  })
  password: string;

1 Comment

this should be the top answer right now
13

Here's an extended example which inlines the validator and provides a default message for it. This way you don't have to enter a message each time you use the @IsEqualTo decorator.

import { 
    registerDecorator, 
    ValidationArguments, 
    ValidationOptions 
} from 'class-validator';

export function IsEqualTo(property: string, validationOptions?: ValidationOptions) {
    return (object: any, propertyName: string) => {
      registerDecorator({
        name: 'isEqualTo',
        target: object.constructor,
        propertyName,
        constraints: [property],
        options: validationOptions,
        validator: {
          validate(value: any, args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = (args.object as any)[relatedPropertyName];
          return value === relatedValue;
        },

        defaultMessage(args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          return `${propertyName} must match ${relatedPropertyName} exactly`;
        },
      },
    });
  };
}

1 Comment

nice....perfect solution
12

Honestly, previous answers feel like over-engineering. You can do something simple as:

import {
  IsDefined,
  IsIn,
  IsString,
  MinLength,
  ValidateIf,
} from 'class-validator';

export class ForgotReturnPasswordDto {
  @IsString()
  @IsDefined()
  @MinLength(3)
  password: string;

  @IsString()
  @IsDefined()
  @IsIn([Math.random()], {
    message: 'Passwords do not match',
  })
  @ValidateIf((o) => o.password !== o.repeatPassword)
  repeatPassword: string;
}

2 Comments

this is the true and simple
this is works fine
9

The accepted answer is pretty good for me, but we could make a spelling mistake like:

@Match('passwordd')
//              👆

So I would like to make it more strict with Generics

@Match(SignUpDto, (s) => s.password)

match.decorator.ts

import { ClassConstructor } from "class-transformer";

export const Match = <T>(
  type: ClassConstructor<T>,
  property: (o: T) => any,
  validationOptions?: ValidationOptions,
) => {
  return (object: any, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [property],
      validator: MatchConstraint,
    });
  };
};

@ValidatorConstraint({ name: "Match" })
export class MatchConstraint implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [fn] = args.constraints;
    return fn(args.object) === value;
  }

  defaultMessage(args: ValidationArguments) {
    const [constraintProperty]: (() => any)[] = args.constraints;
    return `${constraintProperty} and ${args.property} does not match`;
  }
}

So we can use Match decorator like this:

export class SignUpDto {
  // ...
  password: string;

  // finally, we have 😎
  @Match(SignUpDto, (s) => s.password)
  passwordConfirm: string;
}

2 Comments

Which module is ClassConstructor from? I can't seem to find it anywhere.
You need to import it from class-transformer: import { ClassConstructor } from "class-transformer";
4

I like the approach of the IsEqualTo decorator, but I were also concerned about the typos when using a property that is not in my Dto, so I end up with this:

import {
  registerDecorator,
  ValidationArguments,
  ValidationOptions,
} from 'class-validator';

export function IsEqualTo<T>(
  property: keyof T,
  validationOptions?: ValidationOptions,
) {
  return (object: any, propertyName: string) => {
    registerDecorator({
      name: 'isEqualTo',
      target: object.constructor,
      propertyName,
      constraints: [property],
      options: validationOptions,
      validator: {
        validate(value: any, args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = (args.object as any)[relatedPropertyName];
          return value === relatedValue;
        },

        defaultMessage(args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          return `${propertyName} must match ${relatedPropertyName} exactly`;
        },
      },
    });
  };
}

and use it like this:

export class CreateUserDto {
  @IsEqualTo<CreateUserDto>('password')
  readonly password_confirmation: string;
}

Comments

2

I like the accepted answer, but i think we can simplify the process by passing the property we want to validate against to, as a string in the constraints array.

example :

@ValidatorConstraint({ name: 'CustomMatchPasswords', async: false })
export class CustomMatchPasswords implements ValidatorConstraintInterface {
   validate(password: string, args: ValidationArguments) {

      if (password !== (args.object as any)[args.constraints[0]]) return false;
      return true;
   }

   defaultMessage(args: ValidationArguments) {
      return "Passwords do not match!";
   }
}

and then we can use the validator without the need to create a decorator:

    @IsString()
    @MinLength(4)
    @MaxLength(20)
    @Matches(/((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, {message: 'password too weak'})
    password: string;

    @IsString()
    @MinLength(4)
    @MaxLength(20)
    @Validate(CustomMatchPasswords, ['password'])
    passwordConfirm: string;   

Comments

1

Typescript version: Fixed some lint complains

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationOptions,
  registerDecorator,
  ValidationArguments
} from 'class-validator'
import { ClassConstructor } from 'class-transformer'

type Tfn<T> = (o: T) => any

export const Match = <T>(
  type: ClassConstructor<T>,
  property: Tfn<T>,
  validationOptions?: ValidationOptions
) => {
  return (object: unknown, propertyName: string) => {
    registerDecorator({
      target: object.constructor,
      propertyName,
      options: validationOptions,
      constraints: [property],
      validator: MatchConstraint<T>
    })
  }
}

@ValidatorConstraint({ name: 'Match' })
export class MatchConstraint<T> implements ValidatorConstraintInterface {
  validate(value: any, args: ValidationArguments) {
    const [fn] = args.constraints as Tfn<T>[]
    return fn(args.object as T) === value
  }
}

Comments

1

Here is an updated version and usage example of @IsStrongPassword, which I utilize in my decorator:

@IsStrongPassword(
{
  minLength: 8,
  minLowercase: 1,
  minUppercase: 1,
  minNumbers: 1,
  minSymbols: 0,
},
{
  message:
    'The password should contain at least 1 uppercase character, 1 lowercase, 1 number and should be at least 8 characters long.'
 },
)
  password!: string;

By default, this decorator already receives these parameters from the first object sent as a parameter. However, if you need to send a specific/personalized message, it is mandatory to define the parameters by which the password will be verified.

Comments

0

Same as @jrrodasm's suggestion, but I modified it a little like the @nestjs/mapped-types concept.

import { Type } from "@nestjs/common";
import { ValidationArguments, ValidationOptions, registerDecorator } from "class-validator";

export function IsMatchWith<T, K extends keyof T>(classRef: Type<T>, property?: readonly K[], validationOpt?: ValidationOptions) {
    return (obj: any, propertyName: string) => {
        registerDecorator({
            target: obj.constructor,
            propertyName,
            options: validationOpt,
            constraints: [property],
            validator: {
                validate(value: any, args: ValidationArguments) {
                    return !(value !== (args.object as any)[args.constraints[0]])
                },

                defaultMessage({ property, constraints }: ValidationArguments): string {
                    return `${property} must match ${constraints[0]}`;
                }
            }
        })
    }
}

Comments

0

There's also simpler solution without writing own decorator

  @ApiPropertyOptional({ minLength: 6 })
  @IsString()
  @MinLength(6)
  password?: string;

  @ApiPropertyOptional()
  @IsString()
  @MinLength(6)
  @ValidateIf((o) => o.password !== o.passwordConfirm)
  @Equals('password', { message: 'confirmPassword must match password' })
  passwordConfirm?: string;

Comments

-2
@MinLength(requiredlength ex: 5)
 @MaxLength(requiredlength ex:5)

are working in latest version so to validate length we can use that.

1 Comment

What is requiredlength ex:5?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.