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-uiqui 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 (
FediverseAuthServiceservice-layer), RFC 0013 (5 routes JSON/api/v1/auth/login/fediverse/*), RFC 0021 (togglefediverse_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 :
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.- Extension
login.html— ajoute un champ « Se connecter avec une identité ActivityPub » (handle@user@domain) quandfediverse_enabled = true. Submit POST UI → handlerinitiate→ redirect vers/login/fediverse/waiting/{challenge_id}. 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.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éthode | Path | Handler | Auth |
|---|---|---|---|
| GET | /login/fediverse | login_fediverse::form | public |
| POST | /login/fediverse | login_fediverse::initiate | public + CSRF |
| GET | /login/fediverse/waiting/{challenge_id} | login_fediverse::waiting | public |
| GET | /login/fediverse/sse/{challenge_id} | login_fediverse::sse_status | public (challenge bound) |
| GET | /login/fediverse/error | login_fediverse::error | public |
| GET | /fediverse/consent/{challenge_id} | consent::form | local user logged-in |
| POST | /fediverse/consent/{challenge_id} | consent::submit | local 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 reasonslogin.field_label+login.placeholder+login.submitwaiting.title+waiting.subtitle+waiting.cancelconsent.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
- 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.
- 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::sseest intégré. - WebSocket — rejeté : sur-ingénierie, pas de besoin bidirectionnel.
- 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
| Iter | Livrable | Priorité |
|---|---|---|
| 1 | error_unauthorized.html + handler + 4 variantes + i18n | indépendant, déblocant pour les autres iter |
| 2 | Extension login.html + handler initiate | parcours entrant fonctionnel jusqu'à waiting |
| 3 | login_waiting.html + SSE polling | ferme la boucle côté demandeur |
| 4 | consent_screen.html + handler approve/reject | côté IdP, ferme E2E |
| 5 | Tests E2E deux instances local + a11y Lighthouse | acceptation |
Chaque iter passe les 5 quality gates (build / test / clippy / doc / migration N/A) avant la suivante.
Testing plan
- iter. 1 : test handler
errorrendu sur?reason=timeout|rejected|untrusted|invalid_signature→ 200 + i18n attendue. - iter. 2 : test handler
initiatevalide le handle (regex@user@domain), appelleFediverseAuthService::initiate, retourne redirect 303. - iter. 3 : test SSE handler émet bien
event: statusquand 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) avecdebug_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
cancelsurlogin_waiting.htmldoit-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 varianteunknownqui affiche un message générique.
Score (26/35)
| Critère | Note | Détail |
|---|---|---|
| Généralité | 4/5 | Réutilisable par toute app rustwarden-ui. |
| Stabilité API | 5/5 | Pures additions de routes UI, aucun breaking. |
| Performance | 4/5 | SSE + polling DB → mesurable, mais bornée par TTL 5 min. |
| Sécurité | 4/5 | Le parcours touche l'auth, mais SignatureVerifier (RFC 0015) reste autoritatif sur l'IdP. CSRF sur les forms. |
| Complexité | 2/5 | 4 templates + 5 handlers + SSE = ~800 LoC sur 4 iters. |
| Maintenance | 4/5 | Pattern existant Slice A à recopier. |
| Documentation | 3/5 | Cette RFC + rustdoc + 4 entrées CHANGELOG. |
GitRust