Skip to content

feat(auth): coordinateur d'auth persisté pour tous les services - #900

Merged
ecnivtwelve merged 11 commits into
devfrom
refactor/auth-coordinator
Oct 28, 2026
Merged

ecnivtwelve merged 11 commits into
devfrom
refactor/auth-coordinator

Conversation

@tryon-dev

@tryon-dev tryon-dev commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Contribution

Règles de contribution

Caution

Afin de garantir une application stable et pérenne dans le temps, nous t'invitons à vérifier que tu as bien respecté les règles de contribution. Sans cela, ta Pull Request ne pourra pas être examinée.

  • Cette Pull Request porte sur une seule fonctionnalité ou un seul correctif.
  • Cette Pull Request n'est pas faite essentiellement avec de l'IA.
  • Pour tout changement majeur, j’ai créé une issue afin d’échanger avec les mainteneurs de Papillon sur la meilleure façon de l’intégrer.
  • Ma Pull Request respecte les conventions Conventional Commits et Conventional Branch ainsi que les conventions de codage de l'application.
  • J’ai testé mes modifications sur iOS et Android, et l’application fonctionne correctement.
  • J’emploie un langage informel, clair et concis dans mes messages.
  • J’ai documenté mes changements de manière appropriée, soit dans la description de la Pull Request, soit dans le GitBook.
  • J’ai ajouté les traductions nécessaires dans au moins un fichier de langue.

Résumé des changements

Note

Chaque renouvellement de session passe par un coordinateur qui écrit son étape sur disque : jeton envoyé, en attente du nouveau, reçu, prêt ou en échec. Le nouveau jeton est sauvé dès que le serveur le donne. Une coupure au mauvais moment ne déconnecte plus. Remplace #892.

Situation Avant Maintenant
ParametresUtilisateur échoue après Authentification (PRONOTE) Nouveau jeton perdu, déconnexion Jeton sauvé dès onTokenIssued, le login suivant passe
GetUserInfo échoue (Skolengo) Refresh token tourné perdu Sauvé avant GetUserInfo
Deux refresh EcoleDirecte proches Même jeton envoyé deux fois Un seul login, avec le jeton du store
App tuée en plein renouvellement Aucune trace Étape sur disque. Un jeton refusé ensuite donne credential_lost et un message dédié
Tâche de fond et app en même temps Deux logins avec le même jeton Bail persisté de 15 s, rafraîchi pendant le login
Message de maintenance PRONOTE « Déconnecté » « Service indisponible », jeton gardé
Session expirée pendant un chargement Erreur Relogin une fois, requête rejouée
IP suspendue, erreurs réseau, jeton refusé Suspension en mémoire, le reste réessayé à chaque appel pause de 5 min, backoff de 30 s à 15 min, un seul essai. Valable pour le lancement en cours : chaque ouverture réessaie
Messages d'erreur Un seul, en dur Un par raison, en français et en anglais

Ce qui change

  1. services/auth/ : runAuthRefresh déduplique, pose le bail, écrit chaque étape et classe l'échec. Les règles de réessai sont des fonctions pures (policy.ts).
  2. stores/authState/ : MMKV chiffré (clé statique). Seule une empreinte courte des identifiants est stockée.
  3. Patch pawnote : 3e paramètre hooks sur loginToken, même changement que feat(login): add progress hooks to loginToken BlocksHub/Pawnote-LTS#4. À retirer à sa release.
  4. Les 9 services passent par le coordinateur. Chacun garde le classement de ses propres erreurs.
  5. PRONOTE : les 17 méthodes passent par withSession.
  6. AccountManager : l'erreur affichée vient de la raison persistée. Le tirer-pour-rafraîchir lève un backoff.
  7. Accueil : un message par raison. Devmode : section « État d'auth », toucher une ligne la remet à zéro.

Tests

  • tsc : aucune nouvelle erreur, 18 en moins.
  • Simulateur iOS 27 : login PRONOTE réel → ready. Relance de l'app → nouveau login avec le jeton sauvé, ready. Fichier auth-state-storage vérifié chiffré (aucun texte en clair, même format que account-storage).
  • Pas encore testé : Android, coupure réelle entre Authentification et ParametresUtilisateur.

Ajoute un 3e paramètre optionnel hooks à loginToken (onCredentialSent,
onAuthenticationSent, onTokenIssued), appelé entre les requêtes.
onTokenIssued livre le nouveau jeton avant ParametresUtilisateur, qui
pouvait échouer et le perdre.

Même changement que BlocksHub/Pawnote-LTS#4, à retirer dès sa release.
runAuthRefresh enveloppe le login d'un service : il déduplique les appels,
pose un bail persisté de 15 s rafraîchi pendant le login, écrit chaque
étape (starting, credential_sent, awaiting_new_credential,
credential_received, ready, failed) et classe l'échec.

L'état vit dans un MMKV chiffré (clé statique), avec une empreinte courte
des identifiants, jamais les identifiants eux-mêmes.

Les règles de réessai sont des fonctions pures (policy.ts) : backoff réseau
de 30 s à 15 min, 30 min après une suspension d'IP, un seul essai pour un
jeton refusé. Elles ne valent que pour le lancement en cours.
Le login sauve le nouveau jeton dès onTokenIssued, avant
ParametresUtilisateur. Le classement des erreurs pawnote vit dans
services/pronote/errors.ts : un message de maintenance (AuthenticateError)
devient service_error et le jeton est gardé.

Les 17 méthodes passent par withSession : une session expirée est
renouvelée puis la requête rejouée une fois, une suspension d'IP bloque
toute requête. isTokenValid exige aussi l'état ready : une session
expirée, en échec ou remise à zéro n'est plus réutilisée. La
déduplication du plugin disparaît, le coordinateur s'en charge.
EcoleDirecte, Skolengo, Multi, AppScho, Izly, TurboSelf, Alise et ARD
lisent leurs identifiants dans le store et laissent le coordinateur
écrire le nouveau jeton.

- Skolengo sépare OIDCRefresh et GetUserInfo : le refresh token tourné
  est sauvé avant que GetUserInfo puisse échouer.
- EcoleDirecte part du jeton du store, pas de la copie de l'appelant.
- Skolengo et Multi lèvent CredentialsMissingError sans refresh token,
  au lieu d'envoyer une requête avec undefined.
- ARD réessaie dans une boucle : un appel récursif rejoindrait sa
  propre promesse.
- L'erreur affichée porte la raison persistée (credential_lost,
  account_disabled…), qui sait qu'une rotation a été coupée.
- Une maintenance PRONOTE ou une session expirée ne déconnectent plus :
  seules les raisons qui demandent une action de l'utilisateur lèvent
  AuthenticationError.
- initializeAccountManager accepte force, qui lève un backoff ou un
  blocage. Un appel forcé pendant une synchro en cours attend sa fin
  puis relance, au lieu de la rejoindre.
- Les états des services supprimés sont retirés.
L'alerte de déconnexion explique ce qui s'est passé : jeton perdu pendant
un renouvellement, compte désactivé, vérification de sécurité ou session
expirée. Une suspension d'IP a son propre message, et l'indisponibilité
nomme le bon service au lieu de toujours dire PRONOTE.

Le tirer-pour-rafraîchir passe force, qui lève un backoff en cours.
Textes ajoutés en français et en anglais.
Section « État d'auth » : étape, dernière erreur, prochain essai et
jeton peut-être consommé. Toucher une ligne efface l'état et relance une
connexion forcée. Les lignes de détail ont leur propre composant, pour
que leur horloge ne rafraîchisse pas tout l'écran.
tryon-dev and others added 4 commits October 27, 2026 15:21
PRONOTE lève une suspension d'IP en quelques minutes, pas en 30. La pause
passe à 5 minutes, et le message invite à réessayer quelques minutes
plus tard en tirant la page vers le bas.
- timeout de 60 s sur le fetcher PRONOTE : une requête sans réponse ne gèle plus la file de la session ni le bail
- le bail n'est plus renouvelé après 2 min de login
- UnreachableError (bug de décodage) n'est plus classée comme erreur réseau
- commentaire du store d'auth corrigé sur la portée du chiffrement
Un seul runtime JS tourne à la fois : un réveil en arrière-plan par
PapillonKit et l'ouverture de l'app partagent le même process, la
déduplication en mémoire suffit. Le bail faisait attendre jusqu'à 15 s
au lancement suivant quand iOS tuait un réveil en plein login.
@ecnivtwelve
ecnivtwelve merged commit 4b8e54c into dev Oct 28, 2026
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.

2 participants