2. Authentification et Gestion des Sessions
Vue d'ensemble
Cette catégorie regroupe tous les services et contrôleurs responsables de l'authentification des utilisateurs via Firebase, de la gestion des sessions actives, et de la sécurité des communications. Elle assure qu'un utilisateur ne peut être connecté que sur un seul appareil à la fois et que toutes les communications sont authentifiées.
Services et Contrôleurs
AuthController (auth.controller.ts)
Contrôleur Express exposant les endpoints d'authentification.
Routes configurées:
POST /auth/register // Inscription avec avatar (multipart)
POST /auth/login // Connexion avec Firebase token
POST /auth/login-username // Récupération email par username
POST /auth/logout // Déconnexion
GET /auth/check-username/:username // Vérifier disponibilité username
Dépendances:
UserService: CRUD utilisateursFirebaseAuthService: Vérification tokens FirebaseSessionService: Gestion des sessionsAvatarService: Upload et validation avatarsmulter: Middleware pour upload multipart
Endpoint: POST /auth/register
Fonctionnalité: Inscription d'un nouvel utilisateur avec avatar optionnel
Body: FormData
idToken(string, requis): Token ID Firebaseusername(string, requis): Nom d'utilisateur (3-20 caractères)avatar(File, optionnel): Image de profil (max 10MB, formats: JPEG, PNG, GIF, WEBP)
Validation:
// Username
if (username.length < 3 || username.length > 20) {
return 400 "Username must be between 3 and 20 characters";
}
const usernameRegex = /^[a-zA-Z0-9_]+$/;
if (!usernameRegex.test(username)) {
return 400 "Username can only contain letters, numbers, and underscores";
}
// Email (de Firebase)
if (email && email.length > 254) {
return 400 "Email address too long";
}
// Avatar (si fourni)
const validation = avatarService.validateImageFile(avatarFile);
if (!validation.isValid) {
return 400 validation.error;
}
Processus:
- Vérification du token Firebase → récupération du
uidetemail - Vérification qu'aucun utilisateur n'existe avec ce
firebaseUid - Vérification de la disponibilité du username
- Création de l'utilisateur dans MongoDB via
UserService.createUser() - Si avatar fourni : upload via
AvatarService.createAvatar()et mise à jour user - Retour des informations utilisateur
Réponses:
201 Created: Utilisateur créé avec succès{ "message": "User created successfully", "user": { "id": "...", "username": "...", "virtualCurrencyBalance": 1000, "preferences": {...}, "avatarId": "...", "avatarVersion": 1234567890, "createdAt": "2024-..." } }400 Bad Request: Données invalides401 Unauthorized: Token Firebase invalide409 Conflict: Utilisateur ou username existe déjà
Endpoint: GET /auth/check-username/:username
Fonctionnalité: Vérifier la disponibilité d'un username
Paramètres:
username(string, URL param): Username à vérifier
Processus:
- Appel
UserService.isUsernameAvailable(username) - Retour du résultat
Réponses:
200 OK:{ "available": true | false }400 Bad Request: Username manquant
Endpoint: POST /auth/login
Fonctionnalité: Connexion d'un utilisateur avec token Firebase
Body:
{
"idToken": "firebase_id_token"
}
Processus:
- Vérification du token Firebase → récupération du
uid - Récupération de l'utilisateur via
UserService.getUserByFirebaseUid(uid) - Vérification de session active via
SessionService.checkExistingSession(uid)- Si session active et socket connecté → retour 409 ALREADY_CONNECTED
- Création d'une nouvelle session via
SessionService.createSession(uid) - Construction de l'URL de l'avatar si présent
- Retour du
sessionIdet des informations utilisateur
Réponses:
200 OK: Connexion réussie{ "message": "Login successful", "sessionId": "uuid-v4", "user": { "id": "...", "username": "...", "virtualCurrencyBalance": 1000, "avatarId": "...", "avatarVersion": 1234567890, "avatarUrl": "http://server/avatars/...", "preferences": {...} } }401 Unauthorized: Token invalide404 Not Found: Utilisateur introuvable409 Conflict: Utilisateur déjà connecté ailleurs{ "message": "User already connected elsewhere", "error": "ALREADY_CONNECTED" }
Endpoint: POST /auth/login-username
Fonctionnalité: Récupérer l'email associé à un username (pour connexion)
Body:
{
"username": "john_doe",
"password": "..." // Non utilisé côté serveur, validé par Firebase
}
Processus:
- Récupération de l'utilisateur via
UserService.getUserByUsername(username) - Retour de l'email (utilisé par le client pour connexion Firebase)
Réponses:
200 OK:{ "email": "john@example.com" }400 Bad Request: Champs manquants401 Unauthorized: Username introuvable
Endpoint: POST /auth/logout
Fonctionnalité: Déconnexion d'un utilisateur
Body:
{
"idToken": "firebase_id_token"
}
Processus:
- Vérification du token Firebase → récupération du
uid - Suppression de la session via
SessionService.clearSession(uid) - Confirmation de déconnexion
Réponses:
200 OK:{ "message": "Logout successful" }400 Bad Request: Token manquant401 Unauthorized: Token invalide
FirebaseAuthService (firebase-auth.service.ts)
Service singleton gérant l'intégration avec Firebase Admin SDK.
Responsabilités:
- Initialiser Firebase Admin SDK
- Vérifier les tokens ID Firebase
- Récupérer les informations utilisateur de Firebase
Configuration:
constructor() {
this.initializeFirebase();
}
private initializeFirebase(): void {
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert(FIREBASE_CONFIG as admin.ServiceAccount)
});
}
}
Variables d'environnement (FIREBASE_CONFIG dans env.ts):
{
"type": "service_account",
"project_id": "...",
"private_key_id": "...",
"private_key": "...",
"client_email": "...",
"client_id": "...",
"auth_uri": "...",
"token_uri": "...",
"auth_provider_x509_cert_url": "...",
"client_x509_cert_url": "..."
}
Méthode: verifyIdToken
Signature:
async verifyIdToken(idToken: string): Promise<admin.auth.DecodedIdToken | null>
Fonctionnalité: Vérifie et décode un token ID Firebase
Processus:
- Appel à
admin.auth().verifyIdToken(idToken) - Retour du token décodé avec informations utilisateur
- Retour
nullsi erreur
Token décodé contient:
uid: Firebase User ID (unique)email: Email de l'utilisateuremail_verified: Si l'email est vérifiéiat: Date d'émission du tokenexp: Date d'expiration du tokenaud: Audience (project ID)iss: Issuer- Et autres claims Firebase
Méthode: getUserByUid
Signature:
async getUserByUid(uid: string): Promise<admin.auth.UserRecord | null>
Fonctionnalité: Récupère les informations d'un utilisateur Firebase par UID
Utilisation:
- Récupération d'informations supplémentaires (date création, providers, etc.)
- Vérification de l'existence d'un compte Firebase
SessionService (session.service.ts)
Service singleton gérant les sessions utilisateur actives et leur association aux sockets.
Responsabilités:
- Créer et valider les sessions
- Associer sessions et sockets
- Gérer la connexion unique (un appareil à la fois)
- Nettoyer les sessions à la déconnexion
Structure de données:
private sessionToSocket: Map<string, string> = new Map(); // sessionId → socketId
private socketToSession: Map<string, string> = new Map(); // socketId → sessionId
private io: SocketIOServer | null = null;
Dépendances:
UserService: Accès aux données utilisateur (activeSessionId)- Socket.IO Server: Vérification des sockets connectés
Méthode: setSocketServer
Signature:
setSocketServer(io: SocketIOServer): void
Fonctionnalité: Définit la référence au serveur Socket.IO
Appelé dans: server.ts après initialisation de Socket.IO
Méthode: checkExistingSession
Signature:
async checkExistingSession(firebaseUid: string): Promise<boolean>
Fonctionnalité: Vérifie si l'utilisateur a déjà une session active
Processus:
- Récupération de l'utilisateur via
UserService.getUserByFirebaseUid() - Si pas de
activeSessionId→ retourfalse - Récupération du socket ID associé à la session
- Vérification si le socket est toujours connecté via
isSocketConnected() - Si socket déconnecté → nettoyage de la session et retour
false - Si socket connecté → retour
true
Usage: Appelé avant de créer une nouvelle session pour éviter les connexions multiples
Méthode: createSession
Signature:
async createSession(firebaseUid: string, socketId?: string): Promise<string>
Fonctionnalité: Crée une nouvelle session pour un utilisateur
Processus:
- Génération d'un UUID v4 comme session ID
- Mise à jour du champ
activeSessionIdde l'utilisateur dans MongoDB - Si
socketIdfourni → association session-socket - Retour du session ID
Session ID: UUID v4 (ex: 550e8400-e29b-41d4-a716-446655440000)
Méthode: clearSession
Signature:
async clearSession(firebaseUid: string): Promise<void>
Fonctionnalité: Supprime la session d'un utilisateur
Processus:
- Récupération de l'utilisateur
- Si
activeSessionIdprésent → nettoyage des mappings - Suppression du champ
activeSessionIddans MongoDB
Appelé lors:
- Déconnexion volontaire (logout)
- Déconnexion du socket
- Expiration de session
Méthode: clearSessionBySocketId
Signature:
async clearSessionBySocketId(socketId: string): Promise<void>
Fonctionnalité: Supprime une session à partir d'un socket ID
Processus:
- Récupération du session ID via le mapping
socketToSession - Récupération de l'utilisateur via
UserService.getUserByActiveSession() - Nettoyage de la session de l'utilisateur
Appelé lors: Déconnexion d'un socket
Méthode: validateSession
Signature:
async validateSession(sessionId: string): Promise<boolean>
Fonctionnalité: Valide qu'une session est toujours active et valide
Processus:
- Récupération de l'utilisateur via
UserService.getUserByActiveSession() - Si utilisateur introuvable → retour
false - Vérification que le socket associé est toujours connecté
- Si socket déconnecté → nettoyage et retour
false - Retour
truesi tout est OK
Méthode: associateSessionWithSocket
Signature:
associateSessionWithSocket(sessionId: string, socketId: string): void
Fonctionnalité: Associe une session à un socket spécifique
Processus:
- Si le socket a déjà une session → nettoyage de l'ancienne
- Si la session a déjà un socket → nettoyage de l'ancien
- Création des nouveaux mappings bidirectionnels
Appelé lors: Authentification d'un socket avec un session ID
Méthode: handleSocketDisconnect
Signature:
async handleSocketDisconnect(socketId: string): Promise<void>
Fonctionnalité: Gère la déconnexion d'un socket
Processus:
- Récupération du session ID associé
- Suppression des mappings (mais conservation de
activeSessionIddans MongoDB) - L'utilisateur peut se reconnecter avec le même session ID
Note: Ne nettoie pas complètement la session pour permettre la reconnexion automatique
Méthode: getSocketIdBySessionId
Signature:
getSocketIdBySessionId(sessionId: string): string | null
Fonctionnalité: Récupère le socket ID associé à une session
Usage: Envoi de messages ciblés à un utilisateur spécifique
Méthode: getSocketIdByFirebaseUid
Signature:
async getSocketIdByFirebaseUid(firebaseUid: string): Promise<string | null>
Fonctionnalité: Récupère le socket ID d'un utilisateur par son Firebase UID
Processus:
- Récupération de l'utilisateur
- Validation de la session active
- Retour du socket ID associé
Usage: Notifications ciblées (demandes d'amis, messages, etc.)
AuthMiddleware (auth.middleware.ts)
Middleware Express pour protéger les routes nécessitant une authentification.
Utilisation:
router.get('/protected-route', authMiddleware, controller.method);
Processus:
- Extraction du token depuis le header
Authorization: Bearer <token> - Vérification du token via
FirebaseAuthService.verifyIdToken() - Si valide → ajout du
firebaseUiddansreqet passage au handler suivant - Si invalide → retour 401 Unauthorized
Code:
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ message: 'Unauthorized' });
}
const idToken = authHeader.split(' ')[1];
const firebaseAuthService = Container.get(FirebaseAuthService);
const decodedToken = await firebaseAuthService.verifyIdToken(idToken);
if (!decodedToken) {
return res.status(401).json({ message: 'Invalid token' });
}
(req as any).firebaseUid = decodedToken.uid;
next();
};
Ajout dans req:
interface AuthenticatedRequest extends Request {
firebaseUid: string; // Ajouté par le middleware
}
Routes protégées:
/users/profile(GET)/friends/*(GET, POST, DELETE)/channels/*(GET, POST, DELETE)/avatars/upload(POST)/games/visible(GET)
AuthSocketService (auth-socket.service.ts)
Service gérant l'authentification des connexions Socket.IO.
Responsabilités:
- Authentifier les sockets avec un session ID
- Associer les sockets aux sessions
- Gérer les déconnexions
Événement écouté: AuthenticateSocket
Payload:
{
sessionId: string // UUID de la session
}
Processus d'authentification:
- Réception de l'événement
AuthenticateSocketavecsessionId - Validation de la session via
SessionService.validateSession() - Si valide → association session-socket via
SessionService.associateSessionWithSocket() - Émission de confirmation au client
- Si invalide → émission d'erreur et déconnexion du socket
Code:
configureSocket(socket: Socket) {
socket.on(SocketIOEvents.AuthenticateSocket, async (data: { sessionId: string }) => {
const isValid = await this.sessionService.validateSession(data.sessionId);
if (isValid) {
this.sessionService.associateSessionWithSocket(data.sessionId, socket.id);
socket.emit(SocketIOEvents.AuthenticationSuccess);
} else {
socket.emit(SocketIOEvents.AuthenticationFailed, {
message: 'Invalid session'
});
socket.disconnect();
}
});
}
Événement de déconnexion:
socket.on('disconnect', async () => {
await this.sessionService.handleSocketDisconnect(socket.id);
});
Flux d'Authentification Complet
1. Inscription
Client (Web/Mobile)
↓
1. createUserWithEmailAndPassword() → Firebase
↓
2. getIdToken() → Firebase
↓
3. POST /auth/register { idToken, username, avatar }
↓
Server
↓
4. verifyIdToken() → Firebase Admin SDK
↓
5. createUser() → MongoDB
↓
6. createAvatar() → GridFS (si avatar)
↓
7. Response: { user }
↓
Client
↓
8. Connexion automatique après inscription
2. Connexion
Client
↓
1. POST /auth/login-username { username }
↓
Server
↓
2. getUserByUsername() → MongoDB
↓
3. Response: { email }
↓
Client
↓
4. signInWithEmailAndPassword(email, password) → Firebase
↓
5. getIdToken() → Firebase
↓
6. POST /auth/login { idToken }
↓
Server
↓
7. verifyIdToken() → Firebase Admin SDK
↓
8. getUserByFirebaseUid() → MongoDB
↓
9. checkExistingSession() → Vérification session active
↓
10. createSession() → Nouvelle session UUID
↓
11. Response: { sessionId, user }
↓
Client
↓
12. Connexion Socket.IO
↓
13. Emit: AuthenticateSocket { sessionId }
↓
Server (Socket)
↓
14. validateSession()
↓
15. associateSessionWithSocket()
↓
16. Emit: AuthenticationSuccess
↓
Client
↓
17. Navigation vers /home
3. Connexion Multiple (Bloquée)
Client A (déjà connecté)
↓
Socket ID: socket-abc
Session ID: session-123
↓
Client B (tentative de connexion)
↓
1. POST /auth/login { idToken }
↓
Server
↓
2. checkExistingSession()
↓
3. activeSessionId = "session-123"
↓
4. getSocketId("session-123") = "socket-abc"
↓
5. isSocketConnected("socket-abc") = true
↓
6. Response: 409 Conflict { error: "ALREADY_CONNECTED" }
↓
Client B
↓
7. signOut() → Firebase (déconnexion locale)
↓
8. Affichage popup: "Déjà connecté ailleurs"
4. Déconnexion
Client
↓
1. POST /auth/logout { idToken }
↓
Server
↓
2. verifyIdToken()
↓
3. clearSession(firebaseUid)
↓
4. Suppression activeSessionId dans MongoDB
↓
5. Suppression mappings session-socket
↓
6. Response: { message: "Logout successful" }
↓
Client
↓
7. signOut() → Firebase
↓
8. Déconnexion socket
↓
9. Navigation vers /auth
5. Déconnexion Socket (Perte de connexion)
Client (connexion perdue)
↓
Socket déconnecté
↓
Server (Socket)
↓
1. Événement 'disconnect'
↓
2. handleSocketDisconnect(socketId)
↓
3. Suppression mappings (mais conservation activeSessionId)
↓
Client (reconnexion)
↓
4. Socket reconnecté automatiquement
↓
5. Emit: AuthenticateSocket { sessionId } (session ID en mémoire)
↓
Server
↓
6. validateSession() → OK (activeSessionId encore présent)
↓
7. associateSessionWithSocket() → Nouveaux mappings
↓
8. Emit: AuthenticationSuccess
Sécurité
Protection contre les Connexions Multiples
Mécanisme:
- Chaque utilisateur a un seul
activeSessionIddans MongoDB - À la connexion, vérification si une session existe déjà
- Si session existe et socket connecté → refus de la nouvelle connexion
- Si session existe mais socket déconnecté → réutilisation ou nettoyage
Avantages:
- Évite le partage de compte
- Protège contre les connexions simultanées non autorisées
- Force la déconnexion de l'autre appareil avant nouvelle connexion
Validation des Tokens Firebase
Méthode: Vérification cryptographique par Firebase Admin SDK
Vérifications:
- Signature du token (RS256)
- Expiration (1 heure par défaut)
- Audience (project ID)
- Issuer (Firebase)
- Claims personnalisés
Sécurité:
- Impossible de forger un token sans la clé privée Firebase
- Révocation automatique à la déconnexion Firebase
- Pas de stockage de mot de passe côté serveur
Session IDs
Format: UUID v4 (128 bits d'entropie)
Caractéristiques:
- Impossible à deviner
- Unique garanti
- Pas d'information sur l'utilisateur
- Révocable à tout moment
HTTPS Obligatoire
Configuration:
- Toutes les communications en HTTPS en production
- Tokens Firebase transmis uniquement sur canal sécurisé
- Session IDs transmis uniquement sur canal sécurisé
Base de Données
Collection Users
Champ ajouté pour les sessions:
{
_id: ObjectId,
firebaseUid: string, // Clé unique Firebase
username: string,
email: string,
activeSessionId: string, // UUID de la session active (nullable)
// ... autres champs
}
Index:
firebaseUid(unique)activeSessionId(pour recherche rapide)
Tests et Validation
Cas de Test
Test 1: Inscription réussie
- Création compte Firebase
- Enregistrement MongoDB
- Upload avatar
- Retour utilisateur complet
Test 2: Username déjà pris
- Tentative avec username existant
- Erreur 409 Conflict
Test 3: Connexion réussie
- Vérification token
- Création session
- Association socket
- Retour session ID
Test 4: Connexion multiple bloquée
- Utilisateur A connecté
- Utilisateur A tentative connexion ailleurs
- Erreur 409 avec ALREADY_CONNECTED
Test 5: Déconnexion et reconnexion
- Déconnexion propre
- Session nettoyée
- Reconnexion réussie avec nouvelle session
Test 6: Perte de connexion et reconnexion
- Socket déconnecté
- Mappings nettoyés
- Reconnexion avec même session ID
- Restauration des mappings
Performance
Optimisations
- Mappings en mémoire (Map) pour accès O(1)
- Pas de requête DB pour vérification de session (sauf validation)
- Cache des tokens Firebase (géré par Firebase Admin SDK)
- Nettoyage automatique des sessions obsolètes
Scalabilité
Limitations actuelles:
- Sessions en mémoire (perdu au redémarrage serveur)
- Pas de sticky sessions pour load balancing
Améliorations futures:
- Redis pour sessions distribuées
- Sticky sessions avec Socket.IO + Redis adapter
- Expiration automatique des sessions
- Cleanup job pour sessions orphelines
Améliorations Futures
- Authentification multi-facteurs (2FA)
- Gestion des sessions actives (liste et révocation)
- Expiration automatique des sessions (timeout)
- Logs d'audit des connexions
- Rate limiting sur les tentatives de connexion
- Support de multiple appareils (avec liste de sessions)
- Notifications de nouvelle connexion
- Force logout à distance
- Refresh tokens pour renouvellement