Sign in with Apple이 6개월 만에 invalid_client 오류로 작동하지 않나요? 원인과 해결 방법
요약
Sign in with Apple의 웹 환경에서 발생하는 'invalid_client' 오류의 원인과 해결 방법을 다룹니다. Apple의 client secret이 6개월 만료되는 JWT 방식임을 설명하며, 수동 교체 및 GitHub Actions를 통한 자동화 방법을 제안합니다.
핵심 포인트
- Apple의 client secret은 6개월 만료되는 JWT 형태임
- 만료 시 별도의 경고 없이 'invalid_client' 오류 발생
- 수동 해결: .p8 키로 새로운 JWT 생성 후 대시보드 업데이트
- 영구 해결: GitHub Actions를 이용한 자동 교체(Rotation) 구현
만약 여러분의 프로덕션 앱에서 Apple 로그인이 아무것도 변경하지 않았음에도 갑자기 invalid_client 오류와 함께 실패하기 시작해서 이 글을 보고 계신다면, 이 포스트가 도움이 될 것입니다. 해결 방법은 5분 정도 걸리며, 영구적인 해결책은 10분 정도 소요됩니다. 두 방법 모두 아래에 설명되어 있습니다.
증상
- 웹 (web) 환경에서의 "Sign in with Apple"은 몇 달 동안 잘 작동하다가, 모든 시도가
invalid_client와 함께 실패합니다. - Supabase 로그에는
Unable to exchange external code라고 표시될 수 있습니다. - 네이티브 iOS 로그인 (
signInWithIdToken)은 여전히 작동하므로 상황을 더욱 혼란스럽게 만듭니다. - 보통 Apple 인증을 설정한 지 거의 정확히 6개월이 지났을 때 발생합니다.
원인: Apple의 "client secret"은 비밀이 아니라 JWT입니다
다른 모든 OAuth 제공업체(Google, GitHub, Facebook 등)는 정적인 client_secret을 한 번 제공하며, 이는 영구적으로 유지됩니다. 하지만 Apple은 다릅니다. Apple의 경우 사용자가 직접 client secret을 생성해야 합니다: 이는 Apple Developer 계정의 .p8 개인 키(private key)로 서명하는 ES256 JWT입니다.
그리고 여기에 함정이 있습니다: Apple은 JWT의 수명을 6개월로 제한합니다 (exp − iat ≤ 15777000 초). 만료되면 다음과 같은 일이 발생합니다:
- Apple은 아무런 경고를 보내지 않습니다.
- 사용 중인 제공업체 대시보드(Supabase, Firebase, Auth0 등)에는 오류가 표시되지 않습니다.
- 웹 로그인이 그냥
invalid_client를 반환하기 시작합니다.
사용자가 여러분보다 먼저 이 사실을 알게 됩니다.
빠른 해결 방법 (5분)
AuthKey_XXXXXXXXXX.p8파일을 준비합니다 (Sign in with Apple 키를 생성할 때 저장해둔 파일이어야 합니다. 다시 다운로드할 수 없으며 새로 생성만 가능합니다).- 이 파일을 사용하여 새로운 client secret JWT를 생성합니다. Supabase Apple 로그인 문서에 생성 도구가 포함되어 있으며, 오픈 소스 스크립트들도 존재합니다.
- 새로운 secret을 제공업체 대시보드에 붙여넣습니다 (Supabase의 경우: Authentication → Providers → Apple → Secret Key).
완료되었습니다 — 향후 6개월 동안은 괜찮을 것입니다. 바로 이것이 문제입니다.
영구적인 해결 방법: 정기적으로 교체(rotate)하기
Supabase 공식 문서에서는 — 지어낸 말이 아닙니다 — _"6개월마다 반복되는 캘린더 알림을 설정하세요"_라고 권장합니다. 인증 (auth) 시스템의 프로덕션 인프라로서 캘린더 알림을 사용하라는 것입니다.
저는 미래의 제가 그 알림을 믿고 지킬 수 없을 것 같아서, GitHub Action을 통해 이를 자동화했습니다: apple-client-secret-rotator. 스케줄링된 워크플로 (workflow)가 .p8 파일로부터 ES256 JWT를 재생성하고, Management API를 통해 Supabase 프로젝트를 업데이트합니다. 한 번 설정해 두면 영원히 잊어도 됩니다:
# .github/workflows/rotate-apple-secret.yml
name: Rotate Apple client secret
on:
...
4개의 리포지토리 시크릿 (repository secrets) (APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_P8 — .p8 파일의 전체 내용 — 그리고 SUPABASE_ACCESS_TOKEN)을 추가하고, Actions 탭에서 수동으로 한 번 실행하면, 그 이후부터는 크론 (cron)이 알아서 처리합니다.
인증 키 (auth keys)를 건드리는 모든 것은 의심해봐야 하므로, 몇 가지 설계 노트를 공유합니다:
- 의존성 없음 (Zero dependencies). 전체 로직은 순수 Node 내장 기능(JOSE 스타일의 ES256을 위한
dsaEncoding: 'ieee-p1363'을 사용하는crypto.sign)으로 구성된 약 100줄짜리 파일 하나입니다.npm install할 것도 없고, 파일 하나 외에는 감사 (audit)할 것도 없습니다. .p8파일은 워크플로를 벗어나지 않습니다. 파일은 본인 리포지토리의 GitHub Secrets에 저장되며, 본인의 러너 (runner)에서만 읽힙니다.- 시크릿은 로그에서 마스킹 (masked) 처리됩니다 (
::add-mask::) 되며 절대 출력되지 않습니다. - 실패 시 즉시 알 수 있습니다. 잘못된 키, 누락된 입력 또는 Supabase API 오류가 발생하면 명확한 메시지와 함께 워크플로가 실패합니다. 사용자로부터 문제를 전달받는 대신 GitHub의 실행 실패 이메일을 받게 됩니다.
Supabase를 사용하지 않나요? 출력 전용 모드
Supabase 입력값을 생략하면, 이 Action은 새로운 JWT를 워크플로 출력 (workflow output)으로 노출하기만 합니다. 이를 Firebase, Auth0, 셀프 호스팅된 GoTrue, 시크릿 매니저 (secrets manager) 등 원하는 곳으로 파이프라인 (pipe) 연결하세요:
- uses: oskar-makarov/apple-client-secret-rotator@v1
id: apple
with:
...
(만약 인증 (auth) 로직이 NextAuth, Laravel Socialite, better-auth와 같이 귀하의 자체 코드 내에 구현되어 있다면, 이 모든 과정이 필요하지 않을 수도 있습니다. 대신 각 요청 시마다 시크릿 (secret)을 동적으로 생성하십시오. 로테이션 (Rotation)은 시크릿을 타인의 대시보드 (dashboard)에 직접 붙여넣어야 할 때 중요합니다.)
FAQ
왜 6개월이 아니라 5개월인가요? Apple의 절대적인 최대 기간은 약 6개월입니다. 5개월 단위의 크론 (cron) 작업을 설정하면, 작업 실행이 실패하거나 비활성 리포지토리 (repo)에 대해 GitHub이 스케줄을 비활성화할 경우를 대비해 한 달의 여유를 가질 수 있습니다.
이것이 네이티브 iOS 로그인에 영향을 미치나요? 아니요. 네이티브 signInWithIdToken 플로우 (flow)는 클라이언트 시크릿 (client secret)을 사용하지 않습니다. 이것이 웹 (web)은 작동하지 않는 동안 iOS는 계속 작동하는 이유입니다.
만료가 정말로 조용히 진행되나요? 네. Apple로부터 오는 이메일도 없고, 대시보드 (dashboard) 경고도 없습니다. 첫 번째 신호는 프로덕션 (production) 환경에서 발생하는 invalid_client 오류입니다.
저 또한 6개월의 벽에 부딪힌 후 이 도구를 만들었습니다. MIT 라이선스로 제공되며 무료이며, GitHub Marketplace에서 이용할 수 있습니다. 이슈 (Issues)와 PR (Pull Requests)은 언제나 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기