User module에 User Entity를 생성하고, User service에서 User database에 create / fetch 하는 메서드를 간단히 만들어 줍니다. UserService를 Authentication module에서 사용하길 원하기 때문에, UserService를 @Injectable() 데코레이터로 감싸주고, UserModule에서 UserService를 export해줍니다.
password는 가장 안전해야 하는 데이터입니다. 그래서 password는 hash 해야 합니다. hash를 하는 과정에서 필요한 값은 random string 인 salt 값이 필요합니다.
bcrypt
이 모든 과정을 bcrypt 라이브러리를 사용하면 쉽게 할 수 있습니다. bcrypt로 password에 salt값을 적용해 여러번 hash하여 복원하는 것을 어렵게 합니다. bcrypt는 cpu를 잡아먹는 작업이지만, thread pool의 추가적인 thread를 이용해 연산을 수행하므로 암호화하는 과정에서 다른 작업을 수행할 수 있습니다.
Authentication module을 생성하고, Authentication Service 에서 bcrypt를 이용해 요청으로 받은 비밀번호를 암호화하고 저장하겠습니다. 저장하기 위해서 user service가 필요하니 생성자에 전에 export한 user service를 불러와줍니다.
exportclassAuthenticationService{constructor(privatereadonlyusersService:UsersService){}publicasyncregister(registrationData:RegisterDto){consthashedPassword=awaitbcrypt.hash(registrationData.password,10);try{constcreatedUser=awaitthis.usersService.create({...registrationData,password:hashedPassword});createdUser.password=undefined;returncreatedUser;}catch(error){if(error?.code===PostgresErrorCode.UniqueViolation){thrownewHttpException('User with that email already exists',HttpStatus.BAD_REQUEST);}thrownewHttpException('Something went wrong',HttpStatus.INTERNAL_SERVER_ERROR);}}publicasyncgetAuthenticatedUser(email:string,plainTextPassword:string){try{constuser=awaitthis.usersService.getByEmail(email);awaitthis.verifyPassword(plainTextPassword,user.password);user.password=undefined;returnuser;}catch(error){thrownewHttpException('Wrong credentials provided',HttpStatus.BAD_REQUEST);}}privateasyncverifyPassword(plainTextPassword:string,hashedPassword:string){constisPasswordMatching=awaitbcrypt.compare(plainTextPassword,hashedPassword);if(!isPasswordMatching){thrownewHttpException('Wrong credentials provided',HttpStatus.BAD_REQUEST);}}}
createdUser.password = undefined; 는 password를 response로 보내주기 위한 깔끔한 방법은 아닙니다. 나중에 수정하도록 하겠습니다.
위 함수에서 주목할 부분은 회원가입을 할 때는 비밀번호를 bcrypt의 hash 메서드를 이용해 hash 하고 login 할 때는 compare 메서드를 이용해 요청값과 DB에 저장된 비밀번호를 비교하는 것입니다.
여기까지 인증로직을 구현했으니, passport와 authentication을 통합하는 일만 남았습니다. passport는 authentication을 추상화하여 우리가 좀 더 다른 로직에 집중할 수 있게 해줍니다.
인증된 사용자는 매번 어플리케이션에 접속하고 뭔가 요청을 보낼때마다 로그인할 수 는 없습니다. 우리는 이런 귀찮은 것들을 막기 위해 jwt 토큰을 사용하여 사용자에게 어떤 권한이 있는지 검사하면 됩니다.
jwt 토큰을 사용할 때는 2개의 거의 필수적인 변수들이 필요한데, JWT_SECRET 값과 JWT_EXPIRATION_TIME 입니다.
JWT_SECRET 은 절대 노출되어서는 안됩니다. 이를 이용해 토큰을 decode하거나 encode하여 어플리케이션에 영향을 줄 수 있기 때문입니다. JWT_EXPIRATION_TIME 도 너무 길거나 너무 짧게 가져가면 안됩니다. 만료기한이 너무 길다면, 그 안에 유출될 가능성이 있고, 너무 짧다면 사용자가 여러번 로그인을 수행해야 합니다.