관리 메뉴

공부 기록장 💻

[NodeJS/NestJS] User Authentication - JWT 토큰 인증 본문

# Tech Studies/NestJS

[NodeJS/NestJS] User Authentication - JWT 토큰 인증

dream_for 2022. 8. 25. 04:07

 

 

이전에 JWT에 대해 공부를 해보았다.

NestJS 프레임워크 내에서 로그인 과정에서 JWT 토큰을 발급하고, JWT 토큰을 이용한 인증 기능을 구현해보도록 하자.

 

 

먼저 다음 명령어를 터미널에 입력하여 nestjs에서 제공하는 jwt 패키지를 설치하자.

 

$ npm i --save @nestjs/jwt

 

 

JWT 모듈 등록

 

 

auth.module.ts 에 다음과 같이 JwtModule 을 등록하자.

imports 부분에 다음과 같이 추가를 해주도록 한다.

secret 키를 SECRET 으로 지정해주고, signOptions로는 토큰 만료 시간을 300초로 지정해준다.

 

import { JwtModule } from '@nestjs/jwt';


@Module({
  imports: [
    TypeOrmExModule.forCustomRepository([UserRepository]),
    JwtModule.register({
      secret: 'SECRET',
      signOptions: { expiresIn: '300s'},
    }),    
  ],
  exports: [TypeOrmExModule],
  controllers: [AuthController],
  providers: [AuthService, UserService]
})
export class AuthModule {}

 

 


 

Payload 생성 및 Interface 지정

 

먼저 auth 폴더 내에 security 폴더를 생성하고, payload.interface.ts 파일을 만들자.

 

 

paylooad에 보여줄 interface 내부를 다음과 같이 email 한 개의 필드로 지정하여 export 하자.

 

export interface Payload{
    email: string;
}

 

 


JWT 토큰 생성

 

Auth Service

 

먼저 auth.service.ts 파일에서 JwtModule의 JwtService를 사용하여 Jwt 토큰을 발급하고,

로그인 시 리턴 값에 Jwt토큰을 

 

먼저 AuthService의 constructor 생성자 부분에 JwtService 를 추가하자.

 

import { JwtService } from '@nestjs/jwt';

export class AuthService {
    constructor(
        private userService:UserService,
        private jwtService: JwtService
    ){}

 

 

 

auth.service.ts 의 유저를 검증하는 validateUser 함수를 다음과 같이 변경하자.

리턴 값으로는 string 타입의 accessToken으로 수정하고, payload에 userFind의 email 데이터를 담는다.

return 값으로는 jwtService의 sign 메소드를 이용해 payload 값을 담아 만든 accessToken을 이용한다. 

    // 로그인
    async validateUser(userDTO: UserDTO): Promise<{accessToken: string} | undefined>{
        let userFind: User = await this.userService.findByFields({
            where: { email: userDTO.email}
        })
        const validatePassword = await bcrypt.compare(userDTO.password, userFind.password);

        // 사용자를 찾지 못한 경우와 비밀번호가 올바르지 않은 경우
        if(!userFind || !validatePassword ){
            throw new UnauthorizedException('Login Failed!');
        }

        const payload: Payload = { email: userFind.email };
        return {
            accessToken: this.jwtService.sign(payload)
        };
    }

 

 

 

 

Auth Controller

 

auth.controller.ts 파일 또한 다음과 같이 수정하자.

express 패키지에서 이번엔 Response 모듈을 import 하고,

AuthController의 signin 함수에 다음과 같이 두번째 인자에 @Res() 모듈을 사용한 Response 모듈의 res 객체를 추가한다.

 

setHeader 함수를 사용하여 jwt 토큰의 헤더를 지정해주고,

리턴 값으로는 jwt를 json형태로 변경하여 반환하도록 한다.

 

 

import { Request, Response } from 'express';


@Controller('auth')
export class AuthController {
/*
...
*/

    // 로그인
    @Post('/sign-in')
    async signin(@Body() userDTO: UserDTO, @Res() res: Response): Promise<any>{
        const jwt = await this.authService.validateUser(userDTO);
        res.setHeader('Authorization', 'Bearer '+jwt.accessToken);
        return res.json(jwt); // 로그인 시 토큰 리턴
    }
}

 

 

 

Node Js에서 제공하는 response 모듈의 setHeader 함수의 두 인수를 조금 더 살펴보면 다음과 같다.

첫번째 인수인 name으로는 헤더의 이름을, 두번째 인수인 value로는 헤더 값을 지정해준다.

위에서 두번째 값으로는 "Bearer " 문자열과 발급한 accessToken을 함께 지정하였다.

 

 

 

 


 

Postman 검증

 

다음과 같이 로그인 시도를 했을 때, accessToken이 발급된 것을 확인할 수 있다.

 

 

 

 

 


Decoding Encoded AccessToken

 

accessToken의 header, payload, signature를 decode 해보자. (https://jwt.io/)

위에 encoded 된 jwt 토큰을 입력창에 넣어보니, 헤더와 페이로드, 서명 모두 알맞게 나타난 것을 확인할 수 있다.

 

 

위에서 VERIFY SIGNATURE 부분에서 your-256-bit-secret 에

AuthModule에서 JwtModule을 등록할 때 secret 값으로 지정했던 'SECRET'을 입력해보도록 하자.

 

secret 값으로 지정했던 "SECRET" 까지 입력을 하면, 

Signature Verified 로 변경되며 정확한 JWT 토큰 값이 생성된 것이라 볼 수 있다.

 

 

 


 

JWT 토큰 인증 (Guard)

 

이제는 발급한 JWT 토큰에 대해, 요청 시 토큰을 인가(guard)하는 방법에 대해 알아보자.

NestJS에서는 라우팅 전에 작동하는 일종의 미들웨어인 Guard를 사용한다.

 

이 작업을 하기 위해 passport-jwt 패키지를 설치하자.

 

$ npm i --save @nestjs/passport @types/passport-jwt

 

 

Auth Module

 

auth.module.ts 파일에 imports 부분에는 PassPortModule을, providers 부분에는 JwtModule을 추가해주자.

 

@Module({
  imports: [
    TypeOrmExModule.forCustomRepository([UserRepository]),
    JwtModule.register({
      secret: 'SECRET',
      signOptions: { expiresIn: '300s'},
    }),    
    PassportModule
  ],
  exports: [TypeOrmExModule],
  controllers: [AuthController],
  providers: [AuthService, UserService, JwtModule]
})
export class AuthModule {}

 

 

 

JWT 검증을 위해  JwtStrategy를 만들자.

 

Auth Service

 

먼저 auth.service.ts 에 유저의 토큰을 검증하는 함수 tokenValidateUser 을 추가하자.

payload에 담겨있는 email 정보를 바탕으로 user 데이터를 찾아 리턴하게 된다.

 

    // 유저의 토큰 검증
    async tokenValidateUser(payload: Payload): Promise<User | undefined>{
        return await this.userService.findByFields({
            where: { email: payload.email }
        })
    }

 

 

JwtStrategy

 

이번엔 security 폴더 내에 passport.jwt.strategy.ts 파일을 추가하고 다음과 같이 작성하자.

 

PassPortStrategy 함수를 상속받는 JwtStrategy 클래스를 만들어 export 한다.

생성자에는 PassportStrategy에서 제공하는 jwtFromRequest, ignoreExpiration, secretOrKey에 각각 알맞은 값을 넣어주도록 하자.

 

validate() 함수를 만들어 request 요청 마다 토큰을 검증하도록 하자.

위의 auth service에서 만든 tokenValidateUser 함수를 실행하여 payload의 email값과 동일한 email을 갖는 유저인지 검증한다.

 

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy, VerifiedCallback } from "passport-jwt";
import { AuthService } from "../auth.service";
import { Payload } from "./payload.interface";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy){
    constructor(private authService:AuthService){
        super({
            // jwt 토큰 분석
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: true,
            secretOrKey: 'SECRET'
        })
    }

    // 토큰 검증
    async validate(payload: Payload, done: VerifiedCallback): Promise<any>{
        const user = await this.authService.tokenValidateUser(payload);
        if (!user){
            return done(new UnauthorizedException({message: 'user does not exist!'}));
        }
        return done(null, user);
    }
}

 

 


Auth Guard

 

security 폴더 내에 auth.guard.ts 파일을 생성하고 다음과 같이 작성해주자.

 

 

"@nestjs/passport" 패키지에서 AuthGuard를 NestAuthGuard로 import하고,

jwt를 인수로 하는 NestAuthGuard 함수를 상속받은 AuthGuard을 만들고, AuthGuard의 canActiavte 를 이용한다.

 

import { ExecutionContext, Injectable } from "@nestjs/common";
import { AuthGuard as NestAuthGuard } from "@nestjs/passport";
import { Observable } from "rxjs";

@Injectable()
export class AuthGuard extends NestAuthGuard('jwt'){
    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
        return super.canActivate(context)
    }
}

 

 

 

Auth Controller

 

이제 컨트롤러 부분에서, 해당 user의 토큰이 인증된지 아닌지 확인하는 Get 요청을 보내보자.

AuthGuard의 UseGuards 데코레이터를 사용하고,

GET 요청에 대하여 인증이 성공하면, 해당 user을 리턴하게 된다.

    // 토큰 인증 확인
    @Get('/authenticate')
    @UseGuards(AuthGuard())
    isAuthenticated(@Req() req: Request): any {
        const user: any = req.user;
        return user;
    }

 

 

 

 


에러 발생 : "ERROR [ExceptionHandler] metatype is not a constructor"

 

 

auth.contoroller.ts 에서 위에서 작성했던 토큰 인증 라우터 부분에서

@UseGuards() 내에 AuthGuard를 AuthGuard() 로 변경하자. 

 

auth.module.ts 에서 PassPortModule을 imports부분에 추가할 때,

다음과 같이 defaultStrategy가 'jwt'임을 등록하도록 수정하자.

 

 

 

POSTMAN을 이용해 로컬 서버의

localhost:3000/auth/authenticate 에 GET 요청을 보내보자.

 

Authorization의 Type은 위에 첨부한 것 과 같이 Bearer Token 으로 변경해주고,

Headers부분에 Key에는 Authorization을, Value로는 Beaer 토큰값 을 입력해준다.

 

하지만 결과값을 보내 500 Status Code와 함께 에러가 발생하였다. 

 

 

"jwt"가 unknown authentication strategy 라는 에러였다.

 

 

해결방법은?

Auth Module의 providers에 JwtStrategy를 추가하자!

 

 

다시 요청을 보내보니, 올바르게 작동하는 것을 확인할 수 있다 :) 

 

 

이번엔 Bearer 뒷 부분에 발급받은 토큰을 따로 입력하지 않고 get 요청을 보내보자.

 

위와 같이 Unauthorized 메시지와 함께 401 오류 코드가 나타남을 확인할 수 있다.

 

 

728x90
반응형
Comments