Android 앱패키징 Google Login(OAuth)에러 해결

honney·2026년 3월 10일

📋 목차

  1. 어떤 에러였나?
  2. 에러의 원인
  3. OAuth가 뭔지 먼저 짚고 가자
  4. Google 로그인 전체 플로우
  5. JWT Claim이 어디서 오는지가 핵심이었다
  6. 잘못된 값 vs 올바른 값
  7. 해결 방법
  8. 이 에러 만나면 체크할 것
  9. 오늘 배운 것 (TIL)

1. 어떤 에러였나?

어느 날 갑자기 Google 로그인이 안 된다는 제보가 들어왔다.
로컬에서는 잘 됐는데, AOS 빌드 후 서버에서 인증이 계속 실패하는 것이다.

서버 로그에는 이게 찍혔다:

InvalidExternalProviderTokenException

서버 코드를 열어보니 이런 구조였다.

// google.provider.ts
return await jwtVerify(idToken, publicKeys, {
    issuer: "https://accounts.google.com",
    audience: [
        process.env.AUTH_GOOGLE_CLIENT_ID,     // 웹 Client ID
        process.env.AUTH_GOOGLE_IOS_CLIENT_ID  // iOS Client ID
    ],
    algorithms: ["RS256"]
})
.catch(() => {
    throw new InvalidExternalProviderTokenException(); // ← 여기서 에러 발생
});

JWT를 검증할 때 audience 목록과 토큰의 aud 값이 다르면 예외를 던지는 구조.
즉, Android에서 전혀 다른 값이 aud에 담겨서 오고 있었던 것이다.


2. 에러의 원인

결론부터 말하면:

src/main/res/values/strings.xmldefault_web_client_id잘못된 Client ID가 들어가 있었다.
정확히는, 올바른 aud용 값(Web Client ID)이 아닌 테스트 환경의 azp이 들어가 있었던 것.

에러가 터지기까지의 흐름을 추적하면 이렇다:

[이전 작업자]
  google-services.json에서
  Web Client ID(type:3) 대신
  Android Client ID(type:1)를 strings.xml에 기입
        │
        ▼
[Android SDK]
  requestIdToken(serverClientId) 호출 시
  잘못된 값을 Google에 전달
        │
        ▼
[Google 서버]
  받은 serverClientId를
  그대로 ID Token의 aud에 담아 발급
        │
        ▼
[API 서버]
  허용된 Client ID 목록 ≠ aud 값
  → JWT 검증 실패 💥

3. OAuth가 뭔지 먼저 짚고 가자

이 에러를 제대로 이해하려면 OAuth 기본 구조를 알아야 한다.
나도 이번 계기로 처음 제대로 공부했다.

OAuth 2.0 : 사용자가 비밀번호를 제3자 서비스에 직접 넘기지 않고, Google·카카오 같은 기존 계정으로 안전하게 인증하는 개방형 표준 프로토콜

OAuth 구성 요소

구성 요소역할이번 케이스에서
Resource Owner로그인하려는 실제 사용자앱을 사용하는 유저
Client우리가 만든 앱Android 앱
Authorization Server권한을 부여하는 서버, Token 발급Google 서버
Resource Server사용자 정보를 실제로 가진 서버Google (프로필 등)
Access Token리소스 접근 권한 자격증명Google이 발급
ID Token (JWT)사용자 신원 증명 토큰aud, sub 등 claim 포함

4. Google 로그인 전체 플로우

전체 흐름을 단계별로 보면 이렇다.

 사용자          Android Native        Google 서버           우리 API 서버
    │                   │                    │                      │
    │  로그인 버튼 클릭     │                    │                      │
    │──────────────────►│                    │                      │
    │  (WebView 내 UI)  │                    │                      │
    │                   │                    │                      │
    │                   │ ① 구글 로그인 요청     │                      │
    │                   │ (serverClientId    │                      │
    │                   │  = string.xml 값)  │                      │
    │                   │───────────────────►│                      │
    │                   │                    │                      │
    │                   │                    │ ② 사용자 Google 계정   │
    │                   │◄── 로그인 팝업 ────│    인증 (구글 UI)        │
    │                   │                    │                      │
    │                   │ ③ ID Token 발급    │                      │
    │                   │ aud = serverClientId                      │
    │                   │◄───────────────────│                      │
    │                   │                    │                      │
    │                   │ ④ ID Token 전달    │                      │
    │                   │──────────────────────────────────────────►│
    │                   │   POST /auth/google/app                   │
    │                   │   body: { token: "eyJ..." }               │
    │                   │                    │                      │
    │                   │                    │ ⑤ Public Key 조회    │
    │                   │                    │◄─────────────────────│
    │                   │                    │  GET /oauth2/v3/certs│
    │                   │                    │─────────────────────►│
    │                   │                    │                      │
    │                   │                    │  ⑥ JWT 검증          │
    │                   │                    │  - 서명 유효?          │
    │                   │                    │  - iss 맞음?          │
    │                   │                    │  - aud 허용 목록에?    │
    │                   │                    │  - 만료 안됨?          │
    │                   │                    │                      │
    │                   │                    │ ⑦ 사용자 조회/생성      │
    │                   │                    │  DB에서 sub로 찾기     │
    │                   │                    │                      │
    │                   │ ⑧ 우리 서비스 Access Token 반환              │
    │                   │◄──────────────────────────────────────────│
    │                   │                    │                      │
    │  로그인 완료         │                    │                      │
    │◄──────────────────│                    │                      │

단계별 핵심 포인트

  • ① serverClientId : Android SDK가 Google에게 "이 토큰은 누가 쓸 것인지" 알려주는 값. strings.xmldefault_web_client_id가 여기 쓰인다.
  • ② ID Token의 aud 결정 : Google은 serverClientId를 그대로 ID Token의 aud에 담아준다.
  • ④ JWT 검증 실패 : aud가 서버 허용 목록에 없으면 → 예외 발생

5. JWT Claim이 어디서 오는지가 핵심이었다

이 에러의 본질은 aud claim이 어디서 오는지 몰랐기 때문이다.

각 Claim의 출처 정리

Claim의미출처비고
iss"Google이 발급했어"Google 고정값Android 저장 불필요
azp"Android 앱이 요청했어"google-services.json client_type: 1SDK가 자동 세팅
aud ⚠️"이 토큰을 쓸 서버야"strings.xmldefault_web_client_id에러 원인. type:3 값을 넣어야!
sub"이 사용자 고유 번호"로그인 성공 후 Google 런타임 반환DB 저장용 식별자

헷갈렸던 것: azp vs aud

azp = "요청한 앱이 누구냐" → Android Client ID (google-services.json type:1, SDK 자동 세팅)
aud = "이 토큰을 쓸 서버가 누구냐" → Web Client ID (google-services.json type:3, 내가 직접 설정)

둘 다 xxx.apps.googleusercontent.com 형식이라 처음엔 구분하기 어려웠다.


6. 잘못된 값 vs 올바른 값

google-services.json에서 올바른 값 찾는 법

{
  "oauth_client": [
    {
      "client_id": "123-androidXXX.apps.googleusercontent.com",
      "client_type": 1   // Android → azp에 자동 세팅됨. strings.xml에 넣으면 ❌
    },
    {
      "client_id": "123-iosXXX.apps.googleusercontent.com",
      "client_type": 2   // iOS
    },
    {
      "client_id": "123-webXXX.apps.googleusercontent.com",
      "client_type": 3   // ✅ 이걸 default_web_client_id에 넣어야 한다!
    }
  ]
}

비교

구분❌ 잘못된 경우✅ 올바른 경우
default_web_client_idtype:1 Android Client IDtype:3 Web Client ID
ID Token의 aud123-android~.apps.googleusercontent.com123-web~.apps.googleusercontent.com
서버 검증 결과audience mismatch → 예외 발생 💥허용 목록과 일치 → 통과 ✅

7. 해결 방법

Step 1. google-services.json에서 Web Client ID 확인

Firebase Console 또는 Google Cloud Console에서 최신 google-services.json을 다운로드한 뒤,
client_type: 3인 항목의 client_id를 복사한다.

Step 2. strings.xml 수정

<!-- src/main/res/values/strings.xml -->
<resources>
    <!-- ❌ 잘못된 예: Android Client ID나 azp 값을 넣은 경우 -->
    <!-- <string name="default_web_client_id">123-androidXXX.apps.googleusercontent.com</string> -->

    <!-- ✅ 올바른 예: Web Client ID (client_type: 3) -->
    <string name="default_web_client_id">123-webXXX.apps.googleusercontent.com</string>
</resources>

Step 3. 서버 환경변수 확인

# 서버의 AUTH_GOOGLE_CLIENT_ID가 위 Web Client ID와 동일한지 반드시 확인
AUTH_GOOGLE_CLIENT_ID=123-webXXX.apps.googleusercontent.com

Step 4. ID Token 디버깅 (선택)

확인하고 싶다면 jwt.io에서 ID Token을 붙여넣어 aud claim을 직접 확인할 수 있다.

{
  "iss": "https://accounts.google.com",
  "azp": "123-androidXXX.apps.googleusercontent.com",  // Android가 요청했다는 표시
  "aud": "123-webXXX.apps.googleusercontent.com",      // ← 이 값이 서버 허용 목록에 있어야 함
  "sub": "1234567890",
  ...
}

8. 이 에러 만나면 체크할 것

□ strings.xml의 default_web_client_id가 google-services.json client_type:3 값과 일치하는가?
□ 서버의 AUTH_GOOGLE_CLIENT_ID 환경변수가 Web Client ID(type:3)로 설정되어 있는가?
□ jwt.io에서 ID Token의 aud 값을 직접 확인했는가?
□ 테스트 환경과 프로덕션 환경의 google-services.json을 혼용하고 있지는 않은가?
□ iOS Client ID가 있다면 서버 audience 배열에 추가되어 있는가?
□ Firebase Auto-fill로 생성된 값이 나중에 다른 값으로 덮어써지지 않았는가?

9. 오늘 배운 것 (TIL)

💡 aud는 내가 만드는 값이다
Google이 알아서 넣어주는 게 아니라, Android SDK가 requestIdToken(serverClientId)에 넘긴 값을 그대로 aud에 박아준다. 내가 잘못된 값을 넣으면 잘못된 aud가 생긴다.

🔍 azpaud는 완전히 다르다
azp는 "요청한 앱"(Android, 자동 세팅), aud는 "토큰을 쓸 서버"(수동 설정). 둘 다 Client ID처럼 생겼지만 역할이 전혀 다르다.

📄 google-services.jsonclient_type이 핵심이다
type:1(Android), type:2(iOS), type:3(Web). serverClientId에는 반드시 type:3 값을 써야 한다.

profile
보이지 않은 것을 보이게 할 때 기쁨을 느낍니다

0개의 댓글