Skip to content

[#163] feat. 애플 네이티브 로그인 (App Store 심사 요건 4.8) - #164

Merged
howudong merged 3 commits into
mainfrom
feature/163
Sep 7, 2026
Merged

howudong merged 3 commits into
mainfrom
feature/163

Conversation

@nuyh99

@nuyh99 nuyh99 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #163

iOS 앱을 App Store에 올리려면 심사 지침 4.8에 따라, 카카오 로그인을 제공하는 이상 Sign in with Apple도 함께 내야 합니다.

계약

POST /api/v1/users/social-login/apple/native
X-API-Key: <키>

{ "identityToken": "<애플 SDK ID 토큰>", "rawNonce": "<선택>", "name": "<선택>" }

응답과 refreshToken 쿠키는 카카오 네이티브 로그인과 완전히 동일합니다(accessToken·signupRequired·sanctioned·sanctionCode·suspendedUntil).

왜 인터페이스를 하나 더 만들었나

애플은 카카오와 흐름이 다릅니다.

카카오 애플
앱이 주는 것 액세스 토큰 ID 토큰(JWT)
사용자 정보 GET /v2/user/me 토큰 클레임 — userinfo API 없음
인증의 근거 API 응답 서명 검증 그 자체
이름 매번 최초 인가 1회만 (ID 토큰에도 없음)

기존 OAuthClient(getAuthorizationUrl/getAccessToken/getUserInfo)에 애플을 끼우면 세 메서드 중 둘이 "지원하지 않음"이 됩니다. 그래서 네이티브 인증만 NativeSocialAuthenticator로 분리했습니다 — OAuthClient는 카카오 웹 리다이렉트 전용으로 남고, 회원 생성·제재 판정·토큰 발급은 두 제공자가 OAuthFacade에서 그대로 공유합니다.

ID 토큰 검증 (AppleIdTokenVerifier)

사용자 정보 API가 없으니 검증이 곧 인증입니다. 다섯 가지를 봅니다:

  1. 서명 — 애플 JWKS 공개키
  2. iss = https://appleid.apple.com
  3. aud = 설정한 클라이언트 ID(네이티브는 앱 번들 ID pics.ditto.app) — 다른 앱에서 발급된 토큰을 막는 지점
  4. exp
  5. nonce — 앱이 rawNonce를 보냈을 때만, 애플에 넘긴 SHA-256 해시와 대조

공개키는 6시간 TTL 캐시를 두되, 캐시에 없는 kid가 오면 한 번 다시 받아옵니다 — 애플이 서명 키를 교체한 직후의 정상 로그인을 실패로 만들지 않기 위해서입니다.

운영 준비물: 없습니다

새 비밀값·키 파일이 하나도 필요 없습니다. 인가 코드 교환을 하지 않기 때문입니다(그걸 하려면 .p8 키로 서명한 클라이언트 시크릿 JWT가 필요합니다). 검증에 쓰는 건 공개된 번들 ID뿐이고, 기본값이 pics.ditto.app으로 들어가 있습니다. 웹(Services ID)이 붙으면 APPLE_CLIENT_IDS에 콤마로 덧붙이면 됩니다.

하지 않은 것

  • 웹 리다이렉트 애플 로그인 — 심사 요건은 앱에만 적용되고, 애플 웹은 response_mode=form_post(POST 콜백)라 현재 GET 콜백 구조와 맞지 않습니다. 웹은 카카오 유지
  • 카카오·애플 계정 연결 — 같은 사람이 두 제공자로 로그인하면 별도 회원이 됩니다. 이메일로 잇는 건 애플 릴레이 주소 때문에 신뢰할 수 없고, 이메일 일치를 병합 근거로 삼는 것 자체가 계정 탈취 경로입니다. 필요해지면 로그인된 상태에서 명시적으로 잇는 기능으로 따로 다루는 게 맞다고 봅니다 — 이견 있으면 알려주세요
  • 성별·나이는 애플도 주지 않으므로 카카오 일반 앱과 똑같이 온보딩에서 받습니다(ADR 0021)

검증

  • ./gradlew build check 통과
  • AppleIdTokenVerifierTest 15케이스 — 실제 RSA 키로 토큰을 서명해 검증합니다: 정상 통과 3, 거부 5(다른 키 서명·발급자 위조·다른 앱 aud·만료·JWT 아님), nonce 3, 공개키 캐시 4(재사용·kid 교체 시 재조회·재조회해도 없으면 거부·TTL 만료)
  • OAuthControllerTest — 애플 로그인(REST Docs)·identityToken 누락 400·카카오와 별도 회원이 되는지
  • OAuthFacadeTest — 신규 가입 시 이름 저장, 재로그인 때 이름 없어도 기존 값 유지, 제공자별 소셜 계정 분리
  • 배경은 ADR 0022 + docs/domains/auth.md·socialaccount.md

🤖 Generated with Claude Code

iOS 앱은 카카오 로그인을 제공하는 이상 애플 로그인도 함께 내야 한다.

애플은 카카오와 흐름이 다르다 — 앱이 주는 건 액세스 토큰이 아니라 ID 토큰(JWT)이고,
사용자 정보 API가 없어 서명 검증이 곧 인증이며, 이름은 최초 인가 1회만 클라이언트에 간다.
기존 OAuthClient(인가 URL/코드 교환/userinfo)에 끼우면 세 메서드 중 둘이 미지원이 되므로
네이티브 인증을 NativeSocialAuthenticator 로 분리했다.

- POST /api/v1/users/social-login/apple/native — 응답은 카카오 네이티브와 동일 스키마.
- AppleIdTokenVerifier: JWKS 서명 + iss·aud·exp 검증, rawNonce 를 주면 nonce 까지 대조.
  공개키는 TTL 캐시를 두되 모르는 kid 가 오면 한 번 다시 받아온다(애플 키 교체 대응).
- 인가 코드 교환은 하지 않는다 — 검증에 필요한 건 공개된 번들 ID 뿐이라 새 비밀값이 없다.
- 회원 생성·제재 판정·토큰 발급은 카카오와 같은 OAuthFacade 경로를 쓴다.
- 카카오 계정과 잇지 않는다(제공자별 별도 회원). 이메일 병합은 애플 릴레이 주소와
  계정 탈취 위험 때문에 택하지 않았다.

배경: ADR 0022.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nuyh99
nuyh99 requested a review from howudong September 7, 2026 05:45
@nuyh99

nuyh99 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

FE 연동 가이드를 위키에 올렸습니다 → Frontend-Apple-Login-Guide

앱이 챙겨야 할 것 세 가지(이름은 최초 1회만 온다 / rawNonce 권장 / authorizationCode는 보낼 필요 없음)와, 카카오 계정과 이어지지 않는다는 점의 화면 안내를 정리했습니다.

nuyh99 and others added 2 commits September 7, 2026 14:54
SonarCloud New Code 커버리지가 71.5%(기준 80%)로 게이트에 걸렸다. 새 클래스가 전부
infrastructure 모듈인데 검증은 api 통합 테스트로만 했다 — 모듈별 집계라 잡히지 않는다.

- NativeSocialAuthenticatorTest(infrastructure): 카카오 위임, 애플 매핑(sub·이메일·
  애플이 안 주는 값·최초 1회 이름·nonce 전달), Fake 인증기, 팩토리의 미등록 제공자.
- AppleIdTokenVerifierTest: is_private_email 의 boolean·부재 분기와 sub 없는 토큰 거부.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AppleOAuthProperties 를 만들면서 yml 에 블록을 넣지 않았다. local·test 는 Fake 인증기를
쓰므로 테스트가 전부 통과했지만, prod 에서는 client-ids 가 빈 리스트라 모든 애플 토큰의
aud 검증이 실패했을 것이다 — 설정 누락이 클라이언트 오류(1002)처럼 보이는 상태였다.

- application-oauth.yml 의 local·test / prod 두 프로파일에 apple 블록 추가.
  prod 는 APPLE_CLIENT_IDS 로 주입하고 기본값은 앱 번들 ID.
- clientIds 가 비면 WarnException(1002) 이 아니라 ErrorException(INTERNAL_ERROR) 으로
  올린다. 조용히 전부 거부되는 대신 서버 설정 문제로 드러나야 한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nuyh99

nuyh99 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

설정 누락을 하나 고쳐서 푸시했습니다(5643e54).

AppleOAuthProperties 를 만들면서 application-oauth.yml 에 apple 블록을 실제로 넣지 않았습니다. local·test 는 Fake 인증기를 타서 테스트가 전부 통과했지만, prod 에서는 client-ids 가 빈 리스트라 모든 애플 토큰의 aud 검증이 실패했을 것입니다 — 설정 누락이 클라이언트 오류(1002)처럼 보이는 상태였습니다.

  • 두 프로파일에 apple 블록 추가 (prod 는 APPLE_CLIENT_IDS, 기본값은 앱 번들 ID)
  • clientIds 가 비면 1002 가 아니라 INTERNAL_ERROR 로 올립니다. 조용히 전부 거부되는 대신 서버 설정 문제로 드러나야 해서요

앞서 PR 본문에 "기본값이 pics.ditto.app 으로 들어가 있습니다"라고 적었는데, 이 커밋 전까지는 사실이 아니었습니다. 지금은 맞습니다.

@sonarqubecloud

sonarqubecloud Bot commented Sep 7, 2026

Copy link
Copy Markdown

@howudong
howudong merged commit 868468c into main Sep 7, 2026
2 checks passed
@howudong
howudong deleted the feature/163 branch September 7, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat. 애플 네이티브 로그인 (App Store 심사 요건 4.8)

2 participants