← Tous les articles

Sign in with Apple envoie quatre notifications, pas trois

L’annonce publiée par Apple à destination des développeurs au sujet des notifications de serveur à serveur de Sign in with Apple énumère trois éléments que votre endpoint recevra : les changements de préférence de redirection d’e-mail, les suppressions de compte dans votre app et les suppressions définitives d’un compte Apple3. La documentation API, elle, définit quatre types d’événements distincts1.

Une implémentation écrite à partir de l’annonce traite trois branches et laisse tomber en silence un quatrième événement. L’annonce fond email-enabled et email-disabled en une seule puce consacrée aux préférences de redirection. Or ces deux notifications arrivent séparément, avec des valeurs de type distinctes, et un code qui aiguille sur le type sans cas par défaut ignore purement et simplement celle que l’auteur a oubliée.

Deux des quatre événements signifient par ailleurs davantage que ne le laisse entendre leur nom, et l’un d’eux modifie l’état d’authentification de votre app.

En bref

Sign in with Apple délivre quatre types de notifications de serveur à serveur : email-enabled, email-disabled, consent-revoked et account-deleted1. La charge utile arrive sous forme de JWS encapsulé dans un objet JSON sous une clé payload, signé par la clé privée d’Apple, et doit être validée à l’aide de l’algorithme nommé dans le paramètre alg de l’en-tête avant que vous n’agissiez dessus1. consent-revoked invalide les identifiants de l’utilisateur : c’est donc un événement d’authentification, et non un changement de préférence. Les apps natives ne reçoivent aucun rappel côté client lorsqu’un compte Apple est supprimé, si bien que la notification serveur constitue le seul signal1. Apple ne documente ni les tentatives de renvoi, ni la sémantique de livraison.

Les quatre types d’événements

Chaque notification porte une valeur type à l’intérieur du claim events1.

type Ce qui s’est passé
email-enabled L’utilisateur a activé la redirection des e-mails vers son adresse personnelle via Hide My Email
email-disabled L’utilisateur a désactivé la redirection des e-mails
consent-revoked L’utilisateur a révoqué son consentement pour votre app, et ses identifiants sont devenus invalides
account-deleted L’utilisateur a demandé la suppression définitive de son compte Apple

Les deux événements liés à l’e-mail sont précisément ceux que l’annonce fusionne. Ils comptent pourtant indépendamment l’un de l’autre : email-disabled signifie que les messages envoyés à l’adresse relais cessent d’atteindre l’utilisateur, et email-enabled qu’ils recommencent à lui parvenir. Les traiter comme un unique événement « préférences modifiées » vous oblige à aller interroger l’état courant, alors que la notification vous l’avait déjà indiqué.

Deux événements qui ne sont pas ce qu’ils paraissent

consent-revoked est un événement d’authentification. Apple le décrit ainsi : l’utilisateur « révoque le consentement donné à votre app d’utiliser son compte Apple, et ses identifiants deviennent invalides »1. Pas obsolètes, pas en attente d’expiration. Invalides.

Une app qui journalise cet événement à côté des événements d’e-mail et met à jour une ligne de préférences continuera d’afficher une interface d’utilisateur connecté, adossée à des identifiants qui n’authentifient plus rien. L’utilisateur voit son compte jusqu’au premier échec de rafraîchissement du jeton, puis voit quelque chose de pire encore. Le traitement correct consiste à mettre fin à la session et à rediriger vers une nouvelle authentification, par le même chemin de code que pour une autorisation OAuth révoquée.

account-deleted est peut-être le seul avis que vous recevrez. Apple précise que lorsqu’un utilisateur supprime définitivement son compte Apple, Sign in with Apple invalide tous les jetons de l’utilisateur et désactive la redirection des e-mails pour toutes les apps associées, et que, pour les apps natives, le système n’envoie aucun rappel côté client1.

Cette phrase est le meilleur argument qui soit en faveur de la mise en place d’un endpoint. Une équipe exclusivement iOS, sans endpoint de serveur à serveur, ne dispose d’aucun moyen d’apprendre que le compte a disparu. L’enregistrement persiste, l’adresse relais cesse de fonctionner, et l’obligation de suppression qui vous incombe reste lettre morte, faute d’avoir été informé qu’il y avait quelque chose à supprimer.

Enregistrer l’endpoint

La configuration se fait dans Certificates, Identifiers & Profiles : sélectionnez Identifiers, choisissez votre App ID, activez le service Sign in with Apple, cliquez sur Configure et indiquez l’URL de l’endpoint2.

Les contraintes méritent d’être lues avant de concevoir quoi que ce soit autour2.

  • Une seule URL par regroupement d’apps Sign in with Apple et par clé. Pas une par app.
  • Enregistrable uniquement sur un App ID principal.
  • L’URL doit être un URI absolu comportant schéma, hôte et chemin : https://example.com/path/to/endpoint
  • TLS 1.2 ou une version ultérieure est requis pour recevoir les notifications.

La documentation API d’Apple ajoute que vous pouvez utiliser la même URL pour plusieurs équipes de développement et plusieurs apps1. Rapprochée de la règle d’une URL par regroupement, la lecture raisonnable est qu’un service unique peut tout recevoir, chaque regroupement enregistrant son propre pointeur vers lui. Apple ne détaille pas cette interaction : considérez donc un endpoint partagé comme praticable plutôt que comme béni, et assurez-vous que votre gestionnaire sait déterminer quelle app est concernée par une notification avant de vous y fier.

Le plancher TLS 1.2 s’inscrit dans une évolution plus large. OS 27 a commencé à imposer des exigences TLS plus strictes au trafic de gestion, avec le même minimum 1.2, auquel s’ajoutent des suites de chiffrement et des certificats conformes à ATS. Un endpoint qui satisfait aujourd’hui à l’exigence d’Apple n’est pas automatiquement conforme à ATS, et le mouvement d’ensemble va vers plus de sévérité, pas moins.

Lire la charge utile

La livraison arrive sous forme de requête HTTP POST dont le corps est un objet JSON, avec le jeton signé à l’intérieur1 :

{
    "payload": "<SERVER_TO_SERVER_NOTIFICATION_JWS>"
}

Le JWS est encapsulé, pas brut. Analysez le JSON, extrayez payload, puis validez. Une implémentation qui transmet le corps entier de la requête à un vérificateur JWS échoue dès la première notification, et l’échec se présente comme un jeton mal formé plutôt que comme une erreur d’encapsulation, ce qui vous envoie chercher au mauvais endroit.

La validation précède l’interprétation. La charge utile est signée cryptographiquement par la clé privée d’Apple au format JSON Web Signature, et la consigne d’Apple est d’examiner le JWS et d’utiliser l’algorithme spécifié dans le paramètre alg de l’en-tête pour valider la signature1. Ce n’est qu’une fois la signature vérifiée que vous lisez le claim events et que vous aiguillez sur type.

Deux habitudes à conserver de la pratique générale du JWS : ne jamais faire confiance à une valeur alg qui permettrait à un appelant de rétrograder la vérification, et confirmer que l’émetteur et l’audience du jeton correspondent à ce que vous attendez, plutôt que d’accepter n’importe quel jeton bien formé signé par Apple.

La structure décodée

Une fois validée, une notification consent-revoked décodée se présente ainsi1 :

{
    "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 porte les mêmes champs avec un type différent. Les événements d’e-mail en ajoutent deux autres, email et is_private_email.

Trois détails de cette structure vous coûteront du temps si vous les découvrez par surprise.

events est un objet, pas un tableau. Le nom est au pluriel et la valeur est un événement unique. Un code écrit d’après le nom plutôt que d’après la structure itère sur un dictionnaire et récupère des clés.

is_private_email est une chaîne de caractères. Les exemples d’Apple montrent "true" entre guillemets, et non le booléen JSON true. Un décodeur strict qui le mappe vers un Bool échoue, et un décodeur permissif qui considère toute chaîne non vide comme vraie obtient la bonne réponse pour de mauvaises raisons — puis se trompe sur "false".

sub est l’identifiant stable de l’utilisateur, la même valeur que celle reçue à la connexion, et c’est par lui que vous retrouvez le compte concerné par la notification. aud est votre identifiant client, celui qui permet à un endpoint partagé d’aiguiller les notifications de plusieurs apps.

Une remarque sur les exemples fournis par Apple : les deux charges utiles d’e-mail sont dépourvues de virgule entre "is_private_email": "true" et "event_time". Copiez l’un ou l’autre de ces blocs dans un analyseur JSON et il rejettera le document. La structure est juste, la ponctuation ne l’est pas, et celui qui colle cela dans une fixture de test perd dix minutes sur une erreur de syntaxe qui n’est pas la sienne.

La terminologie d’Apple varie également. Le texte décrit une charge utile au format JSON Web Signature, tandis que l’exemple d’encapsulation nomme la valeur SERVER_TO_SERVER_NOTIFICATION_JWT1. C’est le même objet : un JWT signé est un JWS dont la charge utile est du JSON. Bon à savoir lorsque vous fouillez leur documentation et tombez sur les deux termes.

Un gestionnaire de bout en bout

La forme d’un gestionnaire correct découle des contraintes ci-dessus. En Python, avec 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

Quelques-unes de ces lignes sont porteuses.

Fixez l’algorithme. Le JWKS en production d’Apple publie actuellement trois clés RSA, toutes en RS256, avec use: sig et des valeurs kid distinctes. Passer algorithms=["RS256"] au lieu de lire alg dans le jeton ferme la voie de rétrogradation classique où un attaquant fournit un jeton annonçant alg: none.

Résolvez par kid, et non en prenant la première clé. Trois clés sont actives simultanément : c’est à cela que ressemble une rotation de clés vue de l’extérieur. Un gestionnaire qui attrape keys[0] fonctionne jusqu’à la prochaine rotation d’Apple, puis échoue pour une partie des jetons — un cauchemar à déboguer.

Vérifiez aud et iss. Sans eux, vous acceptez n’importe quel jeton Apple valablement signé, y compris un jeton frappé pour une autre app.

Gardez une branche par défaut. Un cinquième type d’événement disparaîtrait sinon sans laisser de trace. C’est exactement ainsi qu’une implémentation écrite d’après l’annonce en trois points d’Apple laisse tomber email-enabled ou email-disabled aujourd’hui.

Ce qu’Apple ne documente pas

Ni la documentation API ni la page d’aide sur les comptes ne disent quoi que ce soit des garanties de livraison. Y chercher les termes retry, redelivery, acknowledgment, status codes, timeouts et idempotency ne donne rien.

Les questions suivantes restent donc sans réponse dans la documentation d’Apple à l’heure où ces lignes sont écrites :

  • Une livraison échouée est-elle réessayée, et combien de fois
  • Sur quelle fenêtre ces tentatives se répartissent, le cas échéant
  • Quel code de statut votre endpoint doit renvoyer pour signaler le succès
  • Si une même notification peut arriver deux fois

Cette absence a une conséquence de conception — et ici je raisonne au-delà de ce qu’Apple énonce, plutôt que de le rapporter. Un endpoint dont la sémantique de livraison n’est pas spécifiée ne peut pas être traité comme un flux d’événements faisant autorité. La posture défensive consiste à traiter chaque notification comme l’indice qu’un changement a eu lieu, et à réconcilier avec vos propres enregistrements plutôt qu’à appliquer l’événement aveuglément. Rendez vos gestionnaires idempotents, puisque vous ne pouvez pas exclure les doublons. Ne construisez pas un flux dont la justesse dépend de la réception de chaque notification, puisque vous ne pouvez pas confirmer les avoir toutes reçues.

Si votre endpoint reste indisponible une heure, la documentation ne vous dit pas si vous avez perdu une heure de suppressions de compte ou si elles sont mises en file d’attente quelque part. Concevez comme si vous les aviez perdues.

Aucun moyen documenté de le tester, non plus

Chercher sandbox, simulate et trigger dans les deux pages ne donne rien. Apple ne documente aucun mécanisme permettant de déclencher une notification à la demande.

Il en résulte une boucle inconfortable. Les événements qui comptent le plus, consent-revoked et account-deleted, sont produits par un utilisateur qui révoque l’accès à votre app ou supprime son compte Apple. Vérifier votre gestionnaire contre un vrai account-deleted suppose que quelqu’un supprime un compte Apple. Ce n’est pas un test que l’on relance deux fois.

Le substitut praticable consiste à scinder le problème. Construisez vous-même les charges utiles décodées à partir des structures ci-dessus, et testez unitairement l’aiguillage, l’idempotence et la logique de réconciliation face à elles. Séparément, testez le transport et le chemin de signature avec une vraie notification que vous pouvez réellement produire : révoquer le consentement pour un compte Apple de test est réversible, là où en supprimer un ne l’est pas, et cela exerce de bout en bout l’analyse de l’encapsulation, la recherche du kid et la vérification de la signature.

Quoi que vous fassiez, confirmez que l’endpoint est joignable et répond rapidement avant de l’enregistrer. Un endpoint enregistré et jamais vérifié, c’est ainsi qu’une équipe découvre, des mois plus tard, que toutes les notifications émises depuis le lancement partaient vers une URL protégée par un certificat expiré.

Une exigence déjà en vigueur

La raison pour laquelle le sujet a fait surface dans l’actualité destinée aux développeurs : depuis le 1er janvier 2026, les développeurs établis en République de Corée doivent fournir un endpoint de notifications de serveur à serveur lorsqu’ils enregistrent un nouveau Services ID ou en mettent à jour un existant, afin d’associer un site web à une app à l’aide de Sign in with Apple3. Apple l’a annoncé le 9 octobre 2025.

L’exigence est étroite, et elle est en vigueur depuis des mois : ce n’est pas une échéance à préparer. Son intérêt est directionnel. Apple a commencé à rendre l’endpoint obligatoire dans au moins une juridiction, pour des motifs qui se généralisent : donner aux personnes le contrôle des données personnelles qu’elles ont partagées, et faire en sorte que la suppression d’un compte se propage réellement. Rien dans cette logique n’est propre à la Corée.

Si vous construisez de toute façon cet endpoint, construisez-le avant qu’un régulateur n’en fasse votre échéance.

À retenir

Pour les ingénieurs backend : - Traitez quatre types, pas trois. email-enabled et email-disabled arrivent séparément. - Analysez le corps JSON et extrayez payload avant de confier quoi que ce soit à un vérificateur JWS. - Validez la signature à l’aide de l’algorithme indiqué dans le paramètre alg de l’en-tête avant de lire le claim events. - Rendez vos gestionnaires idempotents et réconciliez avec vos propres enregistrements. Apple ne documente aucune garantie de livraison.

Pour les équipes iOS : - consent-revoked invalide les identifiants. Traitez-le comme un événement d’authentification qui met fin à la session, et non comme une mise à jour de préférence. - Les apps natives ne reçoivent aucun rappel côté client lors de la suppression d’un compte Apple. Sans endpoint, vous n’apprendrez jamais que c’est arrivé.

Pour ceux qui se demandent si cela en vaut la peine : - L’endpoint est déjà obligatoire pour les développeurs établis en Corée depuis janvier 2026, et le raisonnement se généralise.

FAQ

Combien y a-t-il de types de notifications ?

Quatre : email-enabled, email-disabled, consent-revoked et account-deleted1. L’annonce d’Apple dans l’actualité destinée aux développeurs en décrit trois, en fusionnant les deux événements d’e-mail en une seule puce sur les préférences de redirection3.

Les identifiants de l’utilisateur deviennent invalides1. Traitez-le comme vous traiteriez une autorisation OAuth révoquée : mettez fin à la session et renvoyez l’utilisateur vers une nouvelle authentification, plutôt que de mettre à jour une préférence et de poursuivre.

Ai-je besoin d’un endpoint si je ne publie qu’une app iOS native ?

Apple indique que, pour les apps natives, le système n’envoie aucun rappel côté client lorsqu’un compte Apple est supprimé définitivement1. Sans endpoint de serveur à serveur, aucun mécanisme ne vous en informe.

Que doit renvoyer mon endpoint, et que se passe-t-il s’il est indisponible ?

Apple ne documente ni les attentes en matière de code de statut, ni le comportement de renvoi, ni l’éventualité de livraisons en double. Concevez pour une livraison au moins une fois, voire au plus une fois, rendez vos gestionnaires idempotents et réconciliez avec vos propres enregistrements plutôt que de supposer que chaque notification est bien arrivée.

Un seul endpoint peut-il servir plusieurs apps ?

La documentation d’Apple indique que vous pouvez utiliser la même URL pour plusieurs équipes de développement et plusieurs apps1. L’enregistrement se fait à raison d’une URL par regroupement d’apps Sign in with Apple et par clé, sur un App ID principal2. Un service partagé fonctionne, à condition que votre gestionnaire sache déterminer quelle app est concernée par chaque notification.

Sources


  1. Apple, « Processing changes for Sign in with Apple accounts ». Source pour les quatre types d’événements (email-enabled, email-disabled, consent-revoked, account-deleted), le format de charge utile JWS et la consigne de valider à l’aide du paramètre alg de l’en-tête, l’encapsulation {"payload": "<JWS>"}, l’affirmation selon laquelle un consentement révoqué rend les identifiants invalides, la remarque selon laquelle les apps natives ne reçoivent aucun rappel côté client lors de la suppression d’un compte, l’exigence serveur TLS 1.2, et l’autorisation d’utiliser une seule URL pour plusieurs équipes et plusieurs apps. Consulté le 2 août 2026. 

  2. Apple, « Enabling server-to-server notifications ». Source pour le chemin d’enregistrement via Certificates, Identifiers & Profiles, la règle d’une URL par regroupement d’apps et par clé, la restriction à un App ID principal, l’exigence d’URI absolu et l’exigence TLS 1.2. Consulté le 2 août 2026. 

  3. Apple Developer News, « New requirement for apps using Sign in with Apple for account creation », 9 octobre 2025. Source pour l’exigence coréenne effective au 1er janvier 2026, et pour le résumé en trois points de ce que reçoit l’endpoint. 

Articles connexes

La fork bomb nous a sauvés

L'attaquant de LiteLLM a commis une seule erreur d'implémentation. C'est uniquement grâce à elle que 47 000 installation…

7 min de lecture

Le dépôt ne devrait pas pouvoir voter sur sa propre confiance

Deux CVE contournent le dialogue de confiance de Claude Code en 37 jours. Un invariant suffit : n'interpréter aucun octe…

12 min de lecture

Ce que je refuse d'écrire

La voix d'une série d'articles vient de ce qu'elle refuse de publier, pas de ce qu'elle publie : refus catégoriels, refu…

12 min de lecture