본문 바로가기
PulseBoard

Firebase Functions + Kakao 로그인 문제 해결 정리

by 밤새는 탐험가89 2025. 12. 27.
728x90
SMALL

문제 상황 요약

카카오 로그인을 구현하면서 다음과 같은 현상이 발생했다.

  • ✅ 카카오 SDK 로그인 성공
  • ✅ accessToken 정상 발급
  • ❌ Firebase Functions 호출 시 실패
  • ❌ iOS에서는 항상 아래 에러만 발생
FunctionsError(code: INTERNAL)
  • ❌ Firebase Auth 로그인 실패
  • ❌ 카카오톡 로그인 화면이 바로 닫히고 다시 Login 화면으로 복귀

처음에는 index.js 코드 문제, region 설정, 중복 호출, SceneDelegate, Task 구조 등을 의심했다.

 

1️⃣ 처음 사용했던 구조 (개선 전)

Firebase Functions (개선 전)

const { onRequest } = require("firebase-functions/v2/https");

exports.kakaoCustomToken = onRequest(async (req, res) => {
  const { kakaoAccessToken } = req.body;

  const kakaoResponse = await axios.get(
    "https://kapi.kakao.com/v2/user/me",
    {
      headers: {
        Authorization: `Bearer ${kakaoAccessToken}`,
      },
    }
  );

  const kakaoUserId = kakaoResponse.data.id;
  const uid = `kakao:${kakaoUserId}`;

  const customToken = await admin.auth().createCustomToken(uid);

  res.json({ customToken });
});

 

iOS (Firebase Functions 호출)

functions.httpsCallable("kakaoCustomToken").call(parameters)

 

2️⃣ 개선 전 코드의 구조적 문제점

❌ 이 코드가 직접적인 원인은 아니었지만,
문제를 찾기 매우 어렵게 만드는 구조였다.

문제 1. onRequest + httpsCallable 혼용 

  • onRequest는 일반 HTTP
  • httpsCallable은 Firebase Callable Function 전용 프로토콜

👉 구조적으로 맞지 않는 조합

결과:

  • 서버에서는 에러가 발생
  • iOS에서는 모든 에러가 INTERNAL(13) 로 뭉개져서 전달됨

문제 2. 에러를 그냥 throw Error()로 처리

throw new Error("something wrong");

 

이렇게 던지면:

  • 서버에서는 상세 에러 로그가 보이지만
  • 클라이언트(iOS)에서는:
FunctionsError(code: INTERNAL)

 

👉 원인을 전혀 알 수 없음

 

3️⃣ 실제 콘솔 로그에서 발견된 진짜 힌트

Firebase Functions 로그를 직접 확인하면서 결정적인 문장을 발견했다.

FirebaseAuthError: Permission 'iam.serviceAccounts.signBlob' denied
auth/insufficient-permission

 

4️⃣ 진짜 원인: IAM 권한 문제

무슨 일이 벌어지고 있었나?

admin.auth().createCustomToken(uid)

 

이 한 줄은 내부적으로:

  1. Firebase 서비스 계정으로
  2. JWT(Custom Token)를 생성하고
  3. 서명(sign) 을 수행한다

👉 이때 필요한 권한이 바로:

iam.serviceAccounts.signBlob

 

하지만…

Cloud Functions Gen2 기본 서비스 계정

xxxxx-compute@developer.gserviceaccount.com

 

이 계정에는 토큰 서명 권한이 기본으로 없다.

그래서:

  • 코드가 아무리 맞아도
  • Kakao API가 아무리 정상이어도
  • createCustomToken()은 무조건 실패

 

5️⃣ index.js를 수정한 이유 (개선 후)  

⚠️ 중요
index.js 수정은 “문제를 해결”한 게 아니라
“문제를 정확히 드러나게 만든 과정”이었다.

 

Firebase Functions (개선 후)

const { onCall, HttpsError } = require("firebase-functions/v2/https");

exports.socialLogin = onCall(
  { region: "asia-northeast3" },
  async (request) => {
    try {
      const { accessToken, provider } = request.data;

      if (provider !== "kakao") {
        throw new HttpsError("invalid-argument", "Unsupported provider");
      }

      const kakaoResponse = await axios.get(
        "https://kapi.kakao.com/v2/user/me",
        {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        }
      );

      const uid = `kakao:${kakaoResponse.data.id}`;
      const customToken = await admin.auth().createCustomToken(uid);

      return { customToken };
    } catch (error) {
      throw new HttpsError("internal", error.message);
    }
  }
);

 

6️⃣ 개선 전 / 개선 후 차이 정리

구분 개선 전 개선 후
함수 타입 onRequest onCall
클라 호출 httpsCallable httpsCallable
데이터 접근 req.body request.data
에러 처리 Error HttpsError
iOS 에러 INTERNAL만 보임 실제 원인 확인 가능
디버깅 매우 어려움 가능

 

7️⃣ 최종 해결 방법 (진짜 해결)

IAM에서 다음 권한을 서비스 계정에 추가

대상 서비스 계정:

추가한 역할:

  • Service Account Token Creator
  • (roles/iam.serviceAccountTokenCreator)
  • 필요 시:
    • Service Usage Consumer

👉 권한 전파 후 재배포
👉 즉시 문제 해결

 

8️⃣ 최종 결론

이번 문제의 본질

  • ❌ index.js 버그 아님
  • ❌ Kakao SDK 문제 아님
  • ❌ iOS 비동기 문제 아님
  • ❌ Firebase region 문제 아님
  • Firebase Functions 서비스 계정 IAM 권한 문제

 

9️⃣ 이 경험에서 얻은 교훈 

1. INTERNAL(13) 에러의 80%는 서버 인프라 문제다

2. Callable Function에서는 반드시 HttpsError를 써라

3. createCustomToken()이 실패하면 IAM을 먼저 의심하자

4. 코드보다 권한이 먼저다

 

✅ Index.js 전체 코드

/**
 * Firebase Cloud Functions (Callable) - Social Login
 *
 * 이 파일의 책임:
 * - iOS 앱에서 전달받은 소셜 accessToken을 서버에서 검증한다
 * - 검증된 사용자에 대해 Firebase Custom Token을 생성한다
 * - iOS 앱은 이 Custom Token으로 Firebase Auth 로그인을 완료한다
 *
 * 🔑 왜 서버(Function)가 필요한가?
 * - Firebase Custom Token은 "신뢰된 서버"에서만 생성 가능
 * - iOS 앱에서 직접 createCustomToken을 호출하면 보안상 허용되지 않음
 * - 따라서 Kakao accessToken 검증 + Custom Token 발급은 반드시 서버에서 처리
 */

const { onCall, HttpsError } = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const axios = require("axios");

/**
 * Firebase Admin SDK 초기화
 *
 * - Cloud Functions 환경에서는 서비스 계정으로 자동 인증됨
 * - admin.auth() 등을 사용하기 위해 반드시 필요
 */
admin.initializeApp();

/**
 * socialLogin
 *
 * Firebase Callable Function
 * iOS에서 functions.httpsCallable("socialLogin")으로 호출됨
 *
 * 특징:
 * - onCall 방식 → Firebase SDK가 인증/직렬화 처리
 * - request.data 로 파라미터 전달
 * - 에러는 반드시 HttpsError 로 throw 해야 iOS에서 정상 수신됨
 */
exports.socialLogin = onCall(
  { region: "asia-northeast3" }, // 🇰🇷 서울 리전
  async (request) => {
    try {
      /**
       * 1️⃣ 요청 파라미터 추출
       *
       * iOS에서 전달되는 형태:
       * {
       *   accessToken: string,
       *   provider: "kakao"
       * }
       */
      const { accessToken, provider } = request.data;

      /**
       * 2️⃣ 필수 파라미터 검증
       *
       * - accessToken 또는 provider가 없으면 클라이언트 오류
       * - invalid-argument → iOS에서 명확히 구분 가능한 에러 코드
       */
      if (!accessToken || !provider) {
        throw new HttpsError(
          "invalid-argument",
          "Missing accessToken or provider"
        );
      }

      /**
       * 3️⃣ 지원 Provider 검증
       *
       * - 현재는 Kakao만 지원
       * - 추후 Naver / Apple 확장 가능
       */
      if (provider !== "kakao") {
        throw new HttpsError(
          "failed-precondition",
          "Unsupported provider"
        );
      }

      /**
       * 4️⃣ Kakao API 호출
       *
       * 목적:
       * - accessToken이 실제로 유효한지 검증
       * - Kakao 사용자 고유 ID 획득
       *
       * ⚠️ 이 단계가 "신뢰의 핵심"
       * → 이 검증이 통과된 사용자만 Firebase 계정을 발급함
       */
      let kakaoResponse;
      try {
        kakaoResponse = await axios.get(
          "https://kapi.kakao.com/v2/user/me",
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          }
        );
      } catch (error) {
        /**
         * Kakao API 호출 실패
         * - 토큰 만료
         * - 위조된 토큰
         * - 네트워크 오류
         */
        console.error("❌ Kakao API error:", error.response?.data || error);
        throw new HttpsError(
          "unauthenticated",
          "Invalid Kakao access token"
        );
      }

      /**
       * 5️⃣ Kakao 사용자 ID 추출
       *
       * - Kakao에서 제공하는 immutable identifier
       * - Firebase UID 생성에 사용
       */
      const kakaoUserId = kakaoResponse.data.id;

      if (!kakaoUserId) {
        throw new HttpsError(
          "unauthenticated",
          "Failed to get Kakao user id"
        );
      }

      /**
       * 6️⃣ Firebase UID 생성
       *
       * - provider prefix를 붙여 충돌 방지
       * - 예: kakao:4664280513
       */
      const uid = `kakao:${kakaoUserId}`;

      /**
       * 7️⃣ Firebase Custom Token 생성
       *
       * 🔥 이 지점이 이번 이슈의 핵심 포인트
       *
       * - admin.auth().createCustomToken()은
       *   IAM 권한 (iam.serviceAccounts.signBlob)이 필요
       * - Cloud Functions의 서비스 계정에
       *   "서비스 계정 토큰 생성자" 역할이 없으면 실패
       */
      const customToken = await admin.auth().createCustomToken(uid, {
        provider: "KAKAO",
      });

      /**
       * 8️⃣ iOS로 Custom Token 반환
       *
       * iOS에서는:
       * Auth.auth().signIn(withCustomToken: customToken)
       */
      return { customToken };

    } catch (error) {
      /**
       * 9️⃣ 에러 처리
       *
       * - HttpsError → 그대로 throw (클라이언트에 코드 전달)
       * - 그 외 Error → INTERNAL로 감싸기
       */
      console.error("🔥 socialLogin error:", error);

      if (error instanceof HttpsError) {
        throw error;
      }

      throw new HttpsError(
        "internal",
        "Internal server error"
      );
    }
  }
);
728x90
LIST