13 KiB
Audit intégral – Frontend (Flutter) & Backend (Quarkus)
Date : 4 février 2026
Périmètre : afterwork (Flutter), mic-after-work-server-impl-quarkus-main (Quarkus)
Références : bonnes pratiques Quarkus REST, Flutter clean architecture, REST/JWT, WebSocket, Kafka (recherches web et documentation officielle).
1. Résumé exécutif
| Domaine | État global | Points critiques |
|---|---|---|
| Sécurité API (auth/authz) | Critique | Aucune vérification JWT ; userId pris de l’URL/body sans preuve d’identité |
| Couches backend | Partiel | Resource accède parfois au repository ; validation incohérente (manuel vs Bean Validation) |
| Gestion d’erreurs backend | Partiel | Réponse d’erreur JSON construite à la main (risque d’injection) ; exceptions métier non gérées |
| Frontend – auth | Critique | Aucun en-tête Authorization sur les requêtes API |
| Frontend – architecture | Correct | data/domain/presentation présents ; pas de couche use-case systématique |
| WebSocket | Correct | Heartbeat + reconnexion présents ; pas de backoff exponentiel côté Flutter |
2. Backend (Quarkus) – Analyse détaillée
2.1 Architecture des couches
Recommandation (bonnes pratiques Quarkus) :
Resource (REST, DTO) → Service (métier) → Repository (persistance). La resource ne doit pas appeler le repository pour de la logique métier.
Constat :
- MessageResource (lignes 98–104, 168–174) : appelle directement
usersRepository.findById(userId)pour vérifier l’existence de l’utilisateur, au lieu de déléguer au service (ex.messageService.getUserConversations(userId)qui lèveraitUserNotFoundException). - MessageResource injecte
UsersRepositoryen plus deMessageService→ mélange des responsabilités et duplication de la règle “utilisateur doit exister”.
Recommandation : Déplacer la résolution/utilisateur dans MessageService et faire lever UserNotFoundException ; supprimer l’injection de UsersRepository dans MessageResource.
2.2 Validation des entrées
Recommandation : Utiliser Bean Validation (Hibernate Validator) sur les DTO avec @Valid sur les paramètres des endpoints. Éviter la validation manuelle dans la resource.
Constat :
- SendMessageRequestDTO : pas d’annotations
@NotNull,@NotBlank; validation manuelle viaisValid(). - MessageResource.sendMessage : pas de
@Valid; utiliserequest.isValid()et retourne 400 manuellement. - Autres ressources (UsersResource, EstablishmentResource, FriendshipResource, etc.) : utilisent correctement
@Validet DTO avec contraintes.
Recommandation : Ajouter sur SendMessageRequestDTO les annotations (@NotNull pour senderId, recipientId, @NotBlank pour content, etc.) et appeler l’endpoint avec @Valid SendMessageRequestDTO request. Supprimer isValid() et le bloc manuel 400.
2.3 Authentification et autorisation
Recommandation (OWASP / JWT) :
Chaque endpoint protégé doit valider un JWT (ou session) et dériver l’identité du token. Les paramètres comme userId dans l’URL ne doivent pas être la seule source de vérité : vérifier que le sujet du token correspond à la ressource demandée.
Constat :
- Aucun usage de
@RolesAllowed,@PermitAll, ni de filtre/filtre JWT dans le projet. - Les endpoints utilisent
userIden@PathParam(ex./notifications/user/{userId},/messages/conversations/{userId}) ou dans le body (ex.SendMessageRequestDTO.senderId) sans aucune preuve que l’appelant est cet utilisateur. - Le commentaire dans
NotificationResourceindique : “En production, le userId doit être dérivé du contexte d'authentification (JWT/session), pas de l'URL.” → non implémenté.
Impact : Un attaquant peut lire/modifier les données d’un autre utilisateur en devinant ou en énumérant des UUID.
Recommandation : Introduire l’authentification JWT (ex. quarkus-oidc ou filtre custom), extraire le userId (ou subject) du token, et pour chaque endpoint : soit utiliser ce userId comme source de vérité, soit vérifier que le userId en path/body est égal au sujet du token (pour les rôles appropriés).
2.4 Gestion globale des exceptions
Recommandation : Un seul point de sortie pour les erreurs (ExceptionMapper), réponses en JSON structuré (ex. {"error": "..."}) avec échappement correct. Gérer toutes les exceptions métier connues.
Constat :
- GlobalExceptionHandler : gère
BadRequestException,UserNotFoundException,EventNotFoundException,NotFoundException,UnauthorizedException,ServerException,RuntimeException, et cas par défaut. - FriendshipNotFoundException et EstablishmentHasDependenciesException ne sont pas gérées explicitement → elles tombent dans
RuntimeExceptionou “Unexpected error”, avec un message potentiellement générique ou une stack trace. - buildResponse (ligne 62–65) :
entity("{\"error\":\"" + message + "\"}")
Concaténation directe demessagedans le JSON. Simessagecontient"ou\, le JSON est mal formé et peut poser des risques (injection / parsing côté client). Il faut sérialiser le message en JSON (ex. via Jackson/JSON-B) au lieu de concaténer une chaîne.
Recommandation :
- Ajouter des branches pour
FriendshipNotFoundException(ex. 404) etEstablishmentHasDependenciesException(ex. 409 Conflict). - Remplacer la concaténation par un DTO d’erreur sérialisé (ex.
Map.of("error", message)ou classe dédiée) avec le moteur JSON du framework.
2.5 Ressources qui gèrent les erreurs en local
Recommandation : La resource ne doit pas faire de try/catch générique qui transforme tout en 500. Elle doit déléguer au service ; les exceptions métier doivent être mappées par le GlobalExceptionHandler.
Constat :
- MessageResource : plusieurs méthodes avec
try { ... } catch (Exception e) { return 500 ... }. Les exceptions métier (ex. utilisateur inexistant, conversation inexistante) ne sont pas levées sous forme d’exceptions typées ; elles sont noyées dans un message générique 500.
Recommandation : Faire lever par le service des exceptions métier (ex. UserNotFoundException, NotFoundException) et supprimer les try/catch larges dans la resource ; laisser le GlobalExceptionHandler produire 404/400/500 de façon cohérente.
2.6 Kafka (déjà traité)
- Tuning prod (
max.poll.interval.ms,max.poll.records,session.timeout.ms) déjà ajouté dansapplication-prod.properties. - Bonnes pratiques SmallRye : en cas d’échec critique après consommation, envisager
message.nack()et stratégie de commit manuel si nécessaire (au-delà du scope de cet audit).
3. Frontend (Flutter) – Analyse détaillée
3.1 Structure (clean architecture)
Recommandation : Séparation nette data / domain / presentation ; repositories en abstraction dans domain ; use cases optionnels mais utiles pour une logique métier réutilisable.
Constat :
- Présence de
data/(datasources, models, repositories impl, services),domain/(entities, repositories abstraits, usecases partiels),presentation/(screens, state_management avec BLoC). - Les datasources sont bien séparés ; les repositories implémentent les contrats du domain. Use cases présents seulement pour une partie des flux (ex.
get_user).
Verdict : Conforme à une clean architecture légère. On peut étendre progressivement les use cases pour les flux critiques.
3.2 Appels API et authentification
Recommandation : Toute requête vers une API protégée doit envoyer le token (ex. Authorization: Bearer <token>). Le token doit être lu depuis un stockage sécurisé et rafraîchi si nécessaire.
Constat :
- Aucun datasource (user, notification, chat, event, social, reservation, establishment, etc.) n’ajoute d’en-tête
AuthorizationouBearer. - Les headers utilisés sont principalement
Content-TypeetAccept. Aucune utilisation deSecureStorage(ou équivalent) pour récupérer un token et l’attacher aux requêtes. - L’API backend n’exige aujourd’hui pas de JWT ; en revanche, dès que l’auth sera activée côté backend, tous les appels devront envoyer le token.
Recommandation :
- Créer un client HTTP unique (wrapper ou interceptor) qui récupère le token (ex. depuis
SecureStorage) et ajouteAuthorization: Bearer <token>à chaque requête. - Utiliser ce client dans tous les datasources au lieu d’utiliser
http.Clientbrut sans headers d’auth. - Gérer le cas “token absent ou expiré” (401) : redirection vers login ou refresh.
3.3 WebSocket (notifications et chat)
Recommandation (bonnes pratiques WebSocket) : Heartbeat régulier, reconnexion avec backoff exponentiel, file d’attente des messages en cas de déconnexion si besoin.
Constat :
- RealtimeNotificationService et ChatWebSocketService :
- Connexion avec
WebSocketChannel.connect. - Heartbeat toutes les 30 s (
_heartbeatInterval). - Reconnexion avec délai fixe (
_initialReconnectDelay = 5 s) et plafond de tentatives (_maxReconnectAttempts = 5).
- Connexion avec
- Pas de backoff exponentiel (délai constant entre les tentatives). Pour réduire la charge serveur en cas de panne, un backoff exponentiel est préférable.
Recommandation : Conserver le heartbeat et la reconnexion ; ajouter un backoff exponentiel (ex. 2s, 4s, 8s, 16s, 30s) pour les tentatives de reconnexion, avec un plafond (ex. 30 s).
3.4 Gestion des erreurs et parsing
- Les datasources gèrent les timeouts,
SocketException, et codes HTTP (401, 404, etc.) et lèvent des exceptions métier (ex.ServerException,UnauthorizedException). C’est cohérent. - Vérifier que partout où l’on parse le body d’erreur, on utilise une clé unique (ex.
erroroumessage) alignée avec le backend. Après correction du backend (réponse d’erreur en JSON structuré), adapter si nécessaire le parsing côté Flutter pour lireerroroumessage.
4. Tableau de synthèse des écarts
| # | Composant | Écart | Sévérité | Action recommandée |
|---|---|---|---|---|
| 1 | Backend | Aucune auth JWT ; userId pris de l’URL/body sans preuve | Critique | Introduire JWT et dériver userId du token |
| 2 | Frontend | Aucun en-tête Authorization sur les requêtes API | Critique | Client HTTP centralisé avec Bearer token |
| 3 | Backend | MessageResource : accès direct au repository + validation manuelle | Moyen | Déléguer au service ; Bean Validation sur SendMessageRequestDTO |
| 4 | Backend | buildResponse : concaténation JSON pour le message d’erreur | Moyen | Utiliser un DTO/Map sérialisé en JSON |
| 5 | Backend | FriendshipNotFoundException, EstablishmentHasDependenciesException non gérées dans GlobalExceptionHandler | Moyen | Ajouter les branches et codes HTTP appropriés |
| 6 | Backend | MessageResource : try/catch générique qui masque les exceptions métier | Moyen | Lever des exceptions typées et laisser le handler global gérer |
| 7 | Frontend | Reconnexion WebSocket avec délai fixe | Faible | Implémenter backoff exponentiel |
5. Bonnes pratiques croisées (références)
- Quarkus REST : Resource → Service → Repository ; DTO +
@Valid; ExceptionMapper unique ; pas de logique métier dans la resource. - Sécurité REST/JWT : Vérifier le token sur chaque requête ; ne pas faire confiance au userId passé par le client pour l’autorisation.
- Flutter : Clean architecture avec repositories abstraits ; couche data qui envoie toujours l’auth (client commun avec token).
- WebSocket : Heartbeat + reconnexion avec backoff exponentiel pour limiter la charge et les reconnexions agressives.
6. Conclusion
Les points les plus critiques concernent l’authentification et l’autorisation : côté backend, aucun contrôle sur l’identité de l’appelant ; côté frontend, aucun token n’est envoyé. La cohérence des couches (resource sans accès direct au repository pour la logique métier), la validation (Bean Validation partout, y compris chat), et la gestion d’erreurs (réponse JSON sûre, exceptions métier gérées centralement) sont à renforcer pour aligner le projet sur les bonnes pratiques et sécuriser la production.
7. Corrections appliquées (suite à l'audit)
- GlobalExceptionHandler : Réponse d'erreur en JSON via ObjectMapper ; prise en charge de
FriendshipNotFoundException(404) etEstablishmentHasDependenciesException(409). - SendMessageRequestDTO : Bean Validation ; suppression de
isValid(). - MessageResource :
@Valid,UsersServiceau lieu deUsersRepository, suppression des try/catch locaux. - MessageService :
NotFoundExceptionsi conversation ou message absent. - JWT :
JwtService, token au login (HS256),UserAuthenticateResponseDTO.token, configafterwork.jwt.secret. - Frontend :
SecureStorage.saveAuthToken/getAuthToken,ApiClient(Authorization Bearer), tous datasources + FriendsRepositoryImpl ; sauvegarde du token à l'authentification. - WebSocket : Backoff exponentiel (2^attempt s, max 30 s) dans ChatWebSocketService et RealtimeNotificationService.