NestJS

[NestJS] 사용자 인증 구현(Passport-local, passport JWT)

begong 2024. 11. 17. 22:45
반응형

Documentation | NestJS - A progressive Node.js framework

로컬 로그인 로직 구현

passport 및 passport local 설치

pnpm add @nestjs/passport passport passport-local
pnpm add -D @types/passport-local

bcrypt 설치

pnpm add bcrypt
pnpm add -D @types/bcrypt
  • bcrypt 설치 이유
    1. 용도
    • crypto: 범용 암호화 라이브러리로 해시함수, 대칭/비대칭 암호화 등 다양한 암호화 기능 제공
    • bcrypt: 비밀번호 해싱에 특화된 단방향 해시 함수
    1. 성능
    • crypto: 일반적으로 bcrypt보다 빠름. SHA-256 등의 해시함수는 고속 처리 가능
    • bcrypt: 의도적으로 느리게 설계됨. work factor 조절로 해싱 시간 조절 가능
      • 이는 무차별 대입 공격을 어렵게 만드는 장점
    1. 보안성
    • crypto:
      • 빠른 처리 속도로 인해 무차별 대입 공격에 취약할 수 있음
      • salt를 수동으로 관리해야 함
    • bcrypt:
      • 자동 salt 생성 및 관리
      • 느린 처리 속도로 무차별 대입 공격 방어에 유리
      • work factor 조절로 하드웨어 발전에 대응 가능
    1. 사용 예시:
    // crypto 예시
    const crypto = require('crypto');
    const hash = crypto.createHash('sha256').update('password').digest('hex');
    
    // bcrypt 예시
    const bcrypt = require('bcrypt');
    const hash = await bcrypt.hash('password', 10); // 10은 work factor
    
    
    결론:
    • 비밀번호 해싱: bcrypt 권장 (보안성 우수)
    • 일반 데이터 암호화/해싱: crypto 권장 (다양한 기능, 좋은 성능)

localStrategy 생성

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';

import { AuthService } from '../auth.service';
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;
  }
}
  • PassportStrategy 를 상속받아 localStrategy를 생성
  • validate() 함수 안에 전략(검증로직)을 넣으면 됨
    • 주요 검증 로직은 service layer에 구현
  • 반환되는 데이터는 Request객체의 user속성으로 들어감 Request.user ⇒ 인증 후 반환해줄 타입에 대한 회의 필요!

service layer 에서 비밀번호 검증로직 실행

import { Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';

import { UserService } from '@/user/user.service';

@Injectable()
export class AuthService {
  constructor(private userService: UserService) {}

  async validateLocalLogin(username: string, inputPassword: string) {
    const user = await this.userService.findUserByUsername(username);
    if (!user) {
      throw new UnauthorizedException('잘못된 로그인 정보');
    }
    const isPasswordValid = await bcrypt.compare(inputPassword, user.password);
    if (!isPasswordValid) {
      throw new UnauthorizedException('잘못된 로그인 정보');
    }
    const { password, ...result } = user;
    return result;
  }
}
  • bcrypt 모듈을 통해 로그인 정보 검증

localLogin 함수에 UseGuards를 통해 passport 연결

//auth.controller.ts
import { LocalAuthGuard } from './local/local-auth.guard';
@UseGuards(LocalAuthGuard)
  async localLogin(@Request() req) {
    return {
      status: 'success',
      data: await this.authService.login(req.user),
    };
  }
  

//local-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
  • UseGuards 어노테이션을 통해 passport 전략을 넣을 수 있음.
  • 공식문서에는 하단의 두 방법이 나와있음.
  • @UseGuards(AuthGuard('local')) vs @UseGuards(LocalAuthGuard)
    • 커스터마이징 가능성:
      • AuthGuard('local'): 기본 Passport 가드를 직접 사용하며, 커스터마이징이 제한적
      • LocalAuthGuard: 클래스를 확장하여 추가 로직이나 에러 핸들링을 구현할 수 있음
    • 코드 재사용성:
      • AuthGuard('local'): 매번 'local' 문자열을 직접 입력해야 함
      • LocalAuthGuard: 재사용 가능한 클래스로, 타입 안정성이 더 높음
    • 유지보수성:
      • AuthGuard('local'): 변경이 필요할 때 사용된 모든 곳을 수정해야 함
      • LocalAuthGuard: 한 곳에서 로직을 관리할 수 있어 유지보수가 용이
    • 에러 핸들링:
      • AuthGuard('local'): 기본 에러 핸들링만 가능
      • LocalAuthGuard: handleRequest 메소드를 오버라이드하여 커스텀 에러 핸들링 가능
    • 의존성 주입:
      • LocalAuthGuard는 NestJS의 DI 시스템을 활용할 수 있어, 다른 서비스를 주입받아 사용 가능
  • 확장성도 좋고, 공식문서의 권장도 후자이니 @UseGuards(LocalAuthGuard) 를 사용!!

⇒ 다음은 local 로그인 성공 시 일어날 로직 구현이다.

JWT 설치

pnpm add @nestjs/jwt passport-jwt
pnpm add -D @types/passport-jwt

JWT 모듈에 import, 환경변수 세팅

//auth.module.ts
@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: { expiresIn: '99d' },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, LocalStrategy], 
})
  • config module을 사용중이기 때문에 다음과 같은 방식으로 환경변수를 넣어주었다.
  • localStrategy에서 validate를 한 이후 데이터를 받기 때문에 provider에 localStrategy를 추가해준다.

Auth.service에 createJWT 함수 추가

//auth.service.ts
import { JwtService } from '@nestjs/jwt';

constructor(
    private jwtService: JwtService
  ) {}
  
async createJWT(user: Omit<User, 'password'>) {
    const payload: JWTPayload = { username: user.username, sub: user.id, email: user.email };
    return {
      access_token: this.jwtService.sign(payload), //주입 받은 jwtService 사용
    };
  }
}
  • 로그인 성공시 로직에 필요한 jwt토큰 생성 함수를 만든다.

localLogin함수에 로그인 성공 시 로직 추가

//auth.controller.ts
import { LocalAuthGuard } from './local/local-auth.guard';
@UseGuards(LocalAuthGuard)
  async localLogin(@Request() req) {
    return {
      status: 'success',
      data: await this.authService.createJWT(req.user),
    };
  }
  • validate()함수의 리턴값이 Request객체의 user속성에 들어간다.
  • req.user속성을 파라미터로 받아 위에서 작성한 createJWT() 함수를 통해 토큰을 생성하고 응답한다.

⇒ 이렇게 local 로그인에 대한 인증 및 로그인 로직은 끝났다.

⇒ 다음은 응답받은 jwt를 활용해 요청이 올 때 jwt를 검사하는 로직이다.

JWT

  • JWT 모듈에대한 세팅은 위에서 완료해주었으니 생략

JWT 전략 생성

//auth/jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }

  async validate(payload) {
    return {
      id: payload.sub,
      username: payload.username,
      email: payload.email,
    };
  }
}
  • jwtFromRequest : JWT가 Request에서 추출되는 방법.
    • 현재 코드에서는 authHeader에서 brear토큰을 제공하는 방식으로 지정
  • ignoreExpiration : JWT토큰 만료 확인
    • 만료된 경우 에러 전송
  • secretOrKey : 토큰서명 비밀키
  • 위의 세 옵션처럼 , 토큰추출→ 토큰 만료여부 확인 → 토큰서명 검증순으로 진행
    • 검증실패 시 401 에러 전송
  • local login과는 다르게 validate의 파라미터로 토큰안에 있는 payload가 들어온다.
  • payload에 있는 내용은 변경되거나 할 경우는 거의 없기 때문에 그대로 서비스로직으로 넘겨주어도 된다.
    • 추가적인 검증이 서비스로직에서 필요하다면 validate에 추가해주면된다.

auth.module에 공급자로 추가

//auth.module.ts
providers: [AuthService, LocalStrategy, JwtStrategy]
  • local login과 마찬가지로 validate가 종료되면 데이터를 strategy로 받기때문에 추가해준다.

JwtAuthGuard 생성

//auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

사용

@Controller()
export class AppController {
  constructor(private authService: AuthService) {}

  @UseGuards(JwtAuthGuard)
  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
  }
  • local Login과 마찬가지로 @UseGuards 를 사용하여 필요한 기능에 붙인다.
반응형

'NestJS' 카테고리의 다른 글

[NestJS] Global-guard  (0) 2024.11.17
[NestJS] Github OAuth 구현  (0) 2024.11.17
[NestJS] TypeORMError: Entity metadata for User#applicants was not found  (0) 2024.11.10