nestJS password security
개요
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