icon

Passport를 이용한 인증 구현


 Passport.js는 Node.js를 위한 강력하고 유연한 인증 미들웨어로 NestJS와 결합하여 다양한 인증 전략을 쉽게 구현할 수 있게 해줍니다.

Passport.js 개념과 NestJS에서의 역할

 Passport는 다양한 인증 방식을 '전략'이라는 개념으로 모듈화하여 제공합니다.

 NestJS에서 Passport는 @nestjs/passport 모듈을 통해 통합되어, 선언적이고 모듈화된 방식으로 인증 로직을 구현할 수 있게 해줍니다.

NestJS 프로젝트에 Passport 설정

  1. 필요한 패키지 설치
npm install @nestjs/passport passport passport-local @types/passport-local
  1. AuthModule 생성
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
 
@Module({
  imports: [PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

로컬 전략(Local Strategy) 구현

  1. LocalStrategy 클래스 생성
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
 
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super();
  }
 
  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}
  1. AuthService 구현
@Injectable()
export class AuthService {
  constructor(private usersService: UsersService) {}
 
  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}
  1. 컨트롤러에 적용
@Controller('auth')
export class AuthController {
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return req.user;
  }
}

JWT 전략(JWT Strategy) 구현

  1. 필요한 패키지 설치
npm install @nestjs/jwt passport-jwt @types/passport-jwt
  1. JwtStrategy 클래스 생성
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { jwtConstants } from './constants';
 
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: jwtConstants.secret,
    });
  }
 
  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}
  1. JWT 토큰 생성 및 인증
@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService
  ) {}
 
  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

다양한 Passport 전략 구현

  1. OAuth 전략 (예 : Google OAuth)
import { Strategy } from 'passport-google-oauth20';
import { PassportStrategy } from '@nestjs/passport';
 
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor() {
    super({
      clientID: 'YOUR_CLIENT_ID',
      clientSecret: 'YOUR_CLIENT_SECRET',
      callbackURL: 'http://localhost:3000/auth/google/callback',
      scope: ['email', 'profile'],
    });
  }
 
  async validate(accessToken: string, refreshToken: string, profile: any) {
    // 사용자 정보 처리 로직
    return { userId: profile.id, email: profile.emails[0].value };
  }
}
  1. Social Login (예 : Facebook)
import { Strategy } from 'passport-facebook';
import { PassportStrategy } from '@nestjs/passport';
 
@Injectable()
export class FacebookStrategy extends PassportStrategy(Strategy, 'facebook') {
  constructor() {
    super({
      clientID: 'YOUR_APP_ID',
      clientSecret: 'YOUR_APP_SECRET',
      callbackURL: 'http://localhost:3000/auth/facebook/callback',
      scope: 'email',
      profileFields: ['emails', 'name'],
    });
  }
 
  async validate(accessToken: string, refreshToken: string, profile: any) {
    // 사용자 정보 처리 로직
    return { userId: profile.id, email: profile.emails[0].value };
  }
}

커스텀 가드 생성

import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
 
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  canActivate(context: ExecutionContext) {
    // 추가적인 로직
    return super.canActivate(context);
  }
}
 
// 사용
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
  return req.user;
}

인증 실패 시 예외 처리

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    return user;
  }
}

세션 기반 vs 토큰 기반 인증

 1. 세션 기반 인증

  • 서버에 사용자 상태 저장
  • 메모리 사용량 증가
  • 구현 예
import * as session from 'express-session';
import * as passport from 'passport';
 
// main.ts
app.use(
  session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: false,
  }),
);
app.use(passport.initialize());
app.use(passport.session());

 2. 토큰 기반 인증 (JWT)

  • 클라이언트에 상태 저장
  • 서버의 무상태성 유지
  • 구현은 앞서 설명한 JWT 전략 참조

Best Practices와 보안 고려사항

 1. 안전한 비밀번호 저장

  • bcrypt 등의 해시 함수 사용
import * as bcrypt from 'bcrypt';
 
@Injectable()
export class AuthService {
  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && await bcrypt.compare(password, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

 2. HTTPS 사용 : 모든 인증 관련 통신은 HTTPS로 암호화

 3. JWT 토큰 만료 시간 설정

const token = this.jwtService.sign(payload, { expiresIn: '1h' });

 4. 리프레시 토큰 구현

  • 액세스 토큰의 수명을 짧게 유지하고, 리프레시 토큰으로 갱신

 5. CORS 설정

app.enableCors({
  origin: 'https://your-frontend-domain.com',
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  credentials: true,
});

 6. 레이트 리미팅 구현

import rateLimit from 'express-rate-limit';
 
app.use(
  rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // limit each IP to 100 requests per windowMs
  }),
);

 7. 로깅 및 모니터링

  • 인증 시도, 성공, 실패 등을 로깅
  • 비정상적인 패턴 모니터링

 8. 다단계 인증 (MFA) 고려

  • 중요한 작업에 대해 추가 인증 단계 구현

 9. 안전한 토큰 저장

  • 클라이언트에서 토큰을 안전하게 저장 (HttpOnly 쿠키 등)

 10. 정기적인 보안 감사

  • 의존성 검사 및 업데이트
  • 코드 리뷰 및 보안 테스트