0023-fediverse-ui-user-auth.md 8072 octets

RFC 0023 — Fediverse UI : parcours utilisateur d'authentification fédérée (Slice B)

  • Auteur : EBO (rustwarden)
  • Date : 2026-05-10
  • Statut : Accepted (auto-revue mainteneur unique 2026-05-10, score 26/35)
  • Apps consommatrices concernées : toute app rustwarden-ui qui veut activer la connexion AP-native (utilisateur d'instance fédérée se connecte sur l'instance locale).
  • Plan associé : Plan UI HTML §7 Slice B.
  • Dépendances : RFC 0006 (FediverseAuthService service-layer), RFC 0013 (5 routes JSON /api/v1/auth/login/fediverse/*), RFC 0021 (toggle fediverse_enabled + trust list).

Summary

Livrer les 4 écrans HTML du parcours d'authentification AP-native, qui déclenchent et accompagnent le round-trip avec l'IdP fédéré via les routes JSON livrées par RFC 0013 :

  1. fediverse/error_unauthorized.html — page d'erreur user-friendly avec 4 variantes (timeout / rejected / untrusted / invalid_signature). C'est la cible du redirect quand l'auth échoue à toute étape.
  2. Extension login.html — ajoute un champ « Se connecter avec une identité ActivityPub » (handle @user@domain) quand fediverse_enabled = true. Submit POST UI → handler initiate → redirect vers /login/fediverse/waiting/{challenge_id}.
  3. fediverse/login_waiting.html — page d'attente sur l'instance demandeuse. Polling SSE de /api/v1/auth/login/fediverse/status/{challenge_id} (RFC 0013) avec spinner + skeleton ; redirige vers /home (consume) ou /login/fediverse/error?reason=… selon issue.
  4. fediverse/consent_screen.html — sur l'IdP, rendu quand l'utilisateur (déjà loggué localement) reçoit une demande d'auth fédérée d'une autre instance. Affiche domaine demandeur, scope, boutons Approve / Reject.

Motivation

Sans ces pages, le parcours d'auth fédéré est uniquement pilotable depuis une SPA ou un client API. Plan UI Plan §7 identifie ce parcours comme critique pour l'adoption : c'est le premier moment où un utilisateur externe découvre Rustwarden via une instance fédérée.

Non-goals

  • Pas de WebAuthn / passkey intégré au flow — restera OAuth/OIDC classique pour les comptes locaux.
  • Pas de bulk-approve sur la page consent — un challenge à la fois.
  • Pas de QR-code mobile pour scanner un handle — différé.
  • Pas d'UI de gestion historique des challenges sur l'IdP (audit log) — couvert par Slice C admin/sessions_remote.html.

API proposée

Routes UI HTML (rustwarden-ui)

MéthodePathHandlerAuth
GET/login/fediverselogin_fediverse::formpublic
POST/login/fediverselogin_fediverse::initiatepublic + CSRF
GET/login/fediverse/waiting/{challenge_id}login_fediverse::waitingpublic
GET/login/fediverse/sse/{challenge_id}login_fediverse::sse_statuspublic (challenge bound)
GET/login/fediverse/errorlogin_fediverse::errorpublic
GET/fediverse/consent/{challenge_id}consent::formlocal user logged-in
POST/fediverse/consent/{challenge_id}consent::submitlocal user + CSRF

Les handlers UI délèguent systématiquement à FediverseAuthService (RFC 0006) pour la logique métier ; ils ne touchent pas la state machine du challenge.

Le mode dégradé silencieux (Plan UI §3 P9) s'applique : toutes ces routes retournent 404 quand fediverse_enabled = false, sauf /login qui reste accessible (le champ AP est juste masqué via {% if fediverse_enabled %}).

Templates Askama (nouveaux)

templates/admin/fediverse/  ← Slice A
templates/fediverse/
├── error_unauthorized.html      # iter. 1
├── login_waiting.html           # iter. 3
├── consent_screen.html          # iter. 4
templates/login.html              # extension iter. 2

i18n (nouvelles clés fediverse.auth.*)

  • error.title + error.<reason>.title + error.<reason>.body × 4 reasons
  • login.field_label + login.placeholder + login.submit
  • waiting.title + waiting.subtitle + waiting.cancel
  • consent.title + consent.body (templated {domain}) + consent.scope_<key> + consent.approve + consent.reject

Compatibilité

Aucun impact sur les apps qui n'activent pas experimental-fediverse. Les routes UI sont gated par feature, exactement comme Slice A.

L'extension de login.html est conditionnelle : sans fediverse_enabled = true, le markup AP reste invisible — pas de régression UX pour les apps qui n'utilisent pas la fédération.

Alternatives

  1. Ne rien faire / SPA-only — rejeté : l'app de référence Rustwarden ship une UI HTML ; un parcours critique non couvert HTML brise l'expérience cohérente.
  2. Polling AJAX au lieu de SSE — rejeté : SSE est exactement le shape attendu (one-way push depuis le serveur), conserve la connexion ouverte sans re-poll, et axum::response::sse est intégré.
  3. WebSocket — rejeté : sur-ingénierie, pas de besoin bidirectionnel.
  4. Tout en une seule iter — rejeté : on a vu sur Slice A que 4 templates + handlers + tests = 4-5 turns. On déroule en 4 itérations livrables individuellement.

Stratégie d'itérations

IterLivrablePriorité
1error_unauthorized.html + handler + 4 variantes + i18nindépendant, déblocant pour les autres iter
2Extension login.html + handler initiateparcours entrant fonctionnel jusqu'à waiting
3login_waiting.html + SSE pollingferme la boucle côté demandeur
4consent_screen.html + handler approve/rejectcôté IdP, ferme E2E
5Tests E2E deux instances local + a11y Lighthouseacceptation

Chaque iter passe les 5 quality gates (build / test / clippy / doc / migration N/A) avant la suivante.

Testing plan

  • iter. 1 : test handler error rendu sur ?reason=timeout|rejected|untrusted|invalid_signature → 200 + i18n attendue.
  • iter. 2 : test handler initiate valide le handle (regex @user@domain), appelle FediverseAuthService::initiate, retourne redirect 303.
  • iter. 3 : test SSE handler émet bien event: status quand le challenge passe à Accepted / Rejected ; aucun test client (le navigateur s'en charge).
  • iter. 4 : test handler consent — POST approve appelle record_decision(Accepted), POST reject → record_decision(Rejected), mauvais CSRF → 403.
  • iter. 5 : harness deux binaires Rustwarden (docker-compose.fediverse-test.yml) avec debug_local_federation=true → playwright/curl scripté login E2E.

Migration guide

Aucune. Pure addition de routes + templates. Apps qui activent experimental-fediverse doivent simplement reconstruire leur binaire et fournir le pack i18n.

Open questions

  • Q1 : le cancel sur login_waiting.html doit-il appeler une route DELETE pour cleaner le challenge en DB, ou laisser timeout naturel (TTL 5 min, RFC 0006) ? Réponse provisoire : TTL suffit ; l'utilisateur ferme l'onglet, le worker nettoie. DELETE explicite peut s'ajouter en RFC future.
  • Q2 : dois-je restreindre ?reason= à un enum strict ou tolérer du texte libre ? Réponse : enum strict (whitelist 4 valeurs), tout le reste tombe sur la variante unknown qui affiche un message générique.

Score (26/35)

CritèreNoteDétail
Généralité4/5Réutilisable par toute app rustwarden-ui.
Stabilité API5/5Pures additions de routes UI, aucun breaking.
Performance4/5SSE + polling DB → mesurable, mais bornée par TTL 5 min.
Sécurité4/5Le parcours touche l'auth, mais SignatureVerifier (RFC 0015) reste autoritatif sur l'IdP. CSRF sur les forms.
Complexité2/54 templates + 5 handlers + SSE = ~800 LoC sur 4 iters.
Maintenance4/5Pattern existant Slice A à recopier.
Documentation3/5Cette RFC + rustdoc + 4 entrées CHANGELOG.