최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday

티스토리 뷰

nestjs 에서 passport를 사용한 local 인증 구현 흐름 분석

 

Passport local authentication flowchart

passport 에서 아쉬운 점

1. Request 객체의 user key로 결과값을 반환 받는데, 공식문서에서 설명이 미흡한 점.

2. 위의 1번 항목, local.strategy 에서, user key 대신 다른값으로 받을 수 있도록 속성 변경 못하는 점(내가 못찾았거나)

3. Interface를 만들어서 usernameField, passwordField, requestField 로 기본 제공되었다면 어땠을까 함.

   위의 3번 항목, 직접 사용자가 인터페이스를 구현하여 보완 하면 사용과 분석이 편리할 듯 함.

 

Source code

필요패키지 설치 후, 모듈과 서비스를 cli로 설정

$ npm install --save @nestjs/passport passport passport-local
$ npm install --save-dev @types/passport-local
$ nest g module auth
$ nest g service auth
$ nest g module users
$ nest g service users

 

하드 코딩된 내용으로 사용자 목록과 사용자 이름으로 find 메서드를 사용하여 검색 기능을 만든다.

실제 앱에서는 TypeORM, Sequelize, Mongoose 등을 사용하여 모델을 구축한다.

 

users/users.service.ts

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

// This should be a real class/interface representing a user entity
export type User = any;

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

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

 

UsersModule 에서 이 모듈을 외부에서 사용할 수 있도록 UsersService를 내보기로(exports) 추가해야 한다.

users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';

@Module({
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

 

AuthService는 사용자를 검색하고 암호 확인하는 작업이 포함되어 있다. validateUser() 메서드를 만들고

ES6 스프레드 연산자를 이용해서 { pasword와 ...나머지 }로 편하게 사용자 객체에서 암호를 제거할 수 있다.

validateUser() 는 Passport 로컬 전략의 메서드에서 호출하면 된다.

 

auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@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;
  }
}

 

이제 AuthModule 에서 UsersModule을 가져오도록 업데이트한다. 

 

auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from 'src/users/users.module';

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

 

Passport 구현. 이제 로컬 인증 전략을 구현하자. auth 폴더에 local.strategy.ts 파일을 만들고 다음의 코드를 추가.

 

auth/local.strategy.ts

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;
  }
}

super() 는 옵션 없이 단순히 객체를 호출한다.

기본값은 username과 password 이다. 다른 이름을 사용하려면 super({ usernameField: 'email', userpasswordField: 'pass' }) 로 다른 속성 이름 지정 가능하다.

 

AuthModule에서 방금 정의한 Passport 기능 사용하도록 구성한다.

 

 

auth/auth.module.ts

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

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

 

로그인 경로

/auth/login 기본 경로를 설정하고, 내장 Guard 를 적용하도록 한다.

 

auth/auth.controller.ts

import { Controller, Request, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller()
export class AppController {
  @UseGuards(AuthGuard('local'))
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }
}

API 를 호출해보면

$ # POST to /auth/login
$ curl -X POST http://localhost:3000/auth/login -d '{"username": "john", "password": "changeme"}' -H "Content-Type: application/json"
$ # result -> {"userId":1,"username":"john"}

 

AuthGuard() 에 직접 전략이름을 인자로 넣으면 코드베이스에 매직 문자열이 도입된다.

대신 아래와 같이 고유한 클래스를 만드는것을 권장한다.

 

auth/local-auth.guard.ts

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

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

 

이제 /auth/login 에 LocalAuthGuard를 반영하고 콘트롤러를 업데이트하자

 

auth/auth.controller.ts

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

@Controller()
export class AuthController {
  //@UseGuards(AuthGuard('local')) => auth/local-auth.guard.ts 클래스로 만드는것을 권장
  @UseGuards(LocalAuthGuard)
  @Post('auth/login')
  async login(@Request() req) {
    return req.user;
  }
}

 

결과)

 

POST /auth/login 201
POST auth/login 401

댓글