NestJS에서 JWT(JSON Web Token), Passport, Strategy 패턴을 사용하여 인증을 구현하는 방법에 대해 알아보고, 각 패턴의 장점과 구현 방식, 그리고 더 나아가 활용할 수 있는 방향에 대해 논의합니다.

JWT(JSON Web Token)

JWT(JSON Web Token)는 웹 표준(RFC 7519)으로서, 당사자 간에 JSON 객체로 정보를 안전하게 전송하는 방법을 정의합니다. JWT는 암호화된 토큰을 사용하여 정보를 안전하게 전달하며, 주로 인증 및 권한 부여에 사용됩니다. NestJS에서는 @nestjs/jwt 패키지를 통해 JWT를 간편하게 구현하고 사용할 수 있습니다.

장점

  • 간결성: JWT는 필요한 정보를 JSON 형태로 담아 간결하게 표현할 수 있습니다.
  • 확장성: JWT는 다양한 정보를 payload에 담을 수 있어 확장성이 뛰어납니다.
  • 보안성: JWT는 서명을 통해 위변조를 방지할 수 있어 안전합니다.

구현 방식

NestJS에서 JWT를 사용하려면 @nestjs/jwt 패키지를 설치해야 합니다.

npm install @nestjs/jwt

AuthService에서 JWT를 생성하고 검증하는 로직을 구현합니다.

import { JwtService } from '@nestjs/jwt';
 
@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}
 
  async generateToken(payload: any): Promise<string> {
    return this.jwtService.sign(payload);
  }
 
  async verifyToken(token: string): Promise<any> {
    return this.jwtService.verify(token);
  }
}

더 활용할 수 있는 방향

  • Refresh Token: Access Token의 유효 기간을 짧게 설정하고, Refresh Token을 사용하여 Access Token을 갱신하는 방식으로 보안성을 강화할 수 있습니다.
  • Payload 확장: JWT payload에 사용자 정보 외에 권한 정보, 서비스 정보 등을 추가하여 다양한 용도로 활용할 수 있습니다.

Passport

Passport는 Node.js를 위한 인증 미들웨어입니다. Passport는 다양한 인증 전략(Strategy)을 제공하며, 이를 통해 로컬 인증, OAuth 2.0, OpenID Connect 등 다양한 인증 방식을 지원합니다.

장점

  • 다양한 인증 방식 지원: Passport는 다양한 인증 전략을 제공하여 개발자가 원하는 인증 방식을 쉽게 구현할 수 있도록 돕습니다.
  • 모듈화된 구조: Passport는 인증 로직을 모듈화하여 관리할 수 있도록 돕습니다.
  • 유연성: Passport는 다양한 미들웨어와 함께 사용할 수 있어 유연성이 뛰어납니다.

구현 방식

NestJS에서 Passport를 사용하려면 @nestjs/passport 패키지를 설치해야 합니다.

npm install @nestjs/passport passport-jwt

CookieJwtStrategy를 사용하여 JWT Strategy를 구현하고, 쿠키에서 JWT를 추출하는 방법을 설정합니다.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
 
const cookieExtractor = (req: Request): string | null => {
  let token = null;
  if (req && req.cookies) {
    token = req.cookies['aToken']; // Use 'accessToken' cookie name
  }
  return token;
};
 
@Injectable()
export class CookieJwtStrategy extends PassportStrategy(Strategy, 'cookie-jwt') {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: cookieExtractor, // Use cookie extractor
      ignoreExpiration: false,
      passReqToCallback: true,
      secretOrKeyProvider: (req, rawJwtToken, done) => {
        const key = this.configService.get<string>('JWT_ACCESS_KEY');
        if (!key) {
          return done(new Error('JWT_ACCESS_KEY not configured'), null);
        }
        done(null, key);
      },
    });
  }
 
  async validate(req: Request, payload: any): Promise<any> {
    return payload;
  }
}

더 활용할 수 있는 방향

  • OAuth 2.0 연동: Passport를 사용하여 Google, Facebook, Kakao 등 OAuth 2.0을 지원하는 다양한 서비스와 연동할 수 있습니다.
  • Custom Strategy 구현: Passport에서 제공하는 기본 Strategy 외에 Custom Strategy를 구현하여 특정 서비스에 특화된 인증 방식을 지원할 수 있습니다.

Strategy 패턴

Strategy 패턴은 알고리즘군을 정의하고 각각을 캡슐화하여 필요에 따라 교환할 수 있게 하는 패턴입니다. Passport는 Strategy 패턴을 사용하여 다양한 인증 방식을 지원합니다.

장점

  • 유연성: Strategy 패턴을 사용하면 인증 방식을 쉽게 변경할 수 있습니다.
  • 확장성: 새로운 인증 방식을 추가하는 것이 용이합니다.
  • 유지보수성: 인증 로직이 모듈화되어 있어 유지보수가 용이합니다.

구현 방식

CookieJwtStrategyPassportStrategy를 상속받아 JWT Strategy를 구현합니다. validate 메서드에서는 토큰을 검증하고 사용자 정보를 반환합니다.

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { Request } from 'express';
import { ConfigService } from '@nestjs/config';
 
const cookieExtractor = (req: Request): string | null => {
  let token = null;
  if (req && req.cookies) {
    token = req.cookies['aToken'];
  }
  return token;
};
 
@Injectable()
export class CookieJwtStrategy extends PassportStrategy(Strategy, 'cookie-jwt') {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: cookieExtractor,
      ignoreExpiration: false,
      passReqToCallback: true,
      secretOrKeyProvider: (req, rawJwtToken, done) => {
        const key = this.configService.get<string>('JWT_ACCESS_KEY');
        if (!key) {
          return done(new Error('JWT_ACCESS_KEY not configured'), null);
        }
        done(null, key);
      },
    });
  }
 
  async validate(req: Request, payload: any): Promise<any> {
    return payload;
  }
}

CookieJwtAuthGuardAuthGuard를 상속받아 인증 가드를 구현합니다. canActivate 메서드에서는 인증을 수행하고, handleRequest 메서드에서는 인증 실패 시 예외를 처리합니다. 특히, Access Token이 만료된 경우 Refresh Token을 사용하여 Access Token을 갱신하는 로직이 포함되어 있습니다.

import { Injectable, UnauthorizedException, Inject } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { clearTokenCookies } from '../utils/cookie.helper';
import { Request, Response } from 'express';
import { UserDto } from '../dto/user.dto';
 
@Injectable()
export class CookieJwtAuthGuard extends AuthGuard('cookie-jwt') {
  constructor(
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
  ) {
    super();
  }
 
  handleRequest(err: any, user: any, info: any, context: ExecutionContext, status?: any): any {
    const request = context.switchToHttp().getRequest<Request>();
    const response = context.switchToHttp().getResponse<Response>();
 
    // Access Token 만료 처리
    if (info instanceof TokenExpiredError) {
      console.log('CookieJwtAuthGuard: Access token expired, attempting refresh...');
      return this.handleTokenRefresh(request, response);
    }
 
    if (err || !user) {
      clearTokenCookies(response, this.configService);
      throw err || new UnauthorizedException();
    }
 
    return user;
  }
}

AuthController에서는 @UseGuards 데코레이터를 사용하여 인증 가드를 적용합니다. 다음은 AuthController에서 인증 가드를 사용하는 예시입니다.

import { Controller, Get, UseGuards, Req, Res } from '@nestjs/common';
import { CookieJwtAuthGuard } from '@flow/auth';
import { Request, Response } from 'express';
 
@Controller('auth')
export class AuthController {
  @Get('profile')
  @UseGuards(CookieJwtAuthGuard)
  getProfile(@Req() req: Request) {
    return req.user;
  }
}

더 활용할 수 있는 방향

  • 다양한 인증 로직 구현: Strategy 패턴을 사용하여 다양한 인증 로직을 구현할 수 있습니다. 예를 들어, 사용자 IP 주소를 검증하거나, 사용자 Agent를 검증하는 등의 로직을 추가할 수 있습니다.
  • Custom Strategy 구현: Passport에서 제공하는 기본 Strategy 외에 Custom Strategy를 구현하여 특정 서비스에 특화된 인증 방식을 지원할 수 있습니다.

결론

NestJS에서 JWT, Passport, Strategy 패턴을 사용하여 인증을 구현하면, 간결하고 확장 가능하며 안전한 인증 시스템을 구축할 수 있습니다. 각 패턴의 장점을 이해하고 적절하게 활용하면, 개발 생산성을 높이고 애플리케이션의 보안성을 강화할 수 있습니다.

읽어주셔서 감사합니다!