들어가며

✅ 해당 프로젝트는 Boilerplate 템플릿 https://github.com/cha2hyun/nestjs-prisma-starter-kakao-oauth-jwt으로 제작되어있습니다.

Don’t reinvent the wheel!
NestJS 공식문서에도 기재되어있는 무려 2,000 스타수가 넘는 notiz-dev/nestjs-prisma-starter를 기반으로 카카오 OAUTH 부분만 추가해두었습니다.

하다보니 에러가 있어서 pr 보냈더니 merged 되었다🖐️ 스타수 2K 기여했다는 이 뿌듯함..

하다보니 에러가 있어서 pr 보냈더니 merged 되었다🖐️ 스타수 2K 기여했다는 이 뿌듯함..

카카오 OAUTH는 대부분 한국에서 사용되겠지만, 그래도 범용(?)적으로 처음으로 영문으로 Readme도 적어보았습니다.

Instructions

Hits

🚀 This project is generated from notiz-dev/nestjs-prisma-starter starter template which is referenced in the NestJS official documentation. If you need more information, such as installation and setup, please check README within the template.

👀 This project provides Kakao Oauth login with Passport JWT authentication.

📝 Feel free to let me know if encounter any errors or have any questions. Pull requests are welcome.


Features

  • login/signup with kakao account with JWT tokens

Overview

1. Setup a kakao sdk in your frontend.

Please check kakao documents for setup. In my case i use next.js for the example.


2. Login with kakao account.

image

Here are some Next.js frontend code examples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<Script
  src="https://t1.kakaocdn.net/kakao_js_sdk/2.1.0/kakao.min.js"
  integrity="sha384-dpu02ieKC6NUeKFoGMOKz6102CLEWi9+5RQjWSV0ikYSFFd8M3Wp2reIcquJOemx"
  crossOrigin="anonymous"
  onReady={() => {
    if (!("Kakao" in window)) return;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    if ((window as any).Kakao == null) return;
    if (process.env.NEXT_PUBLIC_KAKAO_JS_KEY == null) return;

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const kakao = (window as any).Kakao;
    if (kakao.isInitialized() !== true) {
      kakao.init(process.env.NEXT_PUBLIC_KAKAO_JS_KEY);
    }
  }}
/>

Add kakao sdk on _app.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const handleLogin = useCallback(async () => {
  if (!("Kakao" in window)) return;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  if ((window as any).Kakao == null) return;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const kakao = (window as any).Kakao;
  kakao.Auth.authorize({
    redirectUri:
      typeof window !== "undefined"
        ? window.location.origin + "/auth/kakao"
        : null,
    prompts: "login",
  });
}, []);

In this case redirect url is /auth/kakao


3. Get Code Parameter.

If your account passes the login, the browser will redirect to your redirectUri with code parameters. image

The URL will look like http://localhost:3002/auth/kakao?code=TJx7M1-sTWkrKQgvOTmfvSUnC5bD2GqtWrA....


4. Perform a Mutation and Await Server Response

On your redirect page, initiate a login mutation to the NestJS server using code and redirectUri as variables.

For instance, here are some Next.js frontend code snippets.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* eslint-disable no-console */
import { Spinner } from "@nextui-org/react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";

import { useLoginMutation } from "@/src/core/config/graphql";
import Authentication from "@/src/core/function/authentication";

const KakaoOauth: NextPage = () => {
  const router = useRouter();
  const [login] = useLoginMutation();
  const [isFetched, setIsFetched] = useState(false);

  useEffect(() => {
    const params = new URL(document.location.toString()).searchParams;
    const code = params.get("code");
    const fetchData = async () => {
      try {
        if (code && typeof window !== undefined && !isFetched) {
          await login({
            variables: {
              code: code,
              redirectUri: window.location.origin + "/auth/kakao",
            },
            onCompleted: async (res) => {
              setIsFetched(true);
              Authentication.setToken({
                accessToken: res.login.accessToken,
                refreshToken: res.login.refreshToken,
              });
              console.log("jwt", res.login.accessToken);
            },
          });
        }
      } catch (err) {
        console.log(err);
      }
    };
    fetchData();
  }, [isFetched, login, router]);

  return (
    <div className="bg-defualt my-20 flex w-full justify-center ">
      <div className="mx-auto w-full flex-1 text-center ">
        <Spinner label="Waiting for server response..." color="warning" />
      </div>
    </div>
  );
};

export default KakaoOauth;

auth/kakao.tsx


5. Returning JWT Token on the NestJS server

First, auth.reslover.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Mutation(() => Auth)
async login(
  @Args("code") code: string,
  @Args("redirectUri")
  redirectUri: string,
) {
  const { accessToken, refreshToken } = await this.auth.kakaoLogin(code, redirectUri);
  return {
    accessToken,
    refreshToken,
  };

src > auth > auth.resolver.ts

it will call kakaoLogin functions in auth.service.ts


Second in auth.service.ts It will fetch the kakao access token using the code and redirectUri parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async kakaoLogin(code: string, redirectUri: string): Promise<Token> {
  try {
    const tokenResponse = await this.kakaoLoginService.getToken(code, redirectUri);
    const kakaoUser = await this.kakaoLoginService.getUser(tokenResponse.access_token);
    const isJoined = await this.prisma.user.findUnique({ where: { kakaoId: kakaoUser.id.toString() } });
    if (!isJoined) {
      return this.createUser({
        kakaoId: kakaoUser.id.toString(),
        email: kakaoUser.kakao_account.email,
        nickname: kakaoUser.properties.nickname,
        connectedAt: kakaoUser.connected_at,
        ageRange: kakaoUser.kakao_account.age_range,
        birthday: kakaoUser.kakao_account.birthday,
        gender: kakaoUser.kakao_account.gender,
        profileImageUrl: kakaoUser.properties.profile_image,
        thumbnailImageUrl: kakaoUser.properties.thumbnail_image,
      });
    }
    else {
      const userId = (await this.prisma.user.findUnique({ where: { kakaoId: kakaoUser.id.toString() } })).id;
      return this.generateTokens({
        userId: userId,
      });
    }
  } catch (e) {
    throw new Error(e);
  }
}

src > auth > auth.service.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 async getToken(code: string, redirectUri: string): Promise<KakaoOauthToken> {
  try {
    const url = new URL("/oauth/token", KAKAO_AUTH_URL).href;
    const tokenResponse = await new Promise<KakaoOauthToken>((resolve, reject) => {
      this.httpService
        .post(
          url,
          new URLSearchParams({
            grant_type: "authorization_code",
            client_id: this.configService.get("KAKAO_API_CLIENT_ID"),
            client_secret: this.configService.get("KAKAO_API_CLIENT_SECRET"),
            code: code,
            redirect_uri: redirectUri,
          }),
          {
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
          },
        )
        .subscribe({
          error: err => reject(err),
          next: response => resolve(response.data),
        });
    });
    return tokenResponse;
  } catch (e) {
    const err = e as AxiosError;
    // eslint-disable-next-line no-console
    console.log(err);
  }
}

Third, it will attempt to retrieve Kakao user information using the access-code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async getUser(token: string) {
  try {
    const url = new URL("/v2/user/me", KAKAO_API_URL).href;
    const infoResponse = await new Promise<KakaoV2UserMe>((resolve, reject) => {
      this.httpService
        .get(url, {
          headers: {
            Authorization: `Bearer ${token}`,
            "Content-type": "Content-type: application/x-www-form-urlencoded;charset=utf-8",
          },
          params: {
            secure_resource: true,
          },
        })
        .subscribe({
          error: err => reject(err),
          next: response => resolve(response.data),
        });
    });
    return infoResponse;
  } catch (e) {
    throw new UnauthorizedException();
  }
}

Fourth, utilizing the id from kakao user information to validate wheater the user already exist on database. If not it will create a new user and generate a JWT token associated with the userId, returning it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async createUser(payload: SignupInput): Promise<Token> {
  try {
    const user = await this.prisma.user.create({
      data: {
        kakaoId: payload.kakaoId,
        email: payload.email,
        nickname: payload.nickname,
        ageRange: payload.ageRange,
        birthday: payload.birthday,
        gender: payload.gender,
        role: "USER",
        kakaoProfile: {
          create: {
            profileImageUrl: payload.profileImageUrl,
            thumbnailImageUrl: payload.thumbnailImageUrl,
            connectedAt: payload.connectedAt,
          },
        },
      },
    });
    return this.generateTokens({
      userId: user.id,
    });
  } catch (e) {
    throw new Error(e);
  }
}

Fifthly, if the user already exists on database. It will return a JWT token genertated with the userId


6. Saving Returned JWT Token on your frontend

The token will be returned to frontend. ensure to save it within the browser.

image

Token will be like

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbG95aDBydXIwMDAwM2hvYm9taTl3N21iIiwiaWF0IjoxNzAwMDM2MTA4LCJleHAiOjE3MDAwMzYyMjh9.M1YjIJcKjeUfYo4P8Humh7fAtc4PAxRI54tJAJDP

7. Execute the ‘Me’ query using JWT tokens.

Add Authorization header with your JWT tokens to retrieve ‘Me’ Authorization : Bearer "YOUR JWT TOKENS"

스크린샷 2023-11-15 오후 5 28 01

on success

image

with worng token


8. Begin building your own projects 🚀

마치며

처음으로 제작한 Boilerplate template. 고작 한 기능이 추가되었지만 하면서 여러 공부도 되었고 공식문서에 적혀있는 기존 템플릿에 PR도 Merged 되어서 매우 뿌듯합니다. 누군가에게 언젠가 사용되길 바랍니다 😀

템플릿 바로가기 : https://github.com/cha2hyun/nestjs-prisma-starter-kakao-oauth-jwt