avatar
Published on

Typeorm study 2 - Hash / JWT / Guard

Authors
  • avatar
    Name
    Haneul
    Twitter

Typeorm study 2 - Hash / JWT / Guard

Enum Type (enumerated type)

Enum Type은 값을 지정하여 하나의 상수처럼 사용할 수 있다. sqlite 디비에서는 enum type을 지원하지 않는다는 오류가 발생하는데 아래 코드처럼 @Column 데코레이터에 type을 'enum' 이 아니라 'simple-enum'으로 지정해주면 해결된다.

export enum UserRole {
  Host = 'Host',
  Listener = 'Listener',
}

registerEnumType(UserRole, { name: 'UserRole' });


@Column({ type: 'simple-enum', enum: UserRole })
  @Field(type => UserRole)
  role: UserRole;

registerEnumType 함수를 사용해 enum type을 등록해준다.

password hash 처리

typeorm 에는 @BeforeInsert , @BeforeUpdate 데코레이터들이 저장 또는 변경되기 전에 패스워드를 hash 처리할 수 있다. bcrypt 패키지를 사용하여 hash 해준다.

    @BeforeInsert()
    @BeforeUpdate()
    async hashPassword(): Promise<void> {
        if (!this.password) {
            return;
        }
        try {
            this.password = await bcrypt.hash(this.password, 10);
        } catch (error) {
            console.log(error);
            throw new InternalServerErrorException();
        }
    }

jwt 처리

@Module({})
@Global()
export class JwtModule {
  static forRoot(options: JwtModuleOptions): DynamicModule {
    return {
      module: JwtModule,
      providers: [
        {
          provide: CONFIG_OPTIONS,
          useValue: options,
        },
        JwtService,
      ],
      exports: [JwtService],
    }
  }
}

jforRoot method를 정의하는데 앞에 static으로 지정되어 있다. 이는 클래스 인스턴스를 만들지 않아도 JwtModule.forRoot() 이런식으로 사용할 수 있게끔 해주는 접근 제어자이다.

이렇게 정의한 custom value provider는 service 파일에서 이렇게 사용한다.

  • constructor( @Inject (CONFIG_OPTIONS) private readonly options: JwtModuleOptions)

@Global 데코레이터는 import 하면 전역적으로 어디서든 사용할 수 있다.

@Injectable()
export class JwtMiddleware implements NestMiddleware {
  constructor(
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService
  ) {}

  async use(req: Request, res: Response, next: NextFunction) {
    if ('x-jwt' in req.headers) {
      const token = req.headers['x-jwt']
      try {
        const decoded = this.jwtService.verify(token.toString())
        if (typeof decoded === 'object' && decoded.hasOwnProperty('id')) {
          const { user, ok } = await this.usersService.findById(decoded['id'])
          if (ok) {
            req['user'] = user
          }
        }
      } catch (error) {}
    }
    next()
  }
}

middleware에서 service를 사용하기 위해 constructor에서 parameter properties를 사용한다. 이렇게 생성한 middleware 아래와 같이 appModule에 추가해주면 된다. 참고: Applying Middleware

export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(JwtMiddleware).forRoutes({
      path: '/graphql',
      method: RequestMethod.POST,
    })
  }
}

로그인 파트

const passwordCorrect = await user.checkPassword(password)
if (!passwordCorrect) {
  return {
    ok: false,
    error: 'Wrong password',
  }
}

const token = this.jwtService.sign(user.id)

user의 checkPassword 메소드를 이용해서 우리가 입력한 패스워드와 hashing 시켜 놓은 password가 일치하는지 확인하는 로직이다. 그 다음에 const token = this.jwtService.sign(user.id)라는 코드로 우리의 user.id를 json web token으로 만든다. 생성된 토큰은 아래 사이트에서 확인해 볼 수 있다.

Guard

nestjs 에서는 유저 인증, 권한부여를 위해서 guard라는 개념을 사용합니다.

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const gqlContext = GqlExecutionContext.create(context).getContext()
    const user = gqlContext['user']
    if (!user) {
      return false
    }
    return true
  }
}

graphql의 context에는 유저 인증 정보가 들어가 있습니다. context에 user 정보를 넘겨주려면, graphql 모듈을 app 모듈에서 import 하는 단계에서 설정을 해줘야 합니다.

GraphQLModule.forRoot({
      autoSchemaFile: true,
      context: ({ req }) => {
        return { user: req['user'] };
      },
    }),

위에서 만든 AuthGuard는 @UseGuards 데코레이터를 이용하여 사용할 수 있습니다.

    @UseGuards(AuthGuard)
    @Query(returns => UserProfileOutput)
    seeProfile(
        @Args() userProfileInput: UserProfileInput,
    ): Promise<UserProfileOutput> {
        return this.usersService.findById(userProfileInput.userId);
    }

resolver위에 @UseGuards 데코레이터를 이용하면 유저 인증이 완성이 됩니다. 로그인 하지 않은 채로, 즉 로그인 토큰을 헤더에 제공하지 않은 상태로 해당 resolver를 사용하면 Forbidden Resource 에러가 발생하게 됩니다.

로그인 유저 정보 획득

export const AuthUser = createParamDecorator((data: unknown, context: ExecutionContext) => {
  const gqlContext = GqlExecutionContext.create(context).getContext()
  const user = gqlContext['user']
  return user
})

createParamDecorator는 context에서 user 정보를 리턴해주는 역할을 하는 데코레이터입니다. 사용방법은 me( @AuthUser () authUser: User): User 이런 식으로 사용할 수 있습니다.

참고 : nestJS