23년 2월 13일 TIL & 개발노트 (흑우집합소)

Posted by , February 13, 2023
TIL흑우집합소
Series ofTIL

thumbnail

23년 2월 13일 TIL & 개발노트 (흑우집합소)

금일 흑우집합소를 개발하며 배운 내용 및 겪은 과정을 정리하여 올려본다.

오늘은 JWT 인증 부분을 처리하였다.

Nest.js에서 JWT 사용시 Error 커스텀 처리

  • Passport-JWT를 그대로 사용하려 했는데 Access Jwt의 토큰 만료시에도 401을 띄운다.
  • 나는 Access Token에 대해서는 토큰 만료 시 서버 내에서 저장된 리프레시 토큰을 확인하고 그에 따른 추가 작업을 하기 원했다.
  • 물론 try catch로 처리할 수 있지만 이렇게 하면 AuthExceptionHandler처리 부분이 너무 지저분해진다.
  • 그래서 좀 찾아본 결과 passport-custom이란 것으로 Strategy부터 커스텀 처리를 할 수 있는 것을 찾았다.(이건 따로 포스팅 예정)
  • 패키지를 설치하고 적용해봤다.
  • 일일히 JWT에 대한 에러를 핸들링 해야 하는 부분이 있지만 직접 핸들링을 할 수 있어서 더 좋은 것 같다.
import { Inject, Injectable } from "@nestjs/common"
import { PassportStrategy } from "@nestjs/passport"
import { Request } from "express"
import { Strategy } from "passport-custom"
import { AuthException } from "../exceptions/auth.exception"
import { AuthJwtErrorCode } from "../exceptions/auth.jwt.error.code"
import {
  VerifyJwtOutboundPort,
  VERIFY_JWT_OUTBOUND_PORT,
} from "../ports/out/jwt/verify.jwt.outbound.port"
import { JWT_TYPE } from "../types/jwt.enums"

@Injectable()
export class AccessJwtStrategy extends PassportStrategy(Strategy, "jwt") {
  constructor(
    @Inject(VERIFY_JWT_OUTBOUND_PORT)
    private readonly verifyjwtOutboundPort: VerifyJwtOutboundPort
  ) {
    super()
  }

  async validate(req: Request, payload: any) {
    const jwt = req.header("TestJwtKey")

    if (!jwt) {
      throw new AuthException(AuthJwtErrorCode.jwtNotFound())
    }

    this.verifyjwtOutboundPort.excute({ jwt, tokenType: JWT_TYPE.ACCESS_TOKEN })

    return payload
  }
}

덕분에 기존에 있던 JwtStrategy에서 직접 처리를 할 수 있게 되었다.
전에 passport-jwt사용할 때는 생성자에도 주렁주렁 매달아서 보냈었다.

constructor() {
        super({
            jwtFromRequest: ExtractJwt.fromExtractors([(req: Request) => req.header("TestJwtKey")]),
            secretOrKey: key,
            issuer: getJwtIssuer(JWT_TYPE.ACCESS_TOKEN),
            audience: getJwtAudience(),
            ignoreExpiration: false,
            passReqToCallback: true,
        } as StrategyOptions);
    }

이제는 Strategy영역이 좀 깔끔해 진 것 같다.

그리고 Nest.js에서 제공하는 JwtService의 경우 verify메서드로 검증을 할 수 있는데,
이때는 생성했던 JWT의 속성을 다 가지고 와야 한다.
속성은 issuer audience 등을 의미한다.

쉽게 생성할 때와 맞춰주면 된다.

return this.jwtService.sign(
  { data: payload },
  {
    secret: secretKey,
    expiresIn: expiredTime,
    audience: getJwtAudience(),
    issuer: getJwtIssuer(params.jwtType),
  }
)

이렇게 생성할 때 issuer audience를 전달했다면 검증할 때도 같이 넣어줘야 한다.

this.jwtService.verify(jwt, {
  secret,
  audience: getJwtAudience(),
  issuer: getJwtIssuer(JWT_TYPE.ACCESS_TOKEN),
} as JwtVerifyOptions)

이런 식으로 처리했다.
JwtVerifyOptions 타입을 타고 들어가면 아래와 같은 것을 확인할 수 있다.

verify<T extends object = any>(token: string, options?: JwtVerifyOptions): T;


export interface JwtVerifyOptions extends jwt.VerifyOptions {
    secret?: string | Buffer;
    publicKey?: string | Buffer;
}

export interface VerifyOptions {
    algorithms?: Algorithm[] | undefined;
    audience?: string | RegExp | Array<string | RegExp> | undefined;
    clockTimestamp?: number | undefined;
    clockTolerance?: number | undefined;
    /** return an object with the decoded `{ payload, header, signature }` instead of only the usual content of the payload. */
    complete?: boolean | undefined;
    issuer?: string | string[] | undefined;
    ignoreExpiration?: boolean | undefined;
    ignoreNotBefore?: boolean | undefined;
    jwtid?: string | undefined;
    /**
     * If you want to check `nonce` claim, provide a string value here.
     * It is used on Open ID for the ID Tokens. ([Open ID implementation notes](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes))
     */
    nonce?: string | undefined;
    subject?: string | undefined;
    maxAge?: string | number | undefined;
}

궁금점

기존에 AuthGuard부분에서 좀 더 조작하면 할 수 있을 것 같았는데 너무 시간을 빼먹어서 진행하지 않았다.

import { ExecutionContext, Injectable } from "@nestjs/common"
import { AuthGuard } from "@nestjs/passport"

@Injectable()
export class AccessJwtAuthGuard extends AuthGuard("accessJwt") {
  handleRequest<TUser = any>(
    err: any,
    user: any,
    info: any,
    context: ExecutionContext,
    status?: any
  ): TUser {
    if (info instanceof JsonWebTokenError) {
      console.log("errror = ")
      // return { "data : ": "test" } as any;
      return super.handleRequest(
        null,
        { "data : ": "test" },
        { "data : ": "aaaa" },
        context,
        undefined
      )
    }

    return super.handleRequest(err, user, info, context, status)
  }
}

기존에는 위와 같이 작성했는데 만료하는 시점은 잡을 수 있었지만 컨트롤러에서 바로 에러 응답을 해버렸다.

@Post("test")
    @UseGuards(AccessJwtAuthGuard)
    testJwt() {
        return { ok: "ok" };
    }

정녕 passport-jwt에서는 약간의 커스텀 처리를 못하는 것인지...
이 부분은 시간이 나면 다시 찾아봐야겠다.