Nest에서의 인증 방식

kakasoo·2022년 4월 24일
0

NestJS

목록 보기
4/7
post-thumbnail
post-custom-banner

공식문서와는 다른 방식

// auth/auth.module.ts

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

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

공식문서 상에서 AuthModule은 다음과 같은 형태로 구현된다. 보다시피, AuthModule에 UsersModule을 import한 형태로 작성된다. 하지만 개인적인 소감으로는, 이 방식보다는 AuthModule이 직접 User를 다루는 게 맞다고 봤다.

따라서 나라면, 코드를 아래처럼 고칠 것이다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../models/tables/user';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';

@Module({
  imports: [
    PassportModule.register({ session: false }),
    TypeOrmModule.forFeature([User]),
  ],
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

즉, AuthModule에 User를 넣는 것이 아닌, 또는 다른 모듈들을 import 하는 것이 아닌, AuthModule 자체가 인증에 필요한 TypeORM Module을 상속받아 직접 다루게 하는 것이다. 논리적으로 볼 때 AuthModule이 AppModule과 UsersModule 사이에 있는 것은 어색하지 않다.

하지만 Express 때의 코드와 마찬가지로, passport.js를 사용하는 것은 우리가 예상하는 로직 흐름과 다른 경우가 조금 있다. 그래서 더 직관적으로 만들기 위해서, 나는 반대로 UserModule이 AuthModule을 import하게 한다.

나는 이게 더 이해하기 쉬운 방식이라고 생각한다.

AuthModule의 구현, 그리고 보충

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../models/tables/user';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { KakaoStrategy } from './strategies/kakao.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';

@Module({
  imports: [
    PassportModule.register({ session: false }),
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        return {
          secret: configService.get('ACCESS_KEY'),
          signOptions: { algorithm: 'HS256', expiresIn: '1y' },
        };
      },
    }),
    TypeOrmModule.forFeature([User]),
  ],
  providers: [AuthService, KakaoStrategy, JwtStrategy],
  exports: [AuthService],
})
export class AuthModule {}

나는 공식문서에 있는 것과는 약간 다르게, AuthModule에서 직접 User Entity를 다루게 한다. 그리고 내용을 더 보충하여 JwtModule이나 KakaoStrategy JwtStrategy 등을 넣어보았다. 이러한 형태로 완성하게 된다면 이제 이 AuthModule을 UserModule에 import 해주면 된다.

UserModule 내부에서는 이제 export된 AuthService를 가져다 사용할 것이기 때문이다.

UserModule에서의 AuthService 사용

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../models/tables/user';
import { UsersController } from '../controllers/users.controller';
import { UsersService } from '../providers/users.service';
import { AuthModule } from '../auth/auth.module';

@Module({
  imports: [TypeOrmModule.forFeature([User]), AuthModule],
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

UserController는 UsersService를 사용한다.
그리고 마찬가지로 UserController는 AuthModule에서 export된 AuthService를 사용한다.

// user.controller.ts

import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { User as UserEntity } from '../models/tables/user';
import { CreateUserDto } from '../dtos/create-user.dto';
import { UsersService } from '../providers/users.service';
import { KaKaoGuard } from '../auth/guards/kakao.guard';
import { User } from '../common/decorators/user.decorator';
import { Profile } from 'passport-kakao';
import { AuthService } from '../auth/auth.service';
import { JwtGuard } from '../auth/guards/jwt.guard';
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';

@ApiTags('유저 / User')
@Controller('api/users')
export class UsersController {
  constructor(
    private readonly authService: AuthService,
    private readonly usersService: UsersService,
  ) {}

  @ApiOperation({ summary: 'MVP : 카카오톡을 이용한 로그인' })
  @UseGuards(KaKaoGuard)
  @Get('kakao/sign-in')
  async kakaoSignIn() {}

  @ApiOperation({ summary: 'MVP : 카카오톡 로그인 후 Redirect 되는 경로' })
  @UseGuards(KaKaoGuard)
  @Get('kakao/callback')
  async kakaoCallback(@User() profile: Profile): Promise<{ token: string }> {
    const { id: oauthId, username: name } = profile;
    let user = await this.usersService.findOne({ oauthId, name });
    if (!user) {
      user = await this.usersService.create({ oauthId, name, nickname: name });
    }
    return this.authService.userLogin(user);
  }

  @ApiOperation({ summary: 'MVP : 유저 프로필 조회 & 토큰에 담긴 값 Parsing.' })
  @ApiHeader({ name: 'token' })
  @UseGuards(JwtGuard)
  @Get('profile')
  async getProfile(@User() user: UserEntity) {
    return user;
  }
}

급조된 코드들이지만, authModule이 어느 시점에 사용된 것인지 확인할 수 있다. 유저가 UseGuard를 통해 guard를 접하게 되고, 인증이 완료되면 authService로 가게 된다. 이 때 authService는 토큰을 발급한다.

AuthService에서의 토큰 발급

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '../models/tables/user';

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}

  userLogin(user: User) {
    const token = this.jwtService.sign({ ...user });
    return { token };
  }
}

인증을 더욱 편리하게 하기 위해 나는 JwtModule을 이용해 jwtService를 사용한다. 토큰을 만들어서 클라이언트에게 돌려주자. 지금까지의 경로를 한 번 다시 요약해보자.

Guard의 흐름을 따라가보자

  1. 어느 컨트롤러의 API 분기를 타게 될 때 가장 먼저 @UseGuard() 데코레이터를 만난다.
  2. UseGuard는 파라미터로, 사용할 guard를 가지고 있다.
  3. guard는 자신에게 할당받은 전략의 이름을 가지고 있다.
  4. 해당 전략의 validate 로직을 타게 되면 이후 인증이 완료된다.
    • 인증이 성공한 경우에는 request에 user 라는 property가 생성된다.
    • 인증이 실패한 경우에는 403 에러를 뱉게 된다.
  5. Controller의 로직으로 돌아오게 되고, authService에 의해 토큰을 발급하여 제공한다.

이렇게 5단계가 LocalStrategy 혹은 Kakao와 같은 OAuth의 로직이다. 이렇게만 가지고도 간단하게 코드를 구현해볼 수 있다. 로컬을 이용해 문서를 작성하는 게 더 좋았겠지만, 이해하는 데에는 무리가 없을 것이다.

profile
자바스크립트를 좋아하는 "백엔드" 개발자
post-custom-banner

0개의 댓글