Skip to content

JWT(Json Web Token)

Github link

  • 사용자가 사용자 이름/비밀번호로 인증할 수 있도록 허용하여 보호된 API 엔드포인트에 대한 후속 호출에서 사용할 수 있도록 JWT를 반환해야 합니다. 이 요구 사항을 충족하기 위한 작업은 순조롭게 진행 중입니다. 이를 완료하려면 JWT를 발급하는 코드를 작성해야 합니다.
  • bearer 토큰 혹은 cookie로 유효한 JWT의 존재를 기반으로 보호되는 API 경로를 만듭니다.

필수 패키지 설치

sh
npm install --save @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt

JWT module의 옵션 설명

JWT는 아래와 같이 정말 방대한 옵션을 담고 있습니다.
production 환경에서는 미세하게 다뤄주면 좋기 떄문에 여러가지 옵션을 사용하지만..
우리는 secret과 signOptions - expiresIn 정도만 다뤄 봅시다.

NametypeDescription
globalbooleanJWT 모듈이 전역 모듈로 설정되어 애플리케이션의 어느 곳에서나 쉽게 접근할 수 있습니다. 기본값은 false입니다.
signOptions토큰을 서명할 때 사용할 옵션들을 지정합니다.
├ algorihmAlgorithmJWT의 서명 알고리즘. HS256(default),HS384,HS512,RS256,RS384,RS512,ES256,ES384,ES512,none
├ keyidstringJWT의 헤더에 포함될 kid (키 ID) 값을 지정합니다. 이는 서명에 사용되는 특정 키를 식별하는 데 사용됩니다.
├ expiresInstring | number토큰의 만료 시간을 설정합니다. 예를 들어, 60, "2 days", "10h" 등으로 지정할 수 있습니다.
├ notBeforestring | number토큰이 활성화되기 전의 대기 시간을 설정합니다.
├ audiencestring | string[]토큰의 대상 수신자를 지정합니다.
├ subjectstring토큰의 주제를 지정합니다.
├ issuerstring토큰의 발행자를 지정합니다.
├ jwtidstringJWT의 고유 식별자를 지정합니다. 토큰을 고유하게 식별하고 중복 사용을 방지하는 데 사용됩니다.
├ mutatePayloadboolean페이로드를 변경할 수 있게 할지 여부를 설정합니다.
├ noTimestampboolean토큰에 iat (issued at) 타임스탬프를 포함하지 않을지 결정합니다.
├ headerJwtHeaderJWT의 헤더에 추가적인 정보를 포함시킬 수 있습니다.
├ encodingstring인코딩 방식을 지정합니다.
├ allowInsecureKeySizesboolean보안에 취약한 키 크기를 허용할지 결정합니다.
├ allowInvalidAsymmetricKeyTypesboolean유효하지 않은 비대칭 키 유형을 허용할지 결정합니다.
secretstring | Buffer대칭 키 서명에 사용될 비밀 키입니다.
publicKeystring | Buffer비대칭 키 서명/검증에 사용될 공개 키입니다.
privateKeyjwt.Secret비대칭 키 서명에 사용될 개인 키입니다.
secretOrKeyProvider이 함수는 요청 유형, 토큰 또는 페이로드, 옵션에 따라 동적으로 비밀 키를 제공합니다.
verifyOptions토큰을 검증할 때 사용할 옵션들입니다.
├ algorithmsAlgorithm[]허용할 알고리즘을 지정합니다.
├ audiencestring | RegExp | Array<string | RegExp>검증할 수신자를 지정합니다.
├ clockTimestampnumber현재 시간을 기준으로 설정합니다.
├ clockTolerancenumber타임스탬프 검증 시 허용할 시간 오차 범위를 지정합니다.
├ completeboolean검증 성공 시 토큰의 전체 payload를 반환할지 여부를 결정합니다.
├ issuerstring | string[]토큰 발행자를 검증합니다.
├ ignoreExpirationboolean토큰의 만료를 무시할지 결정합니다.
├ ignoreNotBeforebooleannotBefore 클레임을 무시할지 결정합니다
├ jwtidstringJWT ID를 검증합니다.
├ noncestringnonce 값을 검증합니다.
├ subjectstring주제(subject)를 검증합니다.
├ maxAgestring | number이 값을 초과하는 토큰은 무효로 처리됩니다.
├ allowInvalidAsymmetricKeyTypesboolean유효하지 않은 비대칭 키 유형을 허용할지 결정합니다.

JWT Sign

lib.module.ts

typescript
@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (
        configService: ConfigService
      ): Promise<JwtModuleOptions> => ({
        secret: configService.get("JWT_SECRET"),
        signOptions: { expiresIn: "5m" },
      }),
    }),
  ],
  controllers: [LoginController],
  providers: [LoginService],
  exports: [LoginService],
})
export class LoginModule {}

useFactory가 나왔네요. 이것은 custom providers에서 나옵니다.
간단하게 설정을 내가 원하는데로 동적으로 바꿔줄수 있고 비동기적인 설정도 가능하게 합니다.
나중에 provider 때 더 자세히 배워 봅시다.

lib - auth - jwt - jwt-sign.service.ts

typescript
@Injectable()
export class JwtSignService {
  constructor(private readonly JwttService: JwtService) {}

  async signJwt(user: UsersDto) {
    return await this.JwttService.signAsync(user);
  }
}

login.module.ts

typescript
@Module({
  imports: [forwardRef(() => LibModule)],
  controllers: [LoginController],
  providers: [LoginService],
  exports: [LoginService],
})
export class LoginModule {}

서로 참조하여 순환종속성이 생김으로 forwardRef를 해줍니다.

login.controller.ts

typescript
@UseGuards(LocalAuthGuard)
@Post()
async login(@Req() req: Request): Promise<string> {
  const user = req.user as UsersDto;
  const jwt = await this.jwtSignService.signJwt({ ...user });
  return jwt;
}

JWT 토큰 쿠키에 넣기

Cookie Parser

JWT Verify

lib - auth - jwt - jwt.strategy.ts

typescript
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, "jwt") {
  constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([
        (req: Request) => {
          return req?.cookies?.token;
        },
      ]), // 요청에서 JWT를 추출하는 방법을 제공
      ignoreExpiration: false, // 만료된 JWT 허용 여부
      secretOrKey: configService.get("JWT_SECRET"), // 비밀 키
    });
  }

  async validate(payload: any) {
    return { id: payload.id, username: payload.username };
  }
}

이제 guard에서 사용하고 jwt를 verify 하는 passport를 만듭니다.
requst를 받아 extract해주는 ExtractJwt는 여러 기능을 제공합니다.

name
fromHeaderstring, 지정된 헤더에서 token을 가져옵니다.
fromBodyFieldstring, body에서 token을 가져옵니다.
fromUrlQueryParameterstring, query parameter에서 token을 가져옵니다.
fromAuthHeaderWithSchemestring, authorization header에서 scheme를 찾고 token을 가져옵니다.
fromExtractorsRequestFn[], request의 원하는 property에서 token을 가져옵니다.
fromAuthHeaderAsBearerTokenauthorization header에서 Bearer token을 가져옵니다.

우리는 cookie에 token이 있으니까 cookie를 extract 해줍니다.
ignoreExpiration(만료여부)은 일단 무시하고 secretOrKey에 sign을 했었던 값을 넣어줍니다.
validate에서 return 된 값은 request.user에 있습니다.

lib - auth - jwt - jwt.guard.ts

typescript
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}

guard를 만들어주고..

lib - lib.module.ts

typescript
@Module({
  imports: [
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (
        configService: ConfigService
      ): Promise<JwtModuleOptions> => ({
        secret: configService.get("JWT_SECRET"),
        signOptions: { expiresIn: "5m" },
      }),
    }),
    LoginModule,
    PassportModule,
  ],
  providers: [LocalStrategy, JwtSignService, JwtStrategy],
  exports: [JwtSignService],
})
export class LibModule {}

잊지말고 module의 provider에 넣어줍니다.

cats.controller.ts

typescript
@Post('many')
@UseGuards(JwtAuthGuard)
createMany(@Body() createCatDto: ArrayCreateCatDto): CatsDto[] {
  return [this.cats];
}

이제.. 로그인 했다 치고~ 우리 이전에 local guard에서 로그인하고 쿠키 넣어주는것 했으니까~
이번에 cats controller에 createMany에 guard를 적용해봅시다.
토큰이 제대로 되었다면 결과를 return하고 그렇지 않다면 Unauthorized 에러가 발생합니다!