본문 바로가기
NestJS

[NestJS] Kakao OAuth2.0 Passport

by Programmer.Junny 2025. 4. 14.

1. Kakao Developers 설정

https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

카카오 디벨로퍼에서 애플리케이션을 생성해야한다.

API를 추가할 애플리케이션을 생성한다.

생성하면 앱 키 에서 각종 키들을 사용할 수 있게 된다.

구글 OAuth2.0 설정과 마찬가지로 카카오도 Redirection URI를 설정해주어야 한다. 로그인 성공 후 해당 URI로 리다이렉션 된다.

테스트앱을 생성하여 테스트를 할 수 있으나 사업자가 필요하므로, 개인적인 테스트를 위한 사람들은 그냥 넘어가도록 하자.

사업자 등록을 하지 않게 된다면 설정할 수 있는 권한이 매우 적다. 우선 기본적인 닉네임을 가지고 테스트하도록 한다.

설정이 완료되었으면 활성화 설정을 ON으로 전환한다.

2. 패키지 인스톨

npm install passport passport-kakao

 

3. .env 등록

KAKAO_CLIENT_ID=[카카오에서 받은 REST API]
KAKAO_CALLBACK_URL=http://localhost:3000/auth/kakao/callback
//kakao.config.ts

import { registerAs } from '@nestjs/config';

export default registerAs('kakao', () => ({
  clientId: process.env.KAKAO_CLIENT_ID,         // 카카오 앱의 REST API 키
  callbackUrl: process.env.KAKAO_CALLBACK_URL,
}));

4. Config를 사용할 수 있도록 모듈 등록

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { UsersModule } from 'src/users/users.module';
import { GoogleStrategy } from './strategies/google.strategy';
import { ConfigModule } from '@nestjs/config';
import googleConfig from 'src/configs/google.config';
import kakaoConfig from 'src/configs/kakao.config';
import { KakaoStrategy } from './strategies/kakao.strategy';

@Module({
  imports: [
    ConfigModule.forFeature(googleConfig),
    ConfigModule.forFeature(kakaoConfig),
    JwtModule.register({}),
    UsersModule,
  ],
  exports: [AuthService],
  controllers: [AuthController],
  providers: [AuthService, GoogleStrategy, KakaoStrategy],
})
export class AuthModule {}

마찬가지로 auth에서 사용하게 될 것이므로, auth.module.ts 에 위와 같이 kakaoConfig를 추가한다.

5. UsersModel 에 kakao 컬럼 추가

@Column({
    unique: true,
    nullable: true,
})
@IsString()
kakao: string;

UsersModel에 kakao 컬럼을 추가하여준다. nullable이 true여야 기존의 DB가 있어도 추가될 수 있게된다.

6. kakao로 로그인한 사용자 찾거나 생성하는 로직 구현

//users.service.ts

async findOrCreateByKakao({
        email,
        nickname,
        kakaoId,
    }: {
        email: string;
        nickname: string;
        kakaoId: string;
    }): Promise<UsersModel> {
        // 먼저, kako 필드가 kakaoId와 매칭되는 사용자가 있는지 확인
        let user = await this.usersRepository.findOne({ where: { kakao: kakaoId } });
        
        // 만약 사용자가 없다면, email로도 찾을 수 있다면 두 가지를 병합할 수도 있음
        if (!user && email) {
            user = await this.usersRepository.findOne({ where: { email } });
        }

        //todo: 카카오 임시로 이메일 설정
        if (!email) {
            email = 'stack@test.com';
        }
    
        // 사용자가 존재하지 않는다면 신규 생성
        if (!user) {
            user = await this.createUser({
                email,
                nickname: nickname,
                password: '',       // 카카오 로그인은 패스워드가 필요 없으므로 빈 문자열 지정
                kakao: kakaoId,   // 카카오 고유 식별자 저장
            });
        } else if (!user.kakao) {
            // 기존에 email로 가입된 사용자의 경우, 구글 연동이 안 되어 있다면 google 필드 업데이트
            user.kakao = kakaoId;
            await this.usersRepository.save(user);
        }
    
        return user;
    }

사업자가 아니기 때문에 카카오로부터 받아올 수 있는 것은 '닉네임' 뿐이므로, email을 임시로 설정해주었다.

email은 UsersModel에서 unique가 true이므로 nullable이 false이다. 그렇다는 소리는 유저를 생성할 때는 반드시 email이 있어야한다는 뜻이 된다. 

그렇기 때문에 email을 임시로 작업해서 넣어주었다.

7. KakaoStrategy 구현

import { Injectable, Inject } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, Profile, VerifyCallback } from 'passport-kakao';
import { ConfigType } from '@nestjs/config';
import kakaoConfig from 'src/configs/kakao.config';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class KakaoStrategy extends PassportStrategy(Strategy, 'kakao') {
  constructor(
    @Inject(kakaoConfig.KEY)
    private readonly config: ConfigType<typeof kakaoConfig>,
    private readonly usersService: UsersService,
  ) {
    super({
      clientID: config.clientId,
      callbackURL: config.callbackUrl,
    });
  }

  async validate(
    accessToken: string,
    refreshToken: string,
    profile: Profile,
    done: VerifyCallback,
  ): Promise<any> {
    // profile에서 필요한 정보를 추출합니다.
    // passport-kakao는 profile 객체 내 _json 속성에 추가 정보를 포함합니다.
    const { id, username, _json } = profile;
    const email =
      _json?.kakao_account && _json.kakao_account.email
        ? _json.kakao_account.email
        : null;

    /*
      UsersService에 kakaOAuth 전용 메소드 (예: findOrCreateByKakao())를 만들어서
      기존 사용자와의 중복 검사 및 신규 생성 로직을 처리할 수 있습니다.
      예)
      async findOrCreateByKakao({ kakaoId, email, nickname }: { kakaoId: string; email?: string; nickname: string; }): Promise<UsersModel>;
    */
    const user = await this.usersService.findOrCreateByKakao({
      email,
      nickname: username,
      kakaoId: id,
    });

    done(null, user);
  }
}

카카오 로그인이 실행될 때 처리되는 KakaoStrategy이다. Passport에 의해 구동된다.

8. KakaoAuthGuard 구현

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

@Injectable()
export class KakaoAuthGuard extends AuthGuard("kakao") {
  async canActivate(context: any): Promise<boolean> {
    const result = (await super.canActivate(context)) as boolean;
    return result;
  }
}

9. 컨트롤러 등록

  @Get('login/kakao')
  @IsPublic(IsPublicEnum.IS_PUBLIC)
  @UseGuards(KakaoAuthGuard)
  kakaoAuth() {
    // 이 엔드포인트는 KakaoAuthGuard가 리다이렉션 처리합니다.
    console.log('GET kakao/login');
  }

  @Get('kakao/callback')
  @IsPublic(IsPublicEnum.IS_PUBLIC)
  @UseGuards(KakaoAuthGuard)
  kakaoAuthRedirect(@User() user: UsersModel) {
    return this.authService.loginUser(user);
  }

auth.controller.ts 에 라우터들을 구현한다.

10. 실행 결과

http://localhost:3000/auth/login/kakao

위의 경로를 웹페이지에 입력하면 위와 같이 카카오로그인 화면이 뜬다.

http://localhost:3000/auth/kakao/callback

이후 계정을 선택하면 위 경로가 호출되며 토큰이 발급되는 것을 볼 수 있다.

마찬가지로 DB에도 등록이 되며 kakao 컬럼에 정상적으로 값이 입력되어있다.

11. 카카오계정과 함께 로그아웃

11.1. 카카오 로그아웃 리다이렉트 URI 추가

사용자가 다른 아이디로 로그인하고 싶을 경우거나, 강제로 로그아웃시켜버릴 경우가 있을 수 있다.

카카오 로그인 - 고급 항목에 Logout Redirect URI 등록을 선택한다.

컨트롤러에서 kakao/logout/callback 엔드포인트를 추가할 예정이므로 위와 같이 등록하였다.

11.2. 라우터 추가

GET https://kauth.kakao.com/oauth/logout?client_id={REST_API_KEY}&logout_redirect_uri={LOGOUT_REDIRECT_URI}

카카오는 REST API Key와 방금 등록한 Logout Redirect URI쿼리파라미터로 추가하여 GET 요청하도록 하고 있다.

  @Get('logout/kakao')
  @IsPublic(IsPublicEnum.IS_PUBLIC)
  kakaoAuthLogout(@Res() res: Response) {
    const restApiKey = this.config.clientId;
    // 로그아웃 후 사용자에게 리다이렉트할 URL 설정 (예: 홈 페이지 또는 로그인 페이지)
    const logoutRedirectUri = this.config.logoutCallbackUrl as string;
    const logoutUrl = `https://kauth.kakao.com/oauth/logout?client_id=${restApiKey}&logout_redirect_uri=${encodeURIComponent(logoutRedirectUri)}`;
    return res.redirect(logoutUrl);
  }

  @Get('kakao/logout/callback')
  @IsPublic(IsPublicEnum.IS_PUBLIC)
  kakaoAuthLogoutRedirect(@Res() res: Response) {
    res.send('<html><body><h1>카카오계정이 로그아웃 되었습니다.</h1></body></html>');
  }

auth 컨트롤러에서 로그아웃 엔드포인트와 로그아웃 시 콜백될 엔드포인트를 구현했다.

브라우저에선 Postman과 같이 AccessToken을 유지하도록 테스트할 수 없어서 임시로 @IsPublic 데코레이터를 추가하였다.

11.3. 실행결과

http://localhost:3000/auth/logout/kakao 를 브라우저에 입력하면 위와 같이 로그아웃 화면이 뜬다.

Callback 도 잘오는 것을 확인할 수 있다.

이후 다시 http://localhost:3000/auth/login/kakao 를 입력하면 기존에는 바로 로그인이 되어 accessToken과 refreshToken 발급화면이 보여졌지만, 이젠 처음부터 계정을 선택하도록 되었으며 로그아웃이 잘 적용된 것을 볼 수 있다.

'NestJS' 카테고리의 다른 글

[NestJS] Redis 적용하기 - Rate Limiting  (0) 2025.04.21
[NestJS] Redis 적용하기 (캐싱)  (0) 2025.04.17
[NestJS] Google Oauth2.0 Passport  (0) 2025.04.12
[NestJS] Swagger 사용법  (1) 2025.04.07
[NestJS] process.env 사용 및 분기처리  (0) 2025.04.05

최근댓글

최근글

skin by © 2024 ttuttak