dev/nestjs

passport, jwt

wlrn566 2023. 2. 10. 01:37

로그인 인증을 위해 passport 라이브러리를 사용한다. 인증이 확인되면 헤더의 인증토큰을 위해 jwt를 발행한다. 요청에서 유효한 jwt인지 확인한다.

 

passport 패키지를 설치한다..

 

npm install --save-dev @types/passport-local
npm install --save @nestjs/passport passport passport-local

 

auth 모듈과 서비스를 생성한다.

 

nest g module auth
nest g service auth

 

UserService에 유저를 확인하는 메서드 하나를 만들어준다.

 

// user.service.ts

export type User = any;

@Injectable()
export class UserService {
  private readonly users = [
    {
      userId: 1,
      username: 'john',
      password: 'j',
    },
    {
      userId: 2,
      username: 'maria',
      password: 'm',
    }
  ];

  async findOne(
    username: string
  ): Promise<User | undefined> {
    console.log('findOne');
    return this.users.find(user => user.username === username);
  }
}

 

UserService를 export 해줘서 다른 모듈에서 사용할 수 있게 해준다.

 

// user.module.ts

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

 

AuthService에 validateUser()를 생성하고 유저 확인 로직을 넣어준다. 이 메서드는 Passport local strategy 에서 호출할 것이다.

실제 어플리케이션에서 비밀번호는 bcrypt 같은 라이브러리를 이용해 해싱된 값으로 처리해야한다.

 

// auth.service.ts

import { UserService } from 'src/user/user.service';
import { Injectable } from '@nestjs/common';

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

  async validateUser(
    username: string,
    pass: string
  ): Promise<any> {
    console.log('AuthService validateUser()');
    const user = await this.userService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }
}

 

AuthService를 export 해준다.

 

// auth.module.ts

@Module({
  imports: [UserModule],
  providers: [AuthService],
})
export class AuthModule {}

 

local.strategy.ts 파일을 만들고 Passport local strategy 를 구현한다.

super()는 Passport local strategy에서 옵션 개체를 사용자 지정하기 위해 사용된다. 따로 지정할 것이 없으면 그냥 둔다.

ex) username이 아닌 email 을 검사하고 싶다면 super({ usernameField: 'email' }) 을 추가한다.

 

// local.strategy.ts

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

@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);
    console.log(user);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

 

AuthModule에 PassportModule을 import하고 providers 에 LocalStrategy를 등록한다.

 

// auth.module.ts

import { LocalStrategy } from './local.strategy';
import { UserModule } from './../user/user.module';
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [UserModule, PassportModule],
  providers: [AuthService, LocalStrategy],
})
export class AuthModule {}

 

AppController 에 가드를 적용시킨다.

 

// app.controller.ts

import { Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @UseGuards(AuthGuard('local')) // @nestjs/passport
  @Post('auth/login')
  async login(
    @Req() req
  ) {
    return req.user;
  }
}

 

body에 username과 password를 제대로 적고 해당 경로로 로그인을 시도하면 user 정보가 출력된다.

 

다만, 전략이름을 직접 전달하는 것보다 클래스를 만들어 주는 것을 권유하고 있다.

 

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport/dist';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}
  // @UseGuards(AuthGuard('local')) // @nestjs/passport
  @UseGuards(LocalAuthGuard) // 위처럼 전략을 직접 적지 않고 클래스로 따로 빼내놓는 것이 좋음
  @Post('auth/login')
  async login(
    @Req() req
  ) {
    return req.user;
  }

 

각각 콘솔을 찍어보면 어떤 흐름으로 되는지 알 수 있다.

 

 

http://localhost:3333/auth/login -> 가드 : LocalStrategy ->

LocalStrategy의 validate() 메서드 내부 authService.validateUser()

-> username, password 검사 -> 일치하는 유저 있으면 유저 정보 리턴

 

 

JWT를 발행하기 위해 패키지를 설치한다.

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

 

AuthService에 메서드를 추가한다.

 

// auth.service.ts

import { UserService } from 'src/user/user.service';
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

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

  async validateUser(
    username: string,
    pass: string
  ): Promise<any> {
    console.log('AuthService validateUser()');
    const user = await this.userService.findOne(username);
    if (user && user.password === pass) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  // jwtService
  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      // jwt 발행
      access_token: this.jwtService.sign(payload),
    }
  }
}

 

AuthModule에 의존성을 주입한다.

 

// auth.module.ts

import { LocalStrategy } from './local.strategy';
import { UserModule } from './../user/user.module';
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.register({
      secret: 'secret key',
      signOptions: {
        expiresIn: '60s', // 토큰 만료기간
      },
    }),
  ],
  providers: [AuthService, LocalStrategy],
  exports: [AuthService],
})
export class AuthModule {}

 

AppController의 로그인 메서드를 AuthService의 login()을 리턴한다.

 

// app.controller.ts

import { AuthService } from './auth/auth.service';
import { Controller, Post, Req, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LocalAuthGuard } from './auth/local-auth.guard';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly authService: AuthService,
  ) {}

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

 

API 를 실행하면 토큰이 발급된다.

 

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImpvaG4iLCJzdWIiOjEsImlhdCI6MTY3NTk1NzM0NSwiZXhwIjoxNjc1OTU3NDA1fQ.o--wEa0TPLreHXEhCLTguKtiixDSgwmsacyrOrPHFu0"
}

 

jwt.strategy.ts 파일을 만들고 passport-jwt 전략을 구현한다.

생성자 super()를 통해 옵션을 넣어준다.

 

 

// jwt.strategy.ts

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // jwt 추출방법 : Authorization 헤더의 Bearer 토큰
      ignoreExpiration: false, //true : Passport에 토큰 검증을 위임하지 않고 직접 검증, false : Passport에 검증 위임
      secretOrKey: 'secret key', // 토큰 대칭 비밀키
    });
  }

  async validate(payload: any) {
    console.log('JwtStrategy validate()');
    return { userId: payload.sub, username: payload.username };
  }
}

 

AuthModule에 Jwt전략을 추가한다.

 

// auth.module.ts

@Module({
  imports: [
    UserModule,
    PassportModule,
    JwtModule.register({
      secret: 'secret key',
      signOptions: {
        expiresIn: '60s', // 토큰 만료기간
      },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy], // new
  exports: [AuthService],
})
export class AuthModule {}

 

jwt가드 클래스를 만든다.

 

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

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

 

AppController에 유저 정보를 가져오는 메서드를 만들고 jwt가드를 등록해준다.

 

// app.controller.ts

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

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly authService: AuthService,
  ) {}

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

  @UseGuards(JwtAuthGuard)
  @Get('user-info')
    getUserInfo(
    @Req() req
  ) {
    return this.userService.findOne(req.user['username']);
  }
}

 

username과 password를 통해 등록된 유저를 확인하는 로그인 후 발행된 토큰을 이용해서 유저 정보를 조회해본다.

유저 정보와 함께 토큰을 주기 위해 AuthService와 AppController를 수정한다.

 

// auth.service.ts

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

  async validateUser(
    username: string,
    pass: string
  ): Promise<any> {
    console.log('AuthService validateUser()');
    const user = await this.userService.findOne(username);

    if (user && user.password === pass) {
      const { password, ...result } = user;
      const accessToken = await this.jwtService.sign(result); // 토큰 발생

      result['token'] = accessToken; // 토큰 추가

      return result;
    }
    return null;
  }
}

// app.controller.ts

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly authService: AuthService,
    private readonly userService: UserService,
  ) {}

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

  @UseGuards(JwtAuthGuard)
  @Get('user-info')
    getUserInfo(
    @Req() req
  ) {
    return this.userService.findOne(req.user['username']);
  }
}

 

auth/login 으로 요청하면 유저 정보와 토큰을 응답해준다.

이 토큰을 가지고 user-info 를 요청한다.

헤더에 Authorization : Bearer '토큰값' 을 넣어야 한다.

 

{
    "userId": 2,
    "username": "maria",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjIsInVzZXJuYW1lIjoibWFyaWEiLCJpYXQiOjE2NzU5NjAzNTYsImV4cCI6MTY3NTk2MDQxNn0.iL3eNYlngFV6ODdcDW09__2dek3ssyfq8rlwR2_P3wc"
}

 

{
    "userId": 2,
    "username": "maria",
    "password": "m"
}

 

만약 유효하지 않은 토큰이라면 오류가 뜬다.

 

{
    "statusCode": 401,
    "message": "Unauthorized"
}