Sign in with Apple이 보내는 알림은 세 종류가 아니라 네 종류입니다
Apple이 Sign in with Apple의 서버 간 알림(server-to-server notification)에 대해 낸 개발자 공지는 엔드포인트가 받게 될 내용을 세 가지로 정리합니다. 이메일 전달 설정 변경, 앱 내 계정 삭제, 그리고 Apple 계정의 영구 삭제입니다.3 그런데 API 문서가 정의하는 이벤트 유형은 네 가지입니다.1
공지만 보고 구현하면 분기는 세 개만 만들어지고, 네 번째 이벤트는 아무 소리 없이 버려집니다. 공지는 email-enabled와 email-disabled를 ‘전달 설정’이라는 항목 하나로 합쳐 놓았습니다. 실제로는 서로 다른 type 값을 가진 별개의 알림으로 도착하고, 기본 분기 없이 type으로만 갈라지는 코드는 작성자가 빠뜨린 쪽을 그대로 무시합니다.
게다가 네 이벤트 중 둘은 이름이 풍기는 것보다 큰 의미를 갖고, 그중 하나는 앱의 인증 상태를 바꿔 놓습니다.
요약 (TL;DR)
Sign in with Apple이 보내는 서버 간 알림은 email-enabled, email-disabled, consent-revoked, account-deleted 네 가지입니다.1 페이로드는 Apple의 개인 키로 서명된 JWS이며, payload 키 아래에 담긴 JSON 객체 안에 감싸인 채로 도착합니다. 무언가를 처리하기 전에 헤더의 alg 파라미터가 지정한 알고리즘으로 서명을 먼저 검증해야 합니다.1 consent-revoked는 사용자의 자격 증명을 무효로 만들기 때문에, 설정 변경이 아니라 인증 이벤트입니다. Apple 계정이 삭제될 때 네이티브 앱에는 클라이언트 측 콜백이 오지 않으므로, 서버 알림이 유일한 신호입니다.1 재시도나 전달 보장에 대해서는 Apple이 아무것도 문서화하지 않았습니다.
네 가지 이벤트 유형
모든 알림은 events 클레임 안에 type 값을 담고 있습니다.1
type |
무슨 일이 일어났는가 |
|---|---|
email-enabled |
사용자가 나의 이메일 가리기(Hide My Email)로 개인 주소에 대한 이메일 전달을 활성화했습니다 |
email-disabled |
사용자가 이메일 전달을 비활성화했습니다 |
consent-revoked |
사용자가 앱에 대한 동의를 철회했고, 해당 자격 증명이 무효가 되었습니다 |
account-deleted |
사용자가 Apple 계정의 영구 삭제를 요청했습니다 |
공지가 하나로 합쳐 버린 것이 바로 이 두 이메일 이벤트입니다. 둘은 따로 의미가 있습니다. email-disabled는 릴레이 주소로 보낸 메일이 더 이상 사용자에게 닿지 않는다는 뜻이고, email-enabled는 다시 닿기 시작한다는 뜻입니다. 이 둘을 ‘설정이 바뀜’이라는 이벤트 하나로 뭉뚱그리면, 알림이 이미 알려 준 현재 상태를 굳이 다시 조회해야 합니다.
이름만 봐서는 알 수 없는 두 이벤트
consent-revoked는 인증 이벤트입니다. Apple의 설명은 사용자가 “앱이 자신의 Apple 계정을 사용하도록 한 동의를 철회하고, 그 자격 증명이 무효가 된다”는 것입니다.1 더 이상 권장되지 않는 상태도, 만료를 앞둔 상태도 아닙니다. 무효입니다.
이 이벤트를 이메일 이벤트와 나란히 로그에 남기고 설정 행 하나를 갱신하는 데 그치는 앱은, 더 이상 인증되지 않는 자격 증명 위에 로그인된 화면을 계속 띄우게 됩니다. 사용자는 다음 토큰 갱신이 실패할 때까지 자기 계정을 보다가, 그다음에는 더 나쁜 것을 보게 됩니다. 올바른 처리는 세션을 종료하고 재인증으로 보내는 것입니다. 철회된 OAuth 권한을 다룰 때와 같은 코드 경로를 쓰면 됩니다.
account-deleted가 받을 수 있는 유일한 통지일 수 있습니다. Apple에 따르면 사용자가 Apple 계정을 영구 삭제하면 Sign in with Apple은 해당 사용자의 모든 토큰을 무효화하고 연결된 모든 앱에서 이메일 전달을 비활성화하며, 네이티브 앱의 경우 시스템이 클라이언트 측 콜백을 보내지 않습니다.1
이 문장이야말로 엔드포인트를 운영해야 하는 가장 강력한 근거입니다. 서버 간 알림 엔드포인트가 없는 iOS 전용 팀에는 계정이 사라졌다는 사실을 알아낼 방법이 아예 없습니다. 레코드는 그대로 남고, 릴레이 주소는 동작을 멈추며, 삭제해야 할 것이 있다는 사실을 아무도 알려 주지 않았으므로 삭제 의무도 이행되지 못한 채 남습니다.
엔드포인트 등록하기
설정은 Certificates, Identifiers & Profiles에서 합니다. Identifiers를 선택하고, 해당 App ID를 고른 뒤, Sign in with Apple 서비스를 활성화하고, Configure를 클릭해 엔드포인트 URL을 입력하면 됩니다.2
설계를 시작하기 전에 제약부터 읽어 두는 편이 좋습니다.2
- Sign in with Apple 앱 그룹과 키마다 URL 하나. 앱마다 하나가 아닙니다.
- 기본(primary) App ID에서만 등록할 수 있습니다.
- URL은 스킴, 호스트, 경로를 모두 갖춘 절대 URI여야 합니다:
https://example.com/path/to/endpoint - 알림을 받으려면 TLS 1.2 이상이 필요합니다.
Apple의 API 문서는 여러 개발자 팀과 여러 앱이 같은 URL을 사용해도 된다고 덧붙입니다.1 앱 그룹마다 URL 하나라는 규칙과 함께 읽으면, 하나의 서비스가 모든 알림을 받고 각 그룹은 그 서비스를 가리키는 자기 몫의 등록을 갖는다는 해석이 합리적입니다. 다만 Apple이 두 규칙의 관계를 명시하지는 않았으므로, 공용 엔드포인트는 ‘공인된 방식’이 아니라 ‘무리 없이 동작하는 방식’ 정도로 보는 편이 안전합니다. 그리고 거기에 의존하기 전에, 핸들러가 알림이 어느 앱에 관한 것인지 구분할 수 있는지부터 확인해야 합니다.
TLS 1.2라는 하한선은 더 큰 흐름과 이어져 있습니다. OS 27은 관리 트래픽에 더 엄격한 TLS 요구사항을 적용하기 시작했고, 같은 1.2 최소 버전에 더해 ATS 규격에 맞는 암호 스위트와 인증서를 요구합니다. 오늘 Apple의 요구사항을 충족하는 엔드포인트가 곧바로 ATS를 만족하는 것은 아니며, 흐름은 느슨해지는 쪽이 아니라 엄격해지는 쪽입니다.
페이로드 읽기
알림은 HTTP POST로 도착하며, 본문은 서명된 토큰을 품고 있는 JSON 객체입니다.1
{
"payload": "<SERVER_TO_SERVER_NOTIFICATION_JWS>"
}
JWS는 날것으로 오지 않고 한 겹 감싸여 옵니다. JSON 본문을 파싱해 payload를 꺼낸 다음에 검증해야 합니다. 요청 본문 전체를 JWS 검증기에 그대로 넘기는 구현은 첫 알림부터 실패하는데, 그 실패가 ‘감싸는 구조를 놓쳤다’가 아니라 ‘토큰 형식이 잘못됐다’로 나타나기 때문에 엉뚱한 곳을 뒤지게 됩니다.
해석보다 검증이 먼저입니다. 페이로드는 JSON Web Signature 형식으로 Apple의 개인 키에 의해 암호학적으로 서명되어 있고, Apple의 지침은 JWS를 살펴 헤더의 alg 파라미터가 지정한 알고리즘으로 서명을 검증하라는 것입니다.1 서명이 통과한 뒤에야 events 클레임을 읽고 type으로 분기합니다.
일반적인 JWS 실무에서 가져올 만한 습관이 두 가지 있습니다. 호출자가 검증 강도를 낮출 수 있게 해 주는 alg 값은 절대 신뢰하지 말 것, 그리고 형식만 맞으면 어떤 Apple 서명 토큰이든 받아들이는 대신 토큰의 발급자와 대상이 기대한 값과 일치하는지 확인할 것.
디코딩된 형태
검증을 마친 consent-revoked 알림을 디코딩하면 다음과 같습니다.1
{
"iss": "https://appleid.apple.com",
"aud": "com.mytest.app",
"iat": 1508184845,
"jti": "abede...67890",
"events": {
"type": "consent-revoked",
"sub": "820417.faa325acbc78e1be1668ba852d492d8a.0219",
"event_time": 1508184845
}
}
account-deleted는 type만 다를 뿐 같은 필드를 담고 있습니다. 이메일 이벤트에는 여기에 email과 is_private_email 두 개가 더 붙습니다.
이 구조에서 미리 알지 못한 채 마주치면 시간을 잡아먹는 지점이 세 군데 있습니다.
events는 배열이 아니라 객체입니다. 이름은 복수형인데 값은 이벤트 하나입니다. 구조가 아니라 이름을 보고 짠 코드는 딕셔너리를 순회하다가 키만 얻게 됩니다.
is_private_email은 문자열입니다. Apple의 예제는 JSON 불리언 true가 아니라 따옴표가 붙은 "true"를 보여 줍니다. 이 값을 Bool로 매핑하는 엄격한 디코더는 실패하고, 비어 있지 않은 문자열이면 참으로 취급하는 관대한 디코더는 엉뚱한 이유로 정답을 맞히다가 "false"에서 틀립니다.
sub는 안정적인 사용자 식별자로, 로그인 시점에 받았던 값과 같습니다. 이 알림이 어느 계정에 관한 것인지 찾아내는 열쇠가 바로 이 값입니다. aud는 클라이언트 식별자이며, 공용 엔드포인트가 여러 앱의 알림을 나눠 처리할 수 있게 해 주는 것도 이 필드입니다.
Apple의 예제 자체에 대한 참고 사항이 하나 있습니다. 두 이메일 페이로드에는 "is_private_email": "true"와 "event_time" 사이에 쉼표가 빠져 있습니다. 둘 중 어느 블록이든 그대로 복사해 JSON 파서에 넣으면 문서가 거부됩니다. 구조는 맞고 문장부호가 틀린 것인데, 이걸 테스트 픽스처에 붙여 넣은 사람은 자기 잘못도 아닌 구문 오류에 10분을 날리게 됩니다.
용어도 오락가락합니다. 본문은 페이로드를 JSON Web Signature 형식이라고 설명하는데, 감싸는 예제에서는 그 값의 이름을 SERVER_TO_SERVER_NOTIFICATION_JWT로 붙여 놓았습니다.1 같은 객체입니다. 서명된 JWT는 JSON 페이로드를 가진 JWS이니까요. Apple 문서를 검색하다 두 용어를 모두 마주쳤을 때 알아 두면 좋습니다.
처음부터 끝까지 동작하는 핸들러
올바른 핸들러의 모양은 위의 제약에서 그대로 따라 나옵니다. 다음은 PyJWT를 사용한 Python 예시입니다.
import json
import jwt
from jwt import PyJWKClient
# Apple publishes its signing keys as a JWKS. Cache the client;
# it fetches and caches keys rather than hitting Apple per request.
JWKS = PyJWKClient("https://appleid.apple.com/auth/keys")
CLIENT_ID = "com.mytest.app" # your aud value
def handle_notification(request_body: bytes):
# 1. The JWS is wrapped in JSON under "payload", not the raw body.
wrapper = json.loads(request_body)
token = wrapper["payload"]
# 2. Resolve the signing key by the token's kid, then verify.
# Pin the algorithm. Never read alg from the token to decide.
signing_key = JWKS.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=CLIENT_ID,
issuer="https://appleid.apple.com",
)
# 3. Only now is anything trustworthy.
event = claims["events"] # an object, not a list
apple_user_id = event["sub"] # stable identifier from sign-in
match event["type"]:
case "consent-revoked" | "account-deleted":
end_all_sessions(apple_user_id)
mark_account_unlinked(apple_user_id)
case "email-disabled":
set_email_forwarding(apple_user_id, enabled=False)
case "email-enabled":
set_email_forwarding(apple_user_id, enabled=True)
case other:
log_unknown_event(other) # do not fail silently
이 중 몇 줄은 구조를 떠받치는 줄입니다.
알고리즘을 고정하세요. 현재 Apple이 운영 중인 JWKS에는 RSA 키 세 개가 게시되어 있고, 셋 다 RS256에 use: sig이며 kid 값만 서로 다릅니다. 토큰에서 alg를 읽는 대신 algorithms=["RS256"]을 명시하면, 공격자가 alg: none을 주장하는 토큰을 밀어 넣는 고전적인 다운그레이드 경로가 막힙니다.
첫 번째 키를 집어 오지 말고 kid로 찾으세요. 키 세 개가 동시에 살아 있다는 것은 바깥에서 본 키 순환(rotation)의 모습입니다. keys[0]을 잡아 오는 핸들러는 Apple이 키를 교체하기 전까지만 잘 돌아가고, 그 뒤로는 일부 토큰에서만 실패합니다. 디버깅하기 아주 괴로운 종류의 실패입니다.
aud와 iss를 검증하세요. 이 둘이 없으면 다른 앱을 위해 발급된 것까지 포함해, 서명만 유효하면 어떤 Apple 토큰이든 받아들이게 됩니다.
기본 분기를 남겨 두세요. 그러지 않으면 다섯 번째 이벤트 유형이 생겼을 때 소리 없이 사라집니다. 세 항목짜리 Apple 공지만 보고 짠 구현이 지금 email-enabled나 email-disabled를 흘리는 방식이 정확히 이것입니다.
Apple이 문서화하지 않은 것
API 문서에도, 계정 도움말 페이지에도 전달 보장에 관한 언급은 없습니다. 두 문서에서 재시도(retry), 재전송(redelivery), 수신 확인(acknowledgment), 상태 코드, 타임아웃, 멱등성(idempotency)을 찾아봐도 아무것도 나오지 않습니다.
그래서 이 글을 쓰는 시점 기준으로 Apple 문서만으로는 답할 수 없는 것들이 다음과 같습니다.
- 전달이 실패하면 재시도하는지, 한다면 몇 번 하는지
- 재시도가 있다면 어느 정도 기간에 걸쳐 이루어지는지
- 성공을 알리려면 엔드포인트가 어떤 상태 코드를 반환해야 하는지
- 같은 알림이 두 번 도착할 수 있는지
이 공백은 설계에 영향을 줍니다. 여기서부터는 Apple이 밝힌 내용을 전하는 것이 아니라 그 너머를 추론하는 이야기입니다. 전달 방식이 명시되지 않은 엔드포인트는 신뢰할 수 있는 이벤트 스트림으로 취급할 수 없습니다. 방어적인 태도는 각 알림을 ‘무언가 바뀌었다’는 힌트로 받아들이고, 이벤트를 그대로 적용하는 대신 자체 기록과 대조하는 것입니다. 중복을 배제할 수 없으니 핸들러는 멱등하게 만드세요. 모든 알림을 다 받았는지 확인할 방법이 없으니, 모든 알림을 받았다는 전제 위에서만 올바르게 동작하는 흐름은 만들지 마세요.
엔드포인트가 한 시간 동안 죽어 있었다면, 그 한 시간 치 계정 삭제 알림을 잃어버린 것인지 어딘가에 쌓여 있는 것인지 문서만으로는 알 수 없습니다. 잃어버렸다고 가정하고 설계하세요.
테스트할 방법도 문서화되어 있지 않습니다
두 페이지에서 테스트 환경, 시뮬레이션, 트리거 같은 키워드를 검색해도 결과가 없습니다. 원할 때 알림을 발생시키는 방법을 Apple은 어디에도 문서화해 두지 않았습니다.
그래서 난처한 순환에 빠집니다. 가장 중요한 이벤트인 consent-revoked와 account-deleted는 사용자가 앱 접근 권한을 철회하거나 Apple 계정을 삭제해야 발생합니다. 진짜 account-deleted로 핸들러를 검증하려면 누군가는 Apple 계정을 실제로 삭제해야 한다는 뜻입니다. 두 번 돌릴 수 있는 종류의 테스트가 아닙니다.
현실적인 대안은 문제를 둘로 쪼개는 것입니다. 먼저 위의 구조를 참고해 디코딩된 페이로드를 직접 만들고, 그것으로 분기와 멱등성, 대조 로직을 단위 테스트합니다. 그리고 별도로, 실제로 만들어 낼 수 있는 알림으로 전송 경로와 서명 검증 경로를 시험합니다. 테스트용 Apple 계정의 동의를 철회하는 일은 계정을 삭제하는 것과 달리 되돌릴 수 있고, 감싸는 구조의 파싱과 kid 조회, 서명 검증까지 처음부터 끝까지 한 번에 훑어 줍니다.
무엇을 하든, 등록하기 전에 엔드포인트에 도달할 수 있는지와 응답이 지체 없이 돌아오는지부터 확인하세요. 등록만 해 놓고 한 번도 확인하지 않은 엔드포인트는, 출시 이후의 모든 알림이 만료된 인증서 뒤의 URL로 갔다는 사실을 몇 달 뒤에 알게 되는 경로입니다.
이미 시행 중인 요구사항
이 이야기가 개발자 뉴스에 등장한 이유는 이렇습니다. 2026년 1월 1일부터 대한민국에 소재한 개발자는 Sign in with Apple로 웹사이트를 앱과 연결하려면, 새 Services ID를 등록하거나 기존 것을 수정할 때 서버 간 알림 엔드포인트를 반드시 제공해야 합니다.3 Apple은 이를 2025년 10월 9일에 공지했습니다.
요구사항 자체의 범위는 좁고, 앞으로 대비할 일이 아니라 이미 몇 달째 시행 중인 사항입니다. 흥미로운 지점은 방향입니다. Apple은 적어도 한 관할권에서 엔드포인트를 의무로 만들기 시작했고, 그 이유는 어디에나 적용됩니다. 사람들이 공유한 개인정보를 스스로 통제할 수 있게 하는 것, 그리고 계정 삭제가 실제로 끝까지 전파되게 하는 것입니다. 이 논리에 한국에만 해당하는 부분은 없습니다.
어차피 엔드포인트를 만들 생각이라면, 규제 당국이 마감일을 정해 주기 전에 만들어 두는 편이 낫습니다.
핵심 정리
백엔드 엔지니어에게:
- 유형은 셋이 아니라 넷을 처리하세요. email-enabled와 email-disabled는 따로 도착합니다.
- JWS 검증기에 무언가를 넘기기 전에 JSON 본문을 파싱해 payload를 꺼내세요.
- events 클레임을 읽기 전에 헤더의 alg가 지정한 알고리즘으로 서명을 검증하세요.
- 핸들러는 멱등하게 만들고, 자체 기록과 대조하세요. 전달 보장은 Apple 문서에 없습니다.
iOS 팀에게:
- consent-revoked는 자격 증명을 무효화합니다. 설정 갱신이 아니라 세션을 끝내는 인증 이벤트로 다루세요.
- Apple 계정이 삭제돼도 네이티브 앱에는 클라이언트 측 콜백이 오지 않습니다. 엔드포인트가 없으면 그 사실을 영영 알 수 없습니다.
만들지 말지 저울질하고 있다면: - 2026년 1월부터 한국에 소재한 개발자에게는 이미 의무이고, 그 근거는 다른 곳에도 그대로 적용됩니다.
자주 묻는 질문 (FAQ)
알림 유형은 몇 가지인가요?
네 가지입니다. email-enabled, email-disabled, consent-revoked, account-deleted.1 Apple 개발자 뉴스 공지는 두 이메일 이벤트를 ‘전달 설정’ 항목 하나로 합쳐 세 가지로 설명합니다.3
consent-revoked는 세션에 어떤 의미인가요?
사용자의 자격 증명이 무효가 됩니다.1 철회된 OAuth 권한을 다루듯 처리하세요. 설정 하나를 갱신하고 그대로 진행하는 대신, 세션을 종료하고 사용자를 재인증으로 보내야 합니다.
네이티브 iOS 앱만 출시하는데도 엔드포인트가 필요한가요?
Apple은 Apple 계정이 영구 삭제될 때 네이티브 앱에는 시스템이 클라이언트 측 콜백을 보내지 않는다고 밝히고 있습니다.1 서버 간 알림 엔드포인트가 없으면 그 사실을 알려 줄 수단이 아예 없습니다.
엔드포인트는 무엇을 반환해야 하고, 죽어 있으면 어떻게 되나요?
Apple은 상태 코드에 대한 기대치도, 재시도 동작도, 중복 전달 여부도 문서화하지 않았습니다. 최소 1회(at-least-once), 혹은 경우에 따라 최대 1회(at-most-once) 전달을 가정해 설계하고, 핸들러를 멱등하게 만들며, 모든 알림이 도착했다고 가정하지 말고 자체 기록과 대조하세요.
엔드포인트 하나로 여러 앱을 처리할 수 있나요?
Apple 문서는 여러 개발자 팀과 앱이 같은 URL을 사용해도 된다고 말합니다.1 등록은 기본(primary) App ID에서, Sign in with Apple 앱 그룹과 키마다 URL 하나입니다.2 각 알림이 어느 앱에 관한 것인지 핸들러가 판별할 수 있다면 공용 서비스도 문제없이 동작합니다.
출처
-
Apple, “Processing changes for Sign in with Apple accounts.” 네 가지 이벤트 유형(
email-enabled,email-disabled,consent-revoked,account-deleted), JWS 페이로드 형식과 헤더의alg파라미터로 검증하라는 지침,{"payload": "<JWS>"}형태로 감싸는 구조, 동의 철회가 자격 증명을 무효로 만든다는 서술, 계정 삭제 시 네이티브 앱에는 클라이언트 측 콜백이 없다는 설명, TLS 1.2 서버 요구사항, 여러 팀과 앱이 하나의 URL을 함께 쓸 수 있다는 허용 조항의 출처. 2026년 8월 2일 확인. ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Apple, “Enabling server-to-server notifications.” Certificates, Identifiers & Profiles를 통한 등록 경로, 앱 그룹과 키마다 URL 하나 규칙, 기본 App ID 제한, 절대 URI 요구사항, TLS 1.2 요구사항의 출처. 2026년 8월 2일 확인. ↩↩↩
-
Apple Developer News, “New requirement for apps using Sign in with Apple for account creation,” 2025년 10월 9일. 2026년 1월 1일부터 시행된 한국 요구사항, 그리고 엔드포인트가 받는 내용을 세 항목으로 요약한 부분의 출처. ↩↩↩