Project using Nest.js/E-commerce App

nestJS password security

Cog Factory 2021. 9. 3. 16:17

개요

   Server는 Client 측에서 보내는 모든 data를 신뢰해서는 안 된다. Client에서도 filtering을 하겠지만 hacker가 마음을 먹으면 충분히 Client filter를 우회해서 값을 주입할 수 있다. 그렇기 때문에 server에서도 password 보안 정책을 해줘야만 한다.

user.entity.ts

import {
  Field,
  InputType,
  ObjectType,
} from '@nestjs/graphql';
import {
  IsEmail,
  IsString,
  MinLength,
} from 'class-validator';
import { BeforeInsert, BeforeUpdate, Column, Entity } from 'typeorm';

@InputType('UserInputType', { isAbstract: true })
@ObjectType()
@Entity()
export class User extends CoreEntity {
  @Column({ unique: true })
  @Field((type) => String)
  @IsEmail()
  email: string;

  @Column()
  @Field((type) => String)
  @MinLength(8)
  password: string;
  
  @BeforeInsert()
  @BeforeUpdate()
  async hashPassword(): Promise<void> {
    if (this.password) {
      try {
        this.password = await bcrypt.hash(this.password, 10);
      } catch (e) {
        console.error(e);
        throw new InternalServerErrorException();
      }
    }
  }

  async checkPassowrd(password: string): Promise<boolean> {
    try {
      const ok = await bcrypt.compare(password, this.password);
      return ok;
    } catch (error) {
      console.error(error);
      throw new InternalServerErrorException();
    }
  }
}

  class-validator module을 이용해서 유효성 검사를 해준다. @MinLength(8)을 이용해서 비밀번호는 최소 8자리 이상이어야 한다.

  @BeforeInsert와 @BeforeUpdate를 이용해서 계정을 생성하거나 비밀번호를 변경할 경우 plain text로 저장하지 않고 hash 값을 저장한다. 

create-account.dto.ts

import { Field, InputType, ObjectType, PickType } from '@nestjs/graphql';
import { MinLength } from 'class-validator';

import { CoreOutput } from 'src/common/dtos/output.dto';
import { User } from '../entities/user.entity';

@InputType()
export class CreateAccountInput extends PickType(User, [
  'email',
  'password',
]) {
  @Field((type) => String)
  @MinLength(8)
  verifyPassword: string;
}

@ObjectType()
export class CreateAccountOutput extends CoreOutput {}

   DTO에도 @MinLength(8)를 이용해서 verifyPassword도 8자리 이상 받을 수 있도록 한다.

users.service.ts

  async createAccount({
    email,
    password,
    verifyPassword,
  }: CreateAccountInput): Promise<CreateAccountOutput> {
    try {
      const exists = await this.users.findOne({ email });
      if (exists) {
        return { ok: false, error: 'There is a user with that email already' };
      }

      if (password !== verifyPassword) {
        return {
          ok: false,
          error: 'Password does not match',
        };
      }

      const regex = new RegExp(
        /(?=.*[!@#$%^&\*\(\)_\+\-=\[\]\{\};\':\"\\\|,\.<>\/\?]+)(?=.*[a-zA-Z]+)(?=.*\d+)/,
      );

      const passwordTestPass = regex.test(password);

      if (!passwordTestPass) {
        return {
          ok: false,
          error: 'Password must contain special character, string and number',
        };
      }

      const user = await this.users.save(
        this.users.create({ email, password }),
      );

      return { ok: true };
    } catch (e) {
      return { ok: false, error: "Couldn't create account" };
    }
  }

   사용자가 설정할 비밀번호와 확인 비밀번호(verifyPassword)가 일치하는지 확인한다. 그리고 비밀번호에 문자, 숫자, 특수문자가 들어있는지 확인하는 Regex를 사용한다. 이를 모두 통과할 경우 계정을 생성한다. change-password 같은 경우에서도 똑같고 추가로 current password와 new password가 다른지 확인해야 한다.

Github link

Set password security for create-account : https://github.com/zpskek/houpang-backend-v1/commit/b9010278adf64572df81e7a42998f3b1259cfc13

Set security password for change-password service : https://github.com/zpskek/houpang-backend-v1/commit/30674b50b8499cb1b017095d3b97dbe84826189f