Skip to content

passport로 oAuth 로그인 회원가입 구현

Summer Min edited this page Nov 23, 2024 · 2 revisions

0. OAuth 사전 작업

[Kakao Developers](https://developers.kakao.com/docs/latest/ko/kakaologin/prerequisite)

  1. https://developers.kakao.com/console/app > 애플리케이션 추가하기

  2. 애플리케이션 선택

  3. 내 애플리케이션 > 앱 설정 > 요약 정보

    • 앱 키 > REST API 키 (인가 코드 요청 시 필요)
  4. 플랫폼 > 플랫폼 설정하기 > Web > 사이트 도메인http://localhost:3000/

  5. Redirect URI 등록하러 가기

  6. 내 애플리케이션 > 제품 설정 > 카카오 로그인 > 보안 > Client Secret > 코드 생성 (선택)

    • 활성화 상태: 사용함
  7. 내 애플리케이션 > 제품 설정 > 카카오 로그인 > 동의항목

    • 닉네임
    • 프로필 사진
    • 카카오계정(이메일) 설정 👈 비즈앱으로 전환해야 동의항목 설정이 가능하다.

    추후 인가 코드를 받을 때 [scope](https://developers.kakao.com/docs/latest/ko/kakaologin/common#user-info-kakao-account)와 관련 있다.

[🟩 Naver Developers](https://developers.naver.com/docs/common/openapiguide/appregister.md#%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%93%B1%EB%A1%9D)

  1. https://developers.naver.com/apps/#/wizard/register > 약관동의 > 계정 정보 등록 > 애플리케이션 등록
  2. 내 애플리케이션 > 개요
    • 애플리케이션 정보
      • Client ID
      • Client Secret
    • 네이버 로그인
      • 개발 상태 (개발 중)

        애플리케이션이 '개발 중' 상태이면 [멤버관리] 탭에서 등록한 아이디만 네이버 로그인을 이용할 수 있습니다. 개발이 완료되어 실 서비스에 적용하고자 하신다면 검수를 요청해 주세요. 검수가 승인되면 모든 아이디로 네이버 로그인을 이용할 수 있습니다.

  3. 내 애플리케이션 > 멤버관리 (애플리케이션 개설자는 등록할 필요가 없다.)
    • 관리자 ID 등록
    • 테스터 ID 등록

→ 사전 작업이라던데 다같이 논의해서 해야할듯? 이거 나중에 하겠음…

→ 아닙니다 테스트해보려니까 설정해둬야되네요

출처: [https://github.com/dangdangwalk/dangdang-walk/wiki#-oauth-사전-작업](https://github.com/dangdangwalk/dangdang-walk/wiki#-oauth-%EC%82%AC%EC%A0%84-%EC%9E%91%EC%97%85)

  • 카카오

image

image

image

  • 네이버

image

image

image

1. 일단 프로젝트 구조 정리

(base) ➜  auth git:(feature-be-#189) ✗ tree
.
├── auth.controller.spec.ts
├── auth.controller.ts
├── auth.module.ts
├── auth.service.spec.ts
├── auth.service.ts
├── dto
│   └── createUser.dto.ts
├── strategies
│   ├── kakao.strategy.ts
│   └── naver.strategy.ts
├── user.entity.ts
└── user.repository.ts

3 directories, 10 files

2. entities/user.entity.ts, entities/user.repository.ts 정의

  • user.entity.ts
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column({ unique: true })
  providerId: string; // 네이버/카카오 ID

  @Column()
  provider: string; // 'naver' 또는 'kakao'

  @Column()
  email: string;

  @Column({ nullable: true })
  nickname: string;

  @Column({ nullable: true })
  profileImage: string;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;
}
  • user.repository.ts
// user.repository.ts
import { DataSource, Repository } from 'typeorm';
import { User } from './user.entity';
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';

@Injectable()
export class UserRepository extends Repository<User> {
  constructor(@InjectDataSource() private dataSource: DataSource) {
    super(User, dataSource.createEntityManager());
  }
}

3. auth.service.ts + DTO 처리

그 전에 DTO 정리 (카카오/네이버 연동에서 제공받을 정보)

  • dto/createUser.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEmail, IsIn } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @ApiProperty({
    example: 'abc1234',
    description: '사용자의 카카오/네이버 아이디',
  })
  providerId: string;

  @IsString()
  @IsIn(['naver', 'kakao'], {
    message: 'provider는 naver 또는 kakao 중 하나여야 합니다.',
  })
  @ApiProperty({
    example: 'naver',
    description: '연동되는 서비스: 네이버/카카오',
  })
  provider: string;

  @IsEmail()
  @ApiProperty({
    example: '[email protected]',
    description: '사용자의 카카오/네이버 이메일 주소',
  })
  email: string;
}
  • auth.service.ts

→ findUser: 해당 유저 없으면 null return

→ createUser: 특정 유저 자동으로 생성

import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
import { CreateUserDto } from './dto/createUser.dto';

@Injectable()
export class AuthService {
  constructor(private readonly userRepository: UserRepository) {}

  async findUser(dto: CreateUserDto): Promise<User | null> {
    const { providerId, provider } = dto;

    const user = await this.userRepository.findOne({
      where: { providerId, provider },
    });

    return user;
  }

  async createUser(dto: CreateUserDto): Promise<User> {
    const user = this.userRepository.create(dto);
    return this.userRepository.save(user);
  }
}

4. AuthGuard → Strategy → passport Strategy 흐름 이해

  • 먼저 auth.module.ts 코드를 완성하면
import { Module } from '@nestjs/common';
import { User } from './user.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { NaverStrategy } from './strategies/naver.strategy';
import { KakaoStrategy } from './strategies/kakao.strategy';
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [AuthController],
  providers: [AuthService, NaverStrategy, KakaoStrategy],
})
export class AuthModule {}

여기서 providers: [AuthService, NaverStrategy, KakaoStrategy], 를 주목하자

  • 그리고 auth.controller.ts 코드를 마저 완성하면
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

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

  @Get('naver')
  @UseGuards(AuthGuard('naver'))
  async naverLogin() {
    // 네이버 로그인 페이지로 리디렉션
    // Passport가 리디렉션 처리
  }

  @Get('naver/callback')
  @UseGuards(AuthGuard('naver'))
  async naverCallback(@Req() req) {
    // 네이버 인증 후 사용자 정보 반환
    return {
      message: '네이버 로그인 성공',
      user: req.user,
    };
  }

  @Get('kakao')
  @UseGuards(AuthGuard('kakao'))
  async kakaoLogin() {
    // 카카오 로그인 페이지로 리디렉션
    // Passport가 리디렉션 처리
  }

  @Get('kakao/callback')
  @UseGuards(AuthGuard('kakao'))
  async kakaoCallback(@Req() req) {
    // 카카오 인증 후 사용자 정보 반환
    return {
      message: '카카오 로그인 성공',
      user: req.user,
    };
  }
}

@UseGuards(AuthGuard('naver')) , @UseGuards(AuthGuard('kakao')) 를 통해 들어온 request는 각각 NaverStrategy, KakaoStrategy에서 처리된다

  • NaverStrategyKakaoStrategy 클래스에서 Passport 전략을 작성하고 'naver''kakao'라는 이름을 부여
  • AuthModule에 해당 전략을 프로바이더로 등록했으니, 이렇게 등록된 전략은 NestJS의 DI 시스템을 통해 AuthGuard가 접근할 수 있게 된다
  • 모듈에 등록된 전략은 해당 모듈에서 사용할 수 있으며, 전략에 설정한 이름(예: 'naver')으로 전역적으로 인식
  • AuthGuard('naver')를 사용하면 NestJS는 컨테이너에서 naver라는 이름으로 등록된 전략을 찾아서 사용

5. passport 라이브러리는 무엇인가? Strategy란 무엇인가?

5-1. passport 라이브러리란?

Passport인증과 관련된 절차를 매우 쉽게 만들어주는 Node.js용 인증 라이브러리

Passport의 주요 역할

  1. 다양한 인증 전략 지원:
    • Passport는 전략 기반으로 설계, 이를 통해 개발자는 필요에 따라 OAuth, JWT, Local Strategy다양한 인증 방식을 쉽게 추가할 수 있다
  2. 인증 흐름 간소화:
    • Passport는 사용자를 로그인 페이지로 리디렉션하고, 토큰을 발급받고, 사용자 정보를 가져오는 등 여러 단계를 단순화하여, 개발자는 간단한 설정과 콜백 함수만으로 인증 과정을 구현할 수 있음
  3. 미들웨어 기반:
    • Passport는 미들웨어로 동작하기 때문에 ExpressNestJS와 같은 프레임워크에 쉽게 통합될 수 있음
    • @UseGuards(AuthGuard('naver'))와 같은 형태로 NestJS에서 인증을 위해 쉽게 사용할 수 있음
  4. Passport를 이용한 oAuth 인증과정의 흐름
    1. 사용자 리디렉션:
      • 사용자가 /auth/naver와 같은 경로에 접근하면 Passport는 미리 설정한 authorizationURL로 사용자를 네이버 로그인 페이지로 리디렉션함
    2. 사용자 인증:
      • 사용자가 로그인하고 동의를 완료하면 네이버는 콜백 URL로 인증 코드를 전달함
    3. 토큰 교환 및 사용자 정보 가져오기:
      • Passport는 tokenURL을 통해 인증 코드를 사용하여 엑세스 토큰을 요청하고, 이 토큰을 사용하여 사용자 정보를 가져옴
    4. validate 메서드 호출:
      • 엑세스 토큰을 사용해 사용자 정보를 가져온 후, Passport는 전략에 정의된 validate 메서드를 호출하여 사용자 정보를 처리함

5-2. Strategy란 무엇인가?

  1. 인증 전략의 개념
  • PassportStrategy를 확장한 NaverStrategyKakaoStrategy는 OAuth2 프로토콜을 이용해 사용자의 인증 흐름을 처리하는 클래스!!!
  1. Strategy가 수행하는 구체적인 역할

2.1 OAuth 인증 흐름 관리

  • 사용자가 네이버 또는 카카오 로그인 페이지로 리디렉션되고, 성공적으로 인증하면 서비스의 콜백 URL로 다시 리디렉션됨
  • 이 과정에서 각 전략은 OAuth 서버와 상호작용하여 엑세스 토큰을 발급, 이를 이용해 사용자의 정보를 가져옴

2.2 사용자 정보 검증 (validate 메서드)

  • validate 메서드는 OAuth 서버로부터 사용자가 인증된 후 호출됨
  • 이 메서드에서는 사용자 정보를 검증하고 필요한 경우 새로운 사용자를 생성함
    • 예를 들어, NaverStrategy에서 인증이 완료되면 validate 메서드가 호출되고, 여기서 엑세스 토큰을 사용하여 사용자 프로필을 확인!!!
    • 이후, CreateUserDto 객체를 생성하여 AuthServicefindOrCreateUser 메서드를 호출함으로써 사용자가 데이터베이스에 존재하지 않으면 새로운 사용자로 등록함

2.3 사용자 정보를 req.user에 설정

  • validate 메서드에서 반환된 사용자 정보는 NestJS의 요청 객체(req)에 req.user로 설정
  • 이렇게 하면 컨트롤러에서 사용자 정보에 쉽게 접근할 수 있음

6. Strategy 코드 완성하기

  • strategies/naver.strategy.ts
// src/auth/strategies/naver.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Profile, Strategy } from 'passport-naver';
import { AuthService } from '../auth.service';
import { CreateUserDto } from '../dto/createUser.dto';

@Injectable()
export class NaverStrategy extends PassportStrategy(Strategy, 'naver') {
  constructor(private authService: AuthService) {
    super({
	    // 환경 변수로 관리
      clientID: process.env.NAVER_CLIENT_ID, 
      // 애플리케이션이 네이버 또는 카카오와 같은 외부 서비스와 통신할 때 
      // 해당 애플리케이션을 식별하기 위한 고유 ID
      clientSecret: process.env.NAVER_CLIENT_SECRET,
      //  이 값은 애플리케이션의 보안성을 높이기 위해 사용됨 
      // 외부 서비스와 통신할 때 애플리케이션의 신뢰성을 보장하기 위해 사용
      callbackURL: process.env.NAVER_CALLBACK_URL,
      // 사용자가 네이버 또는 카카오 로그인 후 리디렉션될 URL
      // 이 URL을 통해 인증이 완료되었을 때 사용자 정보를 처리할 수 있음
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: Profile) {
    // 네이버 인증 이후 사용자 정보 처리
    const createUserDto: CreateUserDto = {
      providerId: profile.id,
      provider: 'naver',
      email: profile._json.email,
    };
    let user = await this.authService.findUser(createUserDto);
    if (!user) {
      user = await this.authService.createUser(createUserDto);
    }
    return user; // req.user로 반환
  }
}
  • 여기서 callBackURL이란?

    • 이 URL은 네이버/카카오의 개발자 콘솔에서 등록해야 하며, 애플리케이션 서버의 특정 엔드포인트를 가리켜야 함
    • 사용자가 네이버/카카오로 로그인 후, 인증이 완료되면 콜백 URL로 돌아옴
    • 이때 콜백 URL 엔드포인트(naver/callback, kakao/callback)이 호출되며, 사용자의 인증 결과가 백엔드 서버로 전달됨
  • 그러면 미룰수없다… 네이버, 카카오 개발자 콘솔에서 등록해야겠다…

7. Client id, Client secret, Callback url 환경변수로 등록

→임시임!!!

→ api prefix를 추가하는걸 깜빡해서;;;; 계속 테스트 실패했음 ㅋㅋㅋㅋㅋ

  • app.module.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './filter/http-exception.filter';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { IoAdapter } from '@nestjs/platform-socket.io';

import * as dotenv from 'dotenv';
dotenv.config();

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useWebSocketAdapter(new IoAdapter(app));
  app.useGlobalFilters(new HttpExceptionFilter());
  app.setGlobalPrefix('api');
// -> 얘 때문에 당연히 api 앞에 붙음 (로그를 제대로 보자...)
  const config = new DocumentBuilder()
    .setTitle('OctoDocs')
    .setDescription('OctoDocs API 명세서')
    .build();
  console.log(process.env.origin);
  const documentFactory = () => SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, documentFactory);
  app.enaableCors({
    origin: process.env.origin,
  });
  await app.listen(3000);
}
bootstrap();
  • redirect URL 다시 등록하기

image

image

  • .env
NAVER_CLIENT_ID=***...
NAVER_CLIENT_SECRET=***...
NAVER_CALLBACK_URL=http://localhost:3000/api/auth/naver/callback 
// 프런트엔드랑 연결하고 수정해야함

KAKAO_CLIENT_ID=***...
KAKAO_CLIENT_SECRET=***...
KAKAO_CALLBACK_URL=http://localhost:3000/api/auth/kakao/callback
// 프런트엔드랑 연결하고 수정해야함

8. 테스트

image

image

image

image

→ 확인 완

→ 로그인을 미리 해두고 사용자 DB에 사용자 정보까지 들어있다? 자동으로 두번째 화면 나오는것까지 확인

개발 문서

⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
🚧 트러블슈팅

팀 문화

🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략

그룹 기록

📢 발표 자료
🌤️ 데일리 스크럼
📑 회의록
🏖️ 그룹 회고
🚸 멘토링 일지
Clone this wiki locally