[Next.js] 삽질하면서 구현한 Next-auth로 애플 소셜 로그인 연동

woohyuk·2023년 7월 28일
7


회사 서비스에서 소셜로그인을 연동하기 위해 next-auth 라이브러리를 사용하기로 결정했다.
필요한 소셜 로그인은 네이버, 구글, 카카오, 애플 이였고
next-auth를 사용하면 쉽고 빠르게 소셜 로그인을 연동할 수 있어서 사용하게 되었다. (애플제외 😂)

말 그대로 네이버, 구글, 카카오는 참고자료도 많고 까다롭지가 않아서 쉽게 연동이 가능하였다.
그런데 애플이 문제였다.

애플로그인 문제상황

애플 소셜 로그인을 연동하는데 있어 다음과 같은 문제들이 존재하였다

  • clientId, clientSecret 값만 필요한 네이버, 구글, 카카오와 달리 애플은 더 많은 것을 요구하였다.
  • https가 아닌 도메인에서 애플 로그인을 사용할 수 없었다. 즉 로컬환경에서 테스트가 불가능 하였다. (ngrok사용으로 해결)
  • callbackUrl이 정상적으로 작동을 안해서 추가적인 코드 작성이 필요하다.
  • APPLE_PRIVATE_KEY를 이용하여 토큰 생성 코드를 작성해야한다.
  • session에서 원하는 값 누락 (이 부분에서 삽질을 제일 많이 한듯 하다.)

이런 문제상황들을 나는 겪게되었고
내가 해결한 부분들을 공유하면서 조금이나마 다른사람들이 애플 로그인을 연동할 때 삽질을 덜 할수 있었으면 한다.

그럼 처음부터 끝까지 연동방법을 적어보도록 하겠다.

Apple Developer 설정

1. App ID 추가

  • 애플 개발자 사이트 => Account 에서 Certificates, IDs & Profiles 메뉴 선택

  • Identifiers 메뉴에서 플러스 버튼을 선택

  • App IDs 를 선택 후 Continue 버튼 클릭

  • 타입을 App 을 선택하고, Continue 버튼 클릭

  • 앱 이름(Description)고유 ID(Bundle ID)를 입력

    BundleID는 도메인을 역순으로 해서 작성하는것을 애플에서 추천한다. ex) com.도메인명.앱이름

  • 스크롤을 내려 Sign In with Apple 에 체크 후 Continue 버튼 클릭

  • 입력 내용을 확인 후 Register 버튼 클릭

2. Service ID 추가

  • Identifiers 메뉴에서 플러스 버튼을 선택

  • Services IDs 를 선택 후 Continue 버튼을 누른다.

  • 서비스 이름과, ID를 입력한다.

    • App ID(Bundle ID)와 중복 될 수 없다.

    • continue => register를 눌러 등록한다.

  • 방금 등록한 서비스를 클릭한다.

  • Sign In with Apple 을 활성화(좌측 체크박스) 후 Configure 버튼을 눌러 설정 팝업을 띄운다.

  • 앞에서 추가한 App ID와 연결해 준다.

  • 도메인 명을 입력해 준다.

    • Apple 로그인은 localhost 도메인을 사용 할 수 없다. 테스트용 도메인명을 임의로 결정하여 추가해 준다.
    • 임의로 결정한 도메인 명은 hosts 에 임시로 추가하여 테스트 한다.
  • Redirect URL을 입력한다.

Next => Done => Continue => Save 버튼을 눌러 완료한다.

3. Key 추가

  • Keys 메뉴를 선택 후 플러스 버튼을 눌러 Key를 추가한다.

  • 키 이름 입력 후 Sign In with Apple 을 체크하고 Configure 버튼을 눌러 설정 화면으로 이동한다.

  • 위에서 추가한 App ID를 선택하고, Save 버튼을 눌러 저장하고 Continue 버튼을 눌러 다음으로 이동한다.
  • 설정 내용 확인 후 Register 버튼을 눌러 다운로드한다.

  • 키를 다운로드 한다.

  • 등록한 키를 다운로드 한다.
    • 키는 1회만 다운로드가 가능하다. 다음에 다운로드 하려면, 우선 Done 버튼을 눌러서 나가고, 다음에 다운로드를 진행한다.
    • AuthKey_XXXXXXXXXX.p8 형태의 파일명으로 다운로드 된다.
    • 다운로드 한 Private Key 파일은 Secret 키를 생성하는데 사용한다.
  • Done 버튼을 눌러 키 등록 및 다운로드를 최종 완료한다.

AuthKey_XXXXXXXXXX.p8 파일 사용법

키 생성 시 발급받은 AuthKey_XXXXXXXXXX.p8 파일을 vscode로 열면

-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgtE//U5HHjWkwoaas
ELnEnN3FEghMXwR/Z46DVrw6yaOgCgYIKoZIzj0DAQehRANCAAQvC0lpRjnDfe23
OBWGeIvdaOPQ83OLFjzKob3gBdK4lsV40haKQ76LBlUJv/QR/S6iUuRgvHZAPo4i
mClraeXI
-----END PRIVATE KEY-----

이런 형식으로 나타나 있는데 나는 BEGIN PRIVATE KEY와 END PRIVATE KEY 사이에 있는 문자열만 env파일에 따로 저장을 해두었다.
(개행처리된 부분도 적용이 되어야 하기에 \n을 사이에 넣어주었다.)

그리고 이 PRIVATE KEY는 애플토큰을 생성할때 사용이 된다.

다른 소셜로그인의 경우 clientSecret 부분에 바로 입력을 하면 되는데
애플 로그인은 따로 token으로 변형을 해준뒤 넣어주어야만 되었다.

appleToken 생성

import { SignJWT } from "jose";
import { createPrivateKey } from "crypto";
import NextAuth from "next-auth/next";
import NaverProvider from "next-auth/providers/naver";
import AppleProvider from "next-auth/providers/apple";

export default async function auth(req, res) {
  const getAppleToken = async () => {
    const key = `-----BEGIN PRIVATE KEY-----\n${process.env.APPLE_PRIVATE_KEY}\n-----END PRIVATE KEY-----\n`;

    const appleToken = await new SignJWT({})
      .setAudience("https://appleid.apple.com")
      .setIssuer(process.env.APPLE_TEAM_ID)
      .setIssuedAt(new Date().getTime() / 1000)
      .setExpirationTime(new Date().getTime() / 1000 + 3600 * 2)
      .setSubject(process.env.APPLE_ID)
      .setProtectedHeader({
        alg: "ES256",
        kid: process.env.APPLE_KEY_ID,
      })
      .sign(createPrivateKey(key));
    return appleToken;
  };

  return await NextAuth(req, res, {
    providers: [
      NaverProvider({
        clientId: process.env.NAVER_CLIENT_ID,
        clientSecret: process.env.NAVER_CLIENT_SECRET,
      }),

      AppleProvider({
        clientId: process.env.APPLE_ID,
        clientSecret: await getAppleToken(),
      }),
    ],

    secret: process.env.NEXTAUTH_SECRET,

    callbacks: {
      async jwt(data) {
        if (data.account) {
          data.token.accessToken = data.account.access_token;
          data.token.provider = data.account.provider;
        }
        return data.token;
      },
      async session({ session, token }) {
        if (session) {
          session.accessToken = token.accessToken;
          session.provider = token.provider;
          session.user.id = token.sub;
        }
        return session;
      },
    },
  });
}

PRIVATE_KEY를 이용하여 애플 토큰을 생성하는 함수를 만들어준뒤 AppleProvider에 clientSecret 부분에 넣어주었다.

callbackUrl 설정

나같은 경우에는 로그인을 한 이후 로직을 처리하기 위해 /oauth/apple 경로로 callback이 되기를 원해서 아래와 같이 설정을 해두었다.

signIn(provider, {
        callbackUrl: `/oauth/${provider}`,
      });

next-auth에서 제공해 주는 signIn 함수를 통해 로그인 성공 후 돌아오는 경로를 설정해 줄수가 있었는데
apple에서는 [...next-auth].js에 추가 설정을 해주어야 동작이 정상적으로 되었다.

import { SignJWT } from "jose";
import { createPrivateKey } from "crypto";
import NextAuth from "next-auth/next";
import NaverProvider from "next-auth/providers/naver";
import AppleProvider from "next-auth/providers/apple";

export default async function auth(req, res) {
  const getAppleToken = async () => {
    const key = `-----BEGIN PRIVATE KEY-----\n${process.env.APPLE_PRIVATE_KEY}\n-----END PRIVATE KEY-----\n`;

    const appleToken = await new SignJWT({})
      .setAudience("https://appleid.apple.com")
      .setIssuer(process.env.APPLE_TEAM_ID)
      .setIssuedAt(new Date().getTime() / 1000)
      .setExpirationTime(new Date().getTime() / 1000 + 3600 * 2)
      .setSubject(process.env.APPLE_ID)
      .setProtectedHeader({
        alg: "ES256",
        kid: process.env.APPLE_KEY_ID,
      })
      .sign(createPrivateKey(key));
    return appleToken;
  };

  return await NextAuth(req, res, {
    /**
     * 애플 로그인 시 쿠키옵션 적용해 주어야 callbackUrl이 정상 작동
     */
    cookies: {
      callbackUrl: {
        name: `__Secure-next-auth.callback-url`,
        options: {
          httpOnly: false,
          sameSite: "none",
          path: "/",
          secure: true,
        },
      },
    },

    providers: [
      NaverProvider({
        clientId: process.env.NAVER_CLIENT_ID,
        clientSecret: process.env.NAVER_CLIENT_SECRET,
      }),

      AppleProvider({
        clientId: process.env.APPLE_ID,
        clientSecret: await getAppleToken(),
      }),
    ],

    secret: process.env.NEXTAUTH_SECRET,

    callbacks: {
      async jwt(data) {
        if (data.account) {
          data.token.accessToken = data.account.access_token;
          data.token.provider = data.account.provider;
        }
        return data.token;
      },
      async session({ session, token }) {
        if (session) {
          session.accessToken = token.accessToken;
          session.provider = token.provider;
          session.user.id = token.sub;
        }
        return session;
      },
    },
  });
}

apple 로그인한 유저의 name값

로그인한 유저의 name값을 가져오고 싶었다.
하지만 next-auth에서 제공해주는 session값에는 아무리 찾아도 name값을 찾을수가 없었다.

여러 삽질끝에 name값을 최초로그인시에 req.body.user에서 찾을수가 있었다.
(애플은 사용자 name값을 최초로그인시 한번만 반환하도록 설정을 해둔거 같다.)

그래서 나는 req.body.user에 값을 session에 넣어서 사용할수 있도록 코드를 수정하였다.

import { SignJWT } from "jose";
import { createPrivateKey } from "crypto";
import NextAuth from "next-auth/next";
import NaverProvider from "next-auth/providers/naver";
import AppleProvider from "next-auth/providers/apple";

export default async function auth(req, res) {
  // 애플 최초 가입일 경우 req.body에 user.name이 담겨옴
  let appleFirstInfo;
  if (
    req?.url?.includes("callback/apple") &&
    req?.method === "POST" &&
    req.body.user
  ) {
    appleFirstInfo = await JSON.parse(req.body.user);
  }

  const getAppleToken = async () => {
    const key = `-----BEGIN PRIVATE KEY-----\n${process.env.APPLE_PRIVATE_KEY}\n-----END PRIVATE KEY-----\n`;

    const appleToken = await new SignJWT({})
      .setAudience("https://appleid.apple.com")
      .setIssuer(process.env.APPLE_TEAM_ID)
      .setIssuedAt(new Date().getTime() / 1000)
      .setExpirationTime(new Date().getTime() / 1000 + 3600 * 2)
      .setSubject(process.env.APPLE_ID)
      .setProtectedHeader({
        alg: "ES256",
        kid: process.env.APPLE_KEY_ID,
      })
      .sign(createPrivateKey(key));
    return appleToken;
  };

  return await NextAuth(req, res, {
    /**
     * 애플 로그인 시 쿠키옵션 적용해 주어야 callbackUrl이 정상 작동
     */
    cookies: {
      callbackUrl: {
        name: `__Secure-next-auth.callback-url`,
        options: {
          httpOnly: false,
          sameSite: "none",
          path: "/",
          secure: true,
        },
      },
    },

    providers: [
      NaverProvider({
        clientId: process.env.NAVER_CLIENT_ID,
        clientSecret: process.env.NAVER_CLIENT_SECRET,
      }),

      AppleProvider({
        clientId: process.env.APPLE_ID,
        clientSecret: await getAppleToken(),
        profile(profile) {
          if (appleFirstInfo) {
            profile.name = `${appleFirstInfo.name.firstName} ${appleFirstInfo.name.lastName}`;
          }

          return {
            id: profile.sub,
            name: profile.name,
            email: profile.email,
            image: null,
          };
        },
      }),
    ],

    secret: process.env.NEXTAUTH_SECRET,

    callbacks: {
      async jwt(data) {
        if (data.account) {
          data.token.accessToken = data.account.access_token;
          data.token.provider = data.account.provider;
        }
        return data.token;
      },
      async session({ session, token }) {
        if (session) {
          session.accessToken = token.accessToken;
          session.provider = token.provider;
          session.user.id = token.sub;
        }
        return session;
      },
    },
  });
}

next-auth를 사용하여 애플로그인을 진행하는 글이 많이 없어서 여러 자료를 찾아서 정리를 해보았다.

profile
기록하는 습관을 기르자

5개의 댓글

comment-user-thumbnail
2023년 8월 18일

안녕하세요. 덕분에 많은 도움 되었습니다.
궁금한 것이 하나 있는데 쿠키 옵션에 callbackUrl을 적용했을 때, 애플 외에 다른 sns 로그인은 문제가 없었나요?

1개의 답글
comment-user-thumbnail
2024년 1월 19일

정말 많은 도움이 되었습니다!!! 감사합니다!
혹시 NEXTAUTH_SECRET 생성은 어떻게 하셨을까요?

1개의 답글
comment-user-thumbnail
2024년 1월 29일

안녕하세요. 혹시 토큰 생성까지는 잘 되는 것 같은데 로그인할 때 아이디, 비밀번호 입력하고나면 /api/auth/signin?error=OAuthCallback 이런 경로로 이동하면서 에러가 나는 상황 겪으셨을까요?

답글 달기