Nest.js passport를 이용한 OAuth2.0 구현(2)

정민교·2024년 4월 21일
0

ai-diary

목록 보기
2/2

지난 시간에 이어 이번에는 User 테이블을 생성하고 User를 검증하는 로직을 작성해서 validate 메소드에 추가할 것이다.

📒User 테이블 생성하기

우선 User Entity를 작성한다.

import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { UserRole } from '../const/role.enum';
import { AuthProvider } from '../../auth/const/provider.enum';

@Entity()
@Unique(['email', 'provider'])
export class User {
  @PrimaryGeneratedColumn({
    type: 'bigint',
    unsigned: true,
    primaryKeyConstraintName: 'PK_user_id',
    comment: '유저 아이디 PK',
  })
  id: number;

  @Column({
    comment: 'Oauth 플랫폼 유저 아이디',
  })
  externalId: string;

  // @Column({
  //   comment: '사용자 닉네임',
  // })
  // nickname: string;

  @Column({
    nullable: false,
    comment: '사용자 이메일',
  })
  email: string;

  @Column({
    type: 'enum',
    enum: AuthProvider,
    nullable: false,
    comment: 'Oauth 제공자',
  })
  provider: string;

  @Column({
    type: 'enum',
    enum: UserRole,
    default: UserRole.USER,
    comment: '사용자 role',
  })
  role: UserRole;
}

위와 같이 User Entity를 작성해주고 TypeORM으로 DB를 연결한다.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
import * as process from 'node:process';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  ENV_DB_DATABASE_KEY,
  ENV_DB_HOST_KEY,
  ENV_DB_PASSWORD_KEY,
  ENV_DB_PORT_KEY,
  ENV_DB_USERNAME_KEY,
} from './common/const/env-keys.const';
import { UsersModule } from './users/users.module';
import { User } from './users/entities/user.entity';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: `.env.${process.env.NODE_ENV}`,
      isGlobal: true,
    }),
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env[ENV_DB_HOST_KEY],
      port: Number.parseInt(process.env[ENV_DB_PORT_KEY]),
      username: process.env[ENV_DB_USERNAME_KEY],
      password: process.env[ENV_DB_PASSWORD_KEY],
      database: process.env[ENV_DB_DATABASE_KEY],
      entities: [User],
      synchronize: true,
      logging: true,
    }),
    AuthModule,
    UsersModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

📒UserService 작성하기

이번에는 UserService를 작성한다.

UserService에 회원을 검증하는 로직을 담당하는 verifyUser 메소드를 작성할 것이다.

사용자를 찾지 못하면 새로운 user를 생성하여 DB에 저장한다.

import { Injectable, NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthProvider } from '../auth/const/provider.enum';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async verifyUser(id: number): Promise<User>;
  async verifyUser(
    email: string,
    provider: keyof typeof AuthProvider,
  ): Promise<User>;

  /**
   * 사용자가 존재하는지 검증
   * */
  async verifyUser(
    idOrEmail: number | string,
    provider?: keyof typeof AuthProvider,
  ): Promise<User> {
    // idOrEmail이 string인데 provider에 값이 없으면 에러 발생
    if (typeof idOrEmail === 'string' && !provider) {
      throw new Error('email로 회원 검증 시 OAuth 제공자가 반드시 필요합니다.');
    }
    // idOrEmail 매개변수
    //   number 타입인 경우 id로 회원 조회
    //   string 타입인 경우 email, provider로 회원 조회
    return this.findUser(
      typeof idOrEmail === 'number'
        ? { id: idOrEmail }
        : { email: idOrEmail, provider },
    );
  }
  /**
   * DB에서 사용자 찾기
   * */
  async findUser(
    criteria: Pick<User, 'id'> | Pick<User, 'email' | 'provider'>,
  ) {
    const user = await this.userRepository.findOne({ where: criteria });
    if (!user) {
      throw new NotFoundException('사용자를 찾을 수 없습니다.');
    }
    return user;
  }

  /**
   * user 생성 후 DB 저장
   * */
  async createUser(createUserDto: CreateUserDto) {
    const user = this.userRepository.create({
      ...createUserDto,
    });
    return this.userRepository.save(user);
  }
}

📒GoogleStrategy에 검증 로직 추가하기

이제 GoogleStrategy 클래스의 validate 메소드 안에 작성한 verifyUser를 포함시킨다.

import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-google-oauth20';
import { Injectable, NotFoundException } from '@nestjs/common';
import * as process from 'node:process';

import {
  ENV_GOOGLE_CALLBACK_URL_KEY,
  ENV_GOOGLE_CLIENT_ID_KEY,
  ENV_GOOGLE_CLIENT_SECRET_KEY,
} from '../../common/const/env-keys.const';
import { UsersService } from '../../users/users.service';
import { UserRole } from '../../users/const/role.enum';

@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
  constructor(private readonly userService: UsersService) {
    super({
      clientID: process.env[ENV_GOOGLE_CLIENT_ID_KEY],
      clientSecret: process.env[ENV_GOOGLE_CLIENT_SECRET_KEY],
      callbackURL: process.env[ENV_GOOGLE_CALLBACK_URL_KEY],
      scope: ['email', 'profile'],
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: Profile) {
    const { emails } = profile;
    const email = emails[0].value;
    let user;
    try {
      user = await this.userService.verifyUser(email, 'GOOGLE');
    } catch (err) {
      if (err instanceof NotFoundException) {
        user = await this.userService.createUser({
          email,
          provider: 'GOOGLE',
          externalId: profile.id,
          role: UserRole.USER,
        });
      } else {
        throw err;
      }
    }
    /*    const user = {
          email: emails[0].value,
          firstName: name.givenName,
          lastName: name.familyName,
          externalId: profile.id,
          accessToken,
          refreshToken,
        };*/

    return user;
  }
}

📒authContorller 작성하기

이제 구글 로그인 api를 요청을 하게 되면

import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Get('google')
  @UseGuards(AuthGuard('google'))
  async googleAuthenticate() {}

  @Get('google/callback')
  @UseGuards(AuthGuard('google'))
  async googleRedirect(@Req() req) {
    console.log(req.user);
  }
}

구글 로그인 요청을 하면 구글 로그인 페이지로 redirect 되고, 구글 로그인이 완료되면, GCP 프로젝트에서 등록했던 callback url로 구글 서버에서 redirect 시킨다.

요청 결과로 다음과 같이 user 가 잘 생성되었다.

📒마무리

google oauth 로그인을 passport를 이용해서 구현하는 것까지 마무리 되었다.

user가 없다면 user를 생성하여 저장하고 기존 user가 있다면 user를 반환하게 된다.

다음에는 jwt 토큰을 자체적으로 발행하여 사용자 인가 처리 작업을 진행할 것이다.

profile
백엔드 개발자

0개의 댓글