노현진's Blog

NestJS 패스포트 및 세션 사용 방법

NestJS에서 패스포트 및 세션 사용 방법에 대해 설명하는 페이지입니다.

Posted
Preview Image
By HyunJinNo

Tags

TypeScript, NestJS, Passport, Session

Environment

Node.js v20.11.1

1. 개요

NestJS에서 패스포트 및 세션 사용 방법에 대해 정리한 페이지입니다.

2. 패스포트(Passport)란?

NestJS에서 패스포트(Passport)는 다양한 인증 전략을 간편하게 구현할 수 있게 해주는 인증 미들웨어입니다. 패스포트는 로컬(Local), OAuth, JWT(JSON Web Token) 등 다양한 인증 방식을 지원합니다. 패스포트를 사용하면 인증 로직을 쉽게 분리해서 개발할 수 있습니다.

strategy는 패스포트에서 인증 로직 수행을 담당하는 클래스를 의미하며, 패스포트 사용 시 인증 로직은 Strategy 파일을 생성해서 사용합니다.

3. 세션(Session)이란?

세션(Session)이란 서버 측에서 사용자의 상태를 저장하는 방식으로, 사용자가 웹사이트에 접속할 때 생성된 고유한 세션 ID를 통해 서버에서 사용자를 식별할 수 있습니다. 세션 ID는 보통 쿠키에 저장되어 클라이언트로 전송됩니다.

세션 기반 인증 시스템에서 사용자가 로그인을 하면, 서버는 세션 저장소에 사용자의 정보를 조회하고 세션 ID를 발급합니다. 발급된 ID는 주로 브라우저의 쿠키에 저장합니다. 그 다음에 사용자가 다른 요청을 보낼 때마다 서버는 세션 저장소에서 세션을 조회한 후 로그인 여부를 결정하여 작업을 처리하고 응답을 합니다. 세션 저장소는 주로 메모리, 디스크, 데이터베이스 등을 사용합니다.

NestJS에서 세션(Session)은 사용자 인증 및 상태 관리를 위한 방법 중 하나로, 세션을 통해 서버는 사용자의 상태를 유지하고, 로그인 상태나 기타 사용자 정보를 지속적으로 관리할 수 있습니다. 세션을 사용하면 서버 자원을 사용하는 것이므로 서버에 부하를 주는 단점이 있지만, 중요한 정보에 대해 위조, 변조, 탈취가 불가능하므로 보안적인 측면에서 더 안전합니다.

4. Step 1 - 패키지 설치하기

다음 명령어를 입력하여 passport 라이브러리와 express-session 라이브러리를 설치합니다.

bash
1npm install @nestjs/passport passport passport-local express-session
bash
1npm install --save-dev @types/passport-local @types/express-session

각 패키지에 대해 설명하자면 다음과 같습니다.

  • passport
    패스포트 라이브러리
  • passport-local
    유저 아이디와 패스워드로 인증하는 로컬 전략을 사용할 때의 Strategy
  • express-session
    세션 저장 라이브러리

5. Step 2 - 패스포트와 세션 설정하기

다음과 같이 main.ts에 패스포트와 세션 설정 코드를 추가합니다.

typescript
1/* ... */
2
3import * as session from "express-session";
4import * as passport from "passport";
5
6// NestJS를 실행시키는 함수
7// NestJS에서는 진입점을 bootstrap()으로 이름 짓는 것이 관례이다.
8async function bootstrap() {
9  /* ... */
10
11  // 세션 사용
12  app.use(
13    session({
14      secret: "very-important-secret", // 세션 암호화에 사용되는 키
15      resave: false, // 세션을 항상 저장할 지 여부
16      saveUninitialized: false, // 세션이 저장되기 전에는 초기화되지 않은 상태로 세션을 미리 만들어 저장
17      cookie: { maxAge: 1000 * 60 * 60 }, // 쿠키 유효기간: 1시간
18    }),
19  );
20
21  // passport 초기화 및 세션 저장소 초기화
22  app.use(passport.initialize());
23  app.use(passport.session());
24
25  /* ... */
26}
27
28bootstrap();

위의 코드를 설명하자면 다음과 같습니다.

  • secret
    세션 암호화에 사용되는 키로, 외부로 유출되지 않도록 주의해야 합니다.
  • resave
    세션 데이터가 변경되지 않더라도 세션을 다시 저장할지 여부를 나타냅니다.
  • saveUninitialized
    초기화되지 않은 세션을 저장할지 여부를 나타냅니다.

6. Step 3 - Guard 구현하기

다음과 같이 로그인에 사용할 가드로그인 후 인증에 사용할 가드를 구현합니다.

typescript
1// auth.guard.ts
2
3import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
4import { AuthGuard } from "@nestjs/passport";
5
6// AuthGuard 상속
7@Injectable()
8export class LocalAuthGuard extends AuthGuard("local") {
9  async canActivate(context: ExecutionContext): Promise<boolean> {
10    const result = (await super.canActivate(context)) as boolean;
11
12    // 로컬 스트래티지 실행
13    const request = context.switchToHttp().getRequest();
14    await super.logIn(request); // 세션 저장
15    return result;
16  }
17}
18
19@Injectable()
20export class AuthenticatedGuard implements CanActivate {
21  canActivate(context: ExecutionContext): Promise<boolean> {
22    const request = context.switchToHttp().getRequest();
23    return request.isAuthenticated(); // 세션에서 정보를 읽어서 인증 확인
24  }
25}

위의 코드를 설명하자면 다음과 같습니다.

  • @nestjs/passport
    패스포트 인증에 가드를 사용할 수 있도록 감싸둔 AuthGuard를 제공하는 라이브러리입니다.
  • AuthGuard('local')
    로컬 strategy를 사용한다는 의미입니다.
  • super.canActive()
    로컬 strategy를 사용하므로 해당 부분에서 passport-local의 로직을 구현한 메서드를 실행합니다.
  • super.logIn()
    로그인 처리 및 세션을 저장합니다.

7. Step 4 - LocalStrategy 구현하기

다음과 같이 유저 아이디와 패스워드로 인증하는 LocalStrategy를 생성합니다.

typescript
1// local.strategy.ts
2
3import { Injectable } from "@nestjs/common";
4import { PassportStrategy } from "@nestjs/passport";
5import { Strategy } from "passport-local";
6import { AuthService } from "./auth.service";
7
8@Injectable()
9export class LocalStrategy extends PassportStrategy(Strategy) {
10  // PassportStrategy 믹스인
11  constructor(private authService: AuthService) {
12    super({ usernameField: "email" }); // 기본값이 username이므로 email로 변경해줌
13  }
14
15  // 유저 정보의 유효성 검증
16  async validate(email: string, password: string): Promise<any> {
17    const user = await this.authService.validateUser(email, password);
18    if (!user) {
19      return null; // null이면 401 에러 발생
20    }
21    return user; // null이 아니면 user 정보 반환
22  }
23}

PassportStrategy(Strategy)는 믹스인이라고 불리는 방법으로, 클래스의 일부만 확장하고 싶을 때 사용합니다. 또한 local-strategy에는 인증 시 사용하는 필드명이 username, password로 정해져 있습니다. 위의 코드에서는 email, password로 인증하게 되므로 usernameField를 email로 변경하였습니다.

추가적으로 위에서 사용한 local-strategy 인증 방법 이외에도 다양한 strategy가 있습니다.

인증 방법패키지명설명
Localpassport-local유저명과 패스워드를 사용해 인증
OAuthpassport-oauth페이스북, 구글, 트위터 등의 외부 서비스에서 인증
SAMLpassport-samlSAML 신원 제공자에서 인증, OneLogin, Okta 등
JWTpassport-jwtJSON Web Token을 사용해 인증
AWS Cognitopassport-cognitoAWS의 Cognito user pool을 사용해 인증
LDAPpassport-ldapauthLDAP 디렉토리를 사용해 인증

이외의 인증 방법에 대해서는 다음 링크를 참고하시길 바랍니다.

Passport.js

8. Step 5 - SessionSerializer 구현하기

다음과 같이 세션에 정보를 저장하거나, 세션에서 정보를 가져오는 SessionSerializer를 생성합니다.

typescript
1// session.serializer.ts
2
3import { Injectable } from "@nestjs/common";
4import { PassportSerializer } from "@nestjs/passport";
5import { UserService } from "../user/user.service";
6
7// PassportSerializer를 상속받음
8@Injectable()
9export class SessionSerializer extends PassportSerializer {
10  // userService를 주입받음
11  constructor(private userService: UserService) {
12    super();
13  }
14
15  // 세션에서 정보를 저장할 때 사용
16  serializeUser(user: any, done: (err: Error, user: any) => void): any {
17    done(null, user.email); // 세션에 저장할 정보
18  }
19
20  // 세션에서 정보를 꺼내올 때 사용
21  async deserializeUser(
22    payload: any,
23    done: (err: Error, payload) => void,
24  ): Promise<any> {
25    const user = await this.userService.getUser(payload);
26
27    // 유저 정보가 없는 경우 done() 함수에 에러 전달
28    if (!user) {
29      done(new Error("No User"), null);
30      return;
31    }
32
33    // eslint-disable-next-line @typescript-eslint/no-unused-vars
34    const { password, ...userInfo } = user;
35
36    // 유저 정보가 있다면 유저 정보 반환
37    done(null, userInfo);
38  }
39}

위의 코드에서 PassportStrategyserializeUser(), deserializeUser(), getPassportInstance()를 제공합니다. 각 메서드에 대해 설명하자면 다음과 같습니다.

  • serializeUser()
    세션에 정보를 저장합니다.
  • deserializeUser()
    세션에서 가져온 정보로 유저 정보를 반환합니다.
  • getPassportUser()
    패스포트 인스턴스를 가져옵니다. 패스포트 인스턴스의 데이터가 필요한 경우 사용합니다.

또한 payload는 세션에서 꺼내온 값을 의미하며, 세션 정보가 없는 경우 403 에러를 응답합니다.

9. Step 6 - 모듈에 설정 추가하기

다음과 같이 모듈에 세션을 사용할 수 있도록 설정을 추가합니다. { session: true }을 지정하여 세션을 사용할 수 있게 해줍니다.

typescript
1import { Module } from "@nestjs/common";
2import { AuthController } from "./auth.controller";
3import { AuthService } from "./auth.service";
4import { UserModule } from "../user/user.module";
5import { PassportModule } from "@nestjs/passport";
6import { LocalStrategy } from "./local.strategy";
7import { SessionSerializer } from "./session.serializer";
8
9@Module({
10  imports: [UserModule, PassportModule.register({ session: true })],
11  controllers: [AuthController],
12  providers: [AuthService, LocalStrategy, SessionSerializer],
13})
14export class AuthModule {}

10. Step 7 - Controller 설정하기

마지막으로 로그인 테스트를 위한 메서드들을 추가합니다.

typescript
1// auth.controller.ts
2
3  /* ... */
4
5  @UseGuards(LocalAuthGuard)
6  @Post('login3')
7  login3(@Request() req) {
8    return req.user;
9  }
10
11  @UseGuards(AuthenticatedGuard)
12  @Get('test-guard2')
13  testGuardWithSession(@Request() req) {
14    return req.user;
15  }
16
17  /* ... */

© HyunJinNo. Some rights reserved.