Passport를 이용한 인증 구현
Passport.js는 Node.js를 위한 강력하고 유연한 인증 미들웨어로 NestJS와 결합하여 다양한 인증 전략을 쉽게 구현할 수 있게 해줍니다.
Passport.js 개념과 NestJS에서의 역할
Passport는 다양한 인증 방식을 '전략'이라는 개념으로 모듈화하여 제공합니다.
NestJS에서 Passport는 @nestjs/passport
모듈을 통해 통합되어, 선언적이고 모듈화된 방식으로 인증 로직을 구현할 수 있게 해줍니다.
NestJS 프로젝트에 Passport 설정
- 필요한 패키지 설치
npm install @nestjs/passport passport passport-local @types/passport-local
- 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) 구현
- 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;
}
}
- 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;
}
}
- 컨트롤러에 적용
@Controller('auth')
export class AuthController {
@UseGuards(AuthGuard('local'))
@Post('login')
async login(@Request() req) {
return req.user;
}
}
JWT 전략(JWT Strategy) 구현
- 필요한 패키지 설치
npm install @nestjs/jwt passport-jwt @types/passport-jwt
- 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 };
}
}
- 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 전략 구현
- 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 };
}
}
- 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. 정기적인 보안 감사
- 의존성 검사 및 업데이트
- 코드 리뷰 및 보안 테스트