← Wszystkie wpisy

Sign in with Apple wysyła cztery powiadomienia, nie trzy

Ogłoszenie Apple dla deweloperów, dotyczące powiadomień serwer–serwer w Sign in with Apple, wymienia trzy rzeczy, które trafią do punktu końcowego: zmiany preferencji przekazywania poczty, usunięcia konta w aplikacji oraz trwałe usunięcia Apple Account.3 Dokumentacja API definiuje cztery odrębne typy zdarzeń.1

Implementacja napisana na podstawie ogłoszenia obsługuje trzy gałęzie i po cichu gubi czwarte zdarzenie. Ogłoszenie zwija email-enabled i email-disabled w jeden punkt o preferencjach przekazywania poczty. W rzeczywistości przychodzą jako osobne powiadomienia z osobnymi wartościami typu, a kod rozgałęziający się po typie bez gałęzi domyślnej ignoruje to z nich, o którym autor akurat zapomniał.

Dwa z czterech zdarzeń znaczą przy tym więcej, niż sugerują ich nazwy, a jedno z nich zmienia stan uwierzytelnienia aplikacji.

W skrócie

Sign in with Apple dostarcza cztery typy powiadomień serwer–serwer: email-enabled, email-disabled, consent-revoked oraz account-deleted.1 Ładunek przychodzi jako JWS opakowany w obiekt JSON pod kluczem payload, podpisany kluczem prywatnym Apple, i przed podjęciem jakiegokolwiek działania musi zostać zweryfikowany algorytmem wskazanym w parametrze alg nagłówka.1 Zdarzenie consent-revoked unieważnia poświadczenia użytkownika, jest więc zdarzeniem uwierzytelniania, a nie zmianą preferencji. Aplikacje natywne nie otrzymują żadnego wywołania zwrotnego po stronie klienta, gdy Apple Account zostaje usunięte — powiadomienie serwerowe jest wtedy jedynym sygnałem.1 Apple nie dokumentuje przy tym ani semantyki dostarczania, ani ponawiania prób.

Cztery typy zdarzeń

Każde powiadomienie niesie wartość type wewnątrz oświadczenia events.1

type Co się wydarzyło
email-enabled Użytkownik włączył przekazywanie poczty na swój adres osobisty przy użyciu Hide My Email
email-disabled Użytkownik wyłączył przekazywanie poczty
consent-revoked Użytkownik cofnął zgodę dla aplikacji, a jego poświadczenia stały się nieważne
account-deleted Użytkownik zażądał trwałego usunięcia swojego Apple Account

Właśnie te dwa zdarzenia pocztowe ogłoszenie scala w jedno. Liczą się jednak niezależnie od siebie: email-disabled oznacza, że poczta wysyłana na adres przekaźnikowy przestaje docierać do użytkownika, a email-enabled — że znów dociera. Potraktowanie ich jako jednego zdarzenia „zmieniły się preferencje” zmusza do dopytywania o bieżący stan, który powiadomienie już przecież podało.

Dwa zdarzenia, które nie są tym, na co wyglądają

consent-revoked to zdarzenie uwierzytelniania. Apple opisuje je tak: użytkownik „cofa zgodę na używanie przez aplikację jego Apple Account, a jego poświadczenia stają się nieważne”.1 Nie przestarzałe, nie oczekujące na wygaśnięcie. Nieważne.

Aplikacja, która zapisze to w logu obok zdarzeń pocztowych i zaktualizuje wiersz z preferencjami, będzie dalej pokazywać interfejs zalogowanego użytkownika oparty na poświadczeniach, które już nie uwierzytelniają. Użytkownik widzi swoje konto do momentu, w którym nie powiedzie się kolejne odświeżenie tokenu — a potem widzi coś gorszego. Poprawna obsługa polega na zakończeniu sesji i przekierowaniu do ponownego uwierzytelnienia, tą samą ścieżką kodu, której użyto by przy cofniętym uprawnieniu OAuth.

account-deleted może być jedynym powiadomieniem, jakie otrzymamy. Apple stwierdza, że gdy użytkownik trwale usuwa swoje Apple Account, Sign in with Apple unieważnia wszystkie tokeny użytkownika i wyłącza przekazywanie poczty we wszystkich powiązanych aplikacjach, a w przypadku aplikacji natywnych system nie wysyła wywołania zwrotnego po stronie klienta.1

To zdanie jest najmocniejszym argumentem za tym, żeby w ogóle utrzymywać taki punkt końcowy. Zespół pracujący wyłącznie nad iOS, bez punktu końcowego serwer–serwer, nie ma żadnego mechanizmu, by dowiedzieć się, że konta już nie ma. Rekord zostaje, adres przekaźnikowy przestaje działać, a ewentualny obowiązek usunięcia danych pozostaje niedopełniony, bo nic nie zasygnalizowało, że jest co usuwać.

Rejestracja punktu końcowego

Konfiguracja odbywa się w Certificates, Identifiers & Profiles: należy wybrać Identifiers, wskazać swój App ID, włączyć usługę Sign in with Apple, kliknąć Configure i podać adres URL punktu końcowego.2

Warto poznać ograniczenia, zanim zaprojektuje się wokół nich rozwiązanie.2

  • Jeden adres URL na grupę aplikacji Sign in with Apple i klucz. Nie jeden na aplikację.
  • Rejestracja możliwa wyłącznie na podstawowym App ID.
  • Adres musi być bezwzględnym URI ze schematem, hostem i ścieżką: https://example.com/path/to/endpoint
  • Do odbierania powiadomień wymagany jest TLS 1.2 lub nowszy.

Dokumentacja API dodaje, że tego samego adresu URL można używać dla wielu zespołów deweloperskich i wielu aplikacji.1 Czytana łącznie z regułą jednego adresu na grupę, rozsądna interpretacja brzmi tak: pojedyncza usługa może odbierać wszystko, a każda grupa rejestruje własny wskaźnik do niej. Apple nie opisuje jednak tej interakcji wprost, więc wspólny punkt końcowy warto traktować jako rozwiązanie wykonalne, a nie błogosławione — i upewnić się, że handler potrafi rozpoznać, której aplikacji dotyczy dane powiadomienie, zanim się na nim oprzemy.

Próg TLS 1.2 wpisuje się w szerszą zmianę. OS 27 zaczął egzekwować surowsze wymagania TLS dla ruchu zarządzania, z tym samym minimum 1.2 oraz zestawami szyfrów i certyfikatami zgodnymi z ATS. Punkt końcowy spełniający dzisiejszy wymóg Apple nie jest automatycznie zgodny z ATS, a kierunek zmian prowadzi ku zaostrzaniu, nie luzowaniu reguł.

Odczyt ładunku

Powiadomienie przychodzi jako żądanie HTTP POST, którego treścią jest obiekt JSON z podpisanym tokenem w środku:1

{
    "payload": "<SERVER_TO_SERVER_NOTIFICATION_JWS>"
}

JWS jest opakowany, nie surowy. Najpierw parsujemy JSON, wyciągamy payload, dopiero potem weryfikujemy. Implementacja, która przekazuje weryfikatorowi JWS całą treść żądania, wywraca się na pierwszym powiadomieniu — a błąd objawia się jako zniekształcony token, nie jako pomyłka w rozpakowaniu, co kieruje poszukiwania w zupełnie złą stronę.

Weryfikacja poprzedza interpretację. Ładunek jest kryptograficznie podpisany kluczem prywatnym Apple w formacie JSON Web Signature, a zalecenie Apple brzmi: zbadać JWS i użyć algorytmu wskazanego w parametrze alg nagłówka do weryfikacji podpisu.1 Dopiero po pozytywnej weryfikacji podpisu odczytuje się oświadczenie events i rozgałęzia po type.

Dwa nawyki z ogólnej praktyki JWS warto zachować: nigdy nie ufać wartości alg, która pozwoliłaby wywołującemu obniżyć poziom weryfikacji, oraz sprawdzać, czy wystawca i odbiorca tokenu zgadzają się z oczekiwaniami, zamiast przyjmować dowolny poprawnie zbudowany token podpisany przez Apple.

Zdekodowana struktura

Po weryfikacji zdekodowane powiadomienie consent-revoked wygląda tak: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 niesie te same pola z inną wartością type. Zdarzenia pocztowe dokładają dwa kolejne: email oraz is_private_email.

Trzy szczegóły tej struktury kosztują sporo czasu, jeśli natknie się na nie przez zaskoczenie.

events to obiekt, nie tablica. Nazwa jest w liczbie mnogiej, a wartością jest pojedyncze zdarzenie. Kod pisany pod nazwę, a nie pod strukturę, iteruje po słowniku i dostaje klucze.

is_private_email to ciąg znaków. W przykładach Apple widnieje "true" w cudzysłowie, a nie wartość logiczna true z JSON. Ścisły dekoder mapujący to pole na Bool zawodzi, a pobłażliwy, traktujący każdy niepusty ciąg jako prawdę, trafia z dobrą odpowiedzią z niewłaściwego powodu — i myli się przy "false".

sub to stabilny identyfikator użytkownika, ta sama wartość, którą otrzymano przy logowaniu, i to po niej odnajduje się konto, którego dotyczy powiadomienie. aud to identyfikator klienta — dzięki niemu wspólny punkt końcowy potrafi kierować powiadomienia do właściwej z kilku aplikacji.

Uwaga na marginesie o przykładach samego Apple: w obu ładunkach pocztowych brakuje przecinka między "is_private_email": "true" a "event_time". Wystarczy skopiować dowolny z tych bloków do parsera JSON, a dokument zostanie odrzucony. Struktura jest poprawna, interpunkcja nie — i czytelnik, który wkleja to do danych testowych, traci dziesięć minut na błąd składni, którego sam nie popełnił.

Terminologia Apple również się rozjeżdża. Tekst opisuje ładunek w formacie JSON Web Signature, podczas gdy przykład opakowania nazywa wartość SERVER_TO_SERVER_NOTIFICATION_JWT.1 To ten sam obiekt: podpisany JWT jest JWS z ładunkiem JSON. Warto o tym wiedzieć, przeszukując ich dokumentację i natrafiając na oba terminy.

Handler od początku do końca

Kształt poprawnego handlera wynika z powyższych ograniczeń. W Pythonie, z użyciem PyJWT:

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

Kilka z tych linii dźwiga cały ciężar.

Algorytm należy przypiąć na sztywno. Publikowany dziś przez Apple zestaw JWKS zawiera trzy klucze RSA, wszystkie RS256, z use: sig i odrębnymi wartościami kid. Przekazanie algorithms=["RS256"] zamiast odczytywania alg z tokenu zamyka klasyczną ścieżkę obniżenia zabezpieczeń, w której atakujący podsuwa token deklarujący alg: none.

Klucz trzeba rozstrzygać po kid, a nie brać pierwszego z brzegu. Trzy klucze są aktywne równocześnie — tak z zewnątrz wygląda rotacja kluczy. Handler sięgający po keys[0] działa do momentu rotacji, a potem zawodzi dla części tokenów, co jest wyjątkowo przykre w diagnostyce.

Warto weryfikować aud oraz iss. Bez tego przyjmuje się dowolny poprawnie podpisany token Apple, w tym wystawiony dla innej aplikacji.

Należy zostawić gałąź domyślną. Piąty typ zdarzenia w przeciwnym razie zniknąłby bez śladu. Dokładnie tak dziś implementacja napisana na podstawie trzypunktowego ogłoszenia Apple gubi email-enabled albo email-disabled.

Czego Apple nie dokumentuje

Ani dokumentacja API, ani strona pomocy dotycząca kont nie mówią nic o gwarancjach dostarczenia. Przeszukanie obu źródeł pod kątem ponawiania, ponownej wysyłki, potwierdzenia, kodów statusu, limitów czasu i idempotencji nie daje żadnych wyników.

Na dzień pisania tego tekstu dokumentacja Apple nie odpowiada więc na pytania:

  • Czy nieudana dostawa jest ponawiana i ile razy
  • W jakim oknie czasowym ponowienia następują, o ile w ogóle
  • Jaki kod statusu powinien zwracać punkt końcowy, by zasygnalizować sukces
  • Czy to samo powiadomienie może przyjść dwa razy

Ta luka ma konsekwencje projektowe — i tutaj wychodzę poza to, co Apple stwierdza, przechodząc od relacjonowania do wnioskowania. Punktu końcowego o nieokreślonej semantyce dostarczania nie da się traktować jako autorytatywnego strumienia zdarzeń. Postawa defensywna polega na tym, by każde powiadomienie traktować jako sygnał, że coś się zmieniło, i uzgadniać je z własnymi rekordami, zamiast stosować zdarzenie na ślepo. Handlery warto uczynić idempotentnymi, bo duplikatów nie sposób wykluczyć. Nie należy też budować przepływu pracy, którego poprawność zależy od otrzymania każdego powiadomienia, skoro nie da się potwierdzić, że wszystkie dotarły.

Jeśli punkt końcowy jest niedostępny przez godzinę, z dokumentacji nie wynika, czy stracono godzinę usunięć kont, czy też czekają one gdzieś w kolejce. Warto projektować tak, jakby przepadły.

Nie ma też udokumentowanego sposobu, by to przetestować

Przeszukanie obu stron pod kątem środowiska testowego, symulacji i ręcznego wyzwalania zdarzeń nie daje wyników. Apple nie dokumentuje żadnego mechanizmu wywołania powiadomienia na żądanie.

Zostaje więc niewygodna pętla. Zdarzenia, które liczą się najbardziej — consent-revoked i account-deleted — powstają w wyniku cofnięcia dostępu do aplikacji albo usunięcia Apple Account przez użytkownika. Sprawdzenie handlera na prawdziwym account-deleted oznacza, że ktoś musi usunąć Apple Account. Takiego testu nie przeprowadza się dwa razy.

Wykonalnym zamiennikiem jest rozdzielenie problemu. Zdekodowane ładunki można skonstruować samodzielnie na podstawie powyższych struktur i przetestować jednostkowo rozgałęzianie, idempotencję oraz logikę uzgadniania. Osobno warto sprawdzić warstwę transportu i podpisu na prawdziwym powiadomieniu, które da się rzeczywiście wywołać: cofnięcie zgody dla testowego Apple Account jest odwracalne w sposób, w jaki usunięcie konta nie jest, a przechodzi całą ścieżkę — parsowanie opakowania, wyszukanie po kid i sprawdzenie podpisu.

Cokolwiek się wybierze, przed rejestracją należy potwierdzić, że punkt końcowy jest osiągalny i odpowiada bez zwłoki. Punkt końcowy zarejestrowany i nigdy nie zweryfikowany to sposób, w jaki zespół odkrywa po miesiącach, że wszystkie powiadomienia od premiery szły pod adres schowany za wygasłym certyfikatem.

Wymóg, który już obowiązuje

Powód, dla którego temat w ogóle trafił do aktualności dla deweloperów: od 1 stycznia 2026 deweloperzy z siedzibą w Republice Korei muszą podać punkt końcowy powiadomień serwer–serwer przy rejestracji nowego Services ID lub aktualizacji istniejącego, aby powiązać witrynę z aplikacją korzystającą z Sign in with Apple.3 Apple ogłosiło to 9 października 2025.

Wymóg jest wąski i obowiązuje już od miesięcy, nie jest więc czymś, do czego dopiero należy się przygotować. Interesujący jest jego kierunek. Apple zaczęło wymagać takiego punktu końcowego w co najmniej jednej jurysdykcji, z pobudek, które łatwo uogólnić: dać ludziom kontrolę nad udostępnionymi danymi osobowymi i sprawić, by usunięcie konta faktycznie się propagowało. W tej logice nie ma nic swoistego dla Korei.

Skoro punkt końcowy i tak trzeba zbudować, lepiej zrobić to, zanim termin wyznaczy regulator.

Najważniejsze wnioski

Dla inżynierów backendu: - Obsłużyć cztery typy, nie trzy. email-enabled i email-disabled przychodzą osobno. - Sparsować treść JSON i wyciągnąć payload, zanim cokolwiek trafi do weryfikatora JWS. - Zweryfikować podpis algorytmem z parametru alg nagłówka przed odczytem oświadczenia events. - Uczynić handlery idempotentnymi i uzgadniać je z własnymi rekordami. Apple nie dokumentuje żadnych gwarancji dostarczenia.

Dla zespołów iOS: - consent-revoked unieważnia poświadczenia. Traktować jako zdarzenie uwierzytelniania kończące sesję, nie jako aktualizację preferencji. - Aplikacje natywne nie dostają wywołania zwrotnego po stronie klienta przy usunięciu Apple Account. Bez punktu końcowego nikt się o tym nie dowie.

Dla wahających się, czy w ogóle się tym zajmować: - Dla deweloperów z siedzibą w Korei punkt końcowy jest obowiązkowy od stycznia 2026, a stojące za tym uzasadnienie łatwo uogólnić.

FAQ

Ile jest typów powiadomień?

Cztery: email-enabled, email-disabled, consent-revoked oraz account-deleted.1 Ogłoszenie w aktualnościach Apple dla deweloperów opisuje trzy, scalając oba zdarzenia pocztowe w jeden punkt o preferencjach przekazywania poczty.3

Poświadczenia użytkownika stają się nieważne.1 Należy potraktować to jak cofnięte uprawnienie OAuth: zakończyć sesję i skierować użytkownika do ponownego uwierzytelnienia, zamiast zaktualizować preferencję i działać dalej.

Czy potrzebuję punktu końcowego, jeśli wydaję wyłącznie natywną aplikację na iOS?

Apple stwierdza, że w przypadku aplikacji natywnych system nie wysyła wywołania zwrotnego po stronie klienta przy trwałym usunięciu Apple Account.1 Bez punktu końcowego serwer–serwer nie istnieje mechanizm, który by o tym poinformował.

Co powinien zwracać mój punkt końcowy i co, jeśli będzie niedostępny?

Apple nie dokumentuje ani oczekiwań co do kodów statusu, ani zachowania przy ponawianiu, ani tego, czy zdarzają się podwójne dostawy. Warto projektować pod dostarczenie co najmniej raz — lub ewentualnie co najwyżej raz — uczynić handlery idempotentnymi i uzgadniać stan z własnymi rekordami, zamiast zakładać, że każde powiadomienie dotarło.

Czy jeden punkt końcowy może obsługiwać wiele aplikacji?

Dokumentacja Apple mówi, że tego samego adresu URL można używać dla wielu zespołów deweloperskich i wielu aplikacji.1 Rejestracja to jeden adres URL na grupę aplikacji Sign in with Apple i klucz, na podstawowym App ID.2 Wspólna usługa działa, pod warunkiem że handler potrafi ustalić, której aplikacji dotyczy każde powiadomienie.

Źródła


  1. Apple, “Processing changes for Sign in with Apple accounts.” Źródło dla czterech typów zdarzeń (email-enabled, email-disabled, consent-revoked, account-deleted), formatu ładunku JWS oraz zalecenia weryfikacji przy użyciu parametru alg z nagłówka, opakowania {"payload": "<JWS>"}, stwierdzenia, że cofnięcie zgody czyni poświadczenia nieważnymi, uwagi, że aplikacje natywne nie otrzymują wywołania zwrotnego po stronie klienta przy usunięciu konta, wymogu TLS 1.2 po stronie serwera oraz dopuszczenia jednego adresu URL dla wielu zespołów i aplikacji. Pobrano 2026-08-02. 

  2. Apple, “Enabling server-to-server notifications.” Źródło dla ścieżki rejestracji przez Certificates, Identifiers & Profiles, reguły jednego adresu URL na grupę aplikacji i klucz, ograniczenia do podstawowego App ID, wymogu bezwzględnego URI oraz wymogu TLS 1.2. Pobrano 2026-08-02. 

  3. Apple Developer News, “New requirement for apps using Sign in with Apple for account creation,” 9 października 2025. Źródło dla koreańskiego wymogu obowiązującego od 1 stycznia 2026 oraz dla trzypunktowego podsumowania tego, co odbiera punkt końcowy. 

Powiązane artykuły

Bomba forkowa nas uratowała

Napastnik stojący za atakiem na LiteLLM popełnił jeden błąd implementacyjny. To właśnie ten błąd sprawił, że 47 000 inst…

6 min czytania

Repozytorium nie powinno głosować nad swoim własnym zaufaniem

Dwie luki CVE obchodzące dialog zaufania Claude Code w 37 dni ujawniają błąd kolejności: nie czytaj żadnego bajtu, zanim…

10 min czytania

Czego odmawiam pisać

Głos klastra blogowego rodzi się z tego, czego odmawia publikować. Odmowy kategorialne, wzorcowe i interesujące — każda …

9 min czytania