개발/Node.js

[Nest.js] Login Authentication 구현 ( passport + jwt strategy )

nofunfromdev 2022. 4. 6. 00:26
 

[Nest.js] Login Authentication 구현 ( passport + jwt + local strategy )

사용자인증은 대부분의 애플리케이션에서 필수적인 부분이다. 인증을 처리하는 방법은 다양하지만 Nest.js 공식홈페이지에서 passport를 안내하고 있어 필자는 passport와 jwt를 선택했다. jwt 전략을

nofunfromdev.tistory.com

Nest.js로 Login 구현하기 (2)

이전글에서는 데이터베이스에서 사용자를 검색하여 로그인부분까지 구현하였다. 이번글에서는 로그인 성공하고 사용자 정보가 아닌 jwt를 반환하고 API 호출 시 유효한 토큰인지 검증하려고 한다.

패키지 설치 및 모듈 생성

$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt

JwtModule 및 token 생성

// auth/auth.service.ts
import { Injectable } from '@nestjs/common';
import { UserService } from '../user/user.service';
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';

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

  async validateUser(id: string, password: string): Promise<any> {
    const user = await this.userService.findOne(id);
    if (!(await bcrypt.compare(password, user?.password ?? ''))) {
      return null;
    }
    return user;
  }

  async login(user: any) {
    const payload = { userUid: user.userUid };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
// app.controller.ts
import { Controller, Post, UseGuards, Request } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { LocalAuthGuard } from './auth/guard/local-auth.guard';

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

  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }
}

login()를 추가하여 AppController에서 호출한다. passport-local전략으로 비밀번호가 맞는지 확인하였고 jwt를 응답한다.
이때 JwtServiceAuthService에 주입시키려면 JwtModule을 추가해야한다.

$ npm i --save @nestjs/config
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { LocalStrategy } from './local.strategy';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        secret: config.get<string>('JWT_SECRET_KEY'),
        signOptions: { expiresIn: '60s' },
      }),
    }),
  ],
  providers: [AuthService, LocalStrategy],
  exports: [AuthService],
})
export class AuthModule {}

나는 환경변수 값을 불러오기 위해 ConfigService를 사용하였다.

$ curl -X POST http://localhost:3000/auth/login -d '{"id": "nofunfromdev", "password": "test"}' -H "Content-Type: application/json
$ # {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyVWlkIjoxLCJpYXQiOjE2NDkxNzA2NjAsImV4cCI6MTY0OTE3MDcyMH0.9xAr3khXvnXKTDlnOp8Aav1OybB_En9cZJ-QZhw8ku8"}

passport jwt Strategy

passport-jwt strategy을 사용하여 Request에 유효한 jwt가 존재하도록 요구하여 엔드포인트를 보호할 수 있다. 

// auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('JWT_SECRET_KEY'),
    });
  }

  async validate(payload: any) {
    return { userUid: payload.userUid };
  }
}

super()에 초기 옵션을 설정한다.

 

jwtFromRequest : Request에서 JWT를 추출하는 방법을 설정한다. Authorization header에 bearer이 표준이다.
ignoreExpiration : 기본적으로 fasle 설정을 한다. 이 설정은 JWT 검증을 Passport모듈에 위임한다. 만료된 JWT가 제공되면 Request는 거부되고 401 Unauthorized 응답이 전송된다.
secretOrKey: 토큰 발급에 쓰일 시크릿 키. 노출금지!

 

validate(): jwt-strategy인경우 passport는 먼저 JWT의 서명을 확인하고 JSON을 해독한다. 그 후 디코딩된 JSON을 단일파라미터로 가지는 validate()를 호출한다. 반환값으로 Request 객체에 user의 속성으로 설정된다. ex) req.user

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

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

jwt guard를 생성하고 엔드포인트에 추가한다.

import { Controller, Post, UseGuards, Request, Get } from '@nestjs/common';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/guard/jwt-auth.guard';
import { LocalAuthGuard } from './auth/guard/local-auth.guard';

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

  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Request() req) {
    return this.authService.login(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('me')
  me(@Request() req) {
    return req.user;
  }
}
$ curl http://localhost:3000/me -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyVWlkIjoxLCJpYXQiOjE2NDkxNzA2NjAsImV4cCI6MTY0OTE3MDcyMH0.9xAr3khXvnXKTDlnOp8Aav1OybB_En9cZJ-QZhw8ku8"
$ # {"userUid" : 1}

validate()에서 반환되는 값은 자동으로 사용자 개체를 생성하여 req.user로 Request에 할당된다.
터미널에서 실행하면 정상적으로 사용자 값이 return 된다.

 

Nest.js Authentication

 

반응형