09) 개인 프로젝트) Auth인증 구현 Part 5

Leo·2021년 2월 16일
0

Project-01

목록 보기
9/12
post-thumbnail

password 암호화

password를 일반적인 string 형태로 놔두는 것을 보안에 매우 치면적입니다. 그렇기 때문에 암호화는 선택사항이 아닌 필수사항입니다. js에서 지원하는 bcrypt 패키지를 이용하려고 합니다.

bcrypt 패키지 다운로드

bcrypt 패키지는 해싱 알고리즘을 지원합니다. 문자열 해시, 일반 문자열을 해시와 비교, 솔트 추가를 처리할 수 있습니다.
bcrypt의 작업은 새로운 스레드 풀을 사용하여 실행됩니다.

$ npm i --save bcrypt
$ npm i --save-dev @types/bcrypt

UsersService 수정

이전 코드에서는 user 정보 추가를 user에서 진행하였지만 이제는 auth에서 관리하도록 합니다. createNewUser를 create로 변경하면서 return 값이 있는 코드로 변경해줍니다.
추가로 getByEmail 메소드에 예외처리 부분을 추가해 줍니다.

/src/users/users.service.ts

...생략

@Injectable()
export class UsersService {
  ...생략
  async create(user: User): Promise<User> {
    await this.usersRepository.save(user);
    return user;
  }

  async getByEmail(email: string) {
    const user = this.usersRepository.findOne({ email });
    if (user) {
      return user;
    }
    throw new HttpException(
      'User with this email does not exist',
      HttpStatus.NOT_FOUND,
    );
  }
}

AuthService register 기능 추가

AuthService에 UsersService의 create를 이용하여 register 기능을 추가합니다.
bcrypt 패키지의 hash 메소드를 사용하여 암호화 해줍니다. 암호화한 password를 UsersService의 create를 사용하여 DB에 추가합니다.
추가하려고 하는 Email의 값이 이미 존재하면 예외가 발생합니다.

/src/auth/auth.service.ts

...생략
import { hash } from 'bcrypt';

@Injectable()
export class AuthService {
  ...생략
  async register(user: User) {
    const hashedPassword = await hash(user.password, 10);
    try {
      const { password, ...returnUser } = await this.usersService.create({
        ...user,
        password: hashedPassword,
      });

      return returnUser;
    } catch (error) {
      if (error?.code === 'ER_DUP_ENTRY') {
        throw new HttpException(
          'User with that email already exists',
          HttpStatus.BAD_REQUEST,
        );
      }
    }
  }
}

입력된 Password와 DB의 Password의 비교

이전 로직에서는 단순히 입력된 Password와 조건문을 통하여 비교하였지만 암호화 로직이 추가되었으므로 코드의 수정이 필요합니다.

verifyPassword 메소드에서는 암호된 Password와 입력된 Password를 비교합니다. 비교를 위하여 bcrypt의 compare 메소드를 사용합니다. 만약 틀릴경우 예외를 발생시킵니다.

정상적인 흐름으로 진행된다면 마지막에 user에서 password를 제외한 부분을 반환합니다.

/src/auth/auth.service.ts

...생략
import { compare, hash } from 'bcrypt';

@Injectable()
export class AuthService {
  ...생략
  async vaildateUser(email: string, plainTextPassword: string): Promise<any> {
    try {
      const user = await this.usersService.getByEmail(email);
      await this.verifyPassword(plainTextPassword, user.password);
      const { password, ...result } = user;
      return result;
    } catch (error) {
      throw new HttpException(
        'Wrong credentials provided',
        HttpStatus.BAD_REQUEST,
      );
    }
  }

  private async verifyPassword(
    plainTextPassword: string,
    hashedPassword: string,
  ) {
    const isPasswordMatch = await compare(plainTextPassword, hashedPassword);
    if (!isPasswordMatch) {
      throw new HttpException(
        'Wrong credentials provided',
        HttpStatus.BAD_REQUEST,
      );
    }
  }
  ...생략
}

AuthController 추가

login과 register는 이제 auth에서 관리가 되기 때문에 app.controller가 아닌 auth.controller가 필요합니다.
nestjs cli 를 이용하여 생성할 수 있습니다.

$ nest g co auth

AuthController에 login, register 경로 추가

이제 AppController를 초기화 한 후 AuthController에서 처리하는 코드를 작성합니다.

/src/app.controller.ts

import { Controller } from '@nestjs/common';

@Controller()
export class AppController {}

/src/auth/auth.controller.ts

import { Body, Controller, Post, Req, Res, UseGuards } from '@nestjs/common';
import { Public } from 'src/skip-auth.decorator';
import { User } from 'src/users/user.entity';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { Response } from 'express';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Public()
  @UseGuards(LocalAuthGuard)
  @Post('login')
  async login(@Req() req, @Res({ passthrough: true }) res: Response) {
    const token = await this.authService.login(req.user);
    res.cookie('Authentication', token, {
      domain: 'localhost',
      path: '/',
      httpOnly: true,
    });
  }

  @Public()
  @Post('register')
  async register(@Body() user: User): Promise<any> {
    return this.authService.register(user);
  }
}

Request, Response

이제는 Password에 들어가는 데이터가 암호화가 되기 때문에 DB Table을 초기화 한 후 실행해줍니다. (간단하게 Table을 삭제합니다.)

POST http://localhost:3000/auth/register

Requset

{
    "email": "test1@gmail.com",
    "username": "test1",
    "password": "qwer1234@"
}

Response

{
    "email": "test1@gmail.com",
    "username": "test1",
    "id": 1
}

POST http://localhost:3000/auth/login

Request

{
    "email": "test1@gmail.com",
    "password": "qwer1234@"
}

POST http://localhost:3000/users/1

Response

{
    "id": 1,
    "email": "test10@gmail.com",
    "username": "test10",
    "password": "$2b$10$xf.EpF4soT5F2FJB7GsWVeicqJJyWKRyBTDZrGuF1jDpNSYf/Px2e"
}

다음 포스트에서 수정될 사항

  • secret key의 저장 장소
  • token의 시간 설정
  • username이 아닌 email 사용
  • password 암호화
profile
개발자

0개의 댓글