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:

Endpoint: POST /auth/register

Fonctionnalité: Inscription d'un nouvel utilisateur avec avatar optionnel

Body: FormData

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:

  1. Vérification du token Firebase → récupération du uid et email
  2. Vérification qu'aucun utilisateur n'existe avec ce firebaseUid
  3. Vérification de la disponibilité du username
  4. Création de l'utilisateur dans MongoDB via UserService.createUser()
  5. Si avatar fourni : upload via AvatarService.createAvatar() et mise à jour user
  6. Retour des informations utilisateur

Réponses:

Endpoint: GET /auth/check-username/:username

Fonctionnalité: Vérifier la disponibilité d'un username

Paramètres:

Processus:

  1. Appel UserService.isUsernameAvailable(username)
  2. Retour du résultat

Réponses:

Endpoint: POST /auth/login

Fonctionnalité: Connexion d'un utilisateur avec token Firebase

Body:

{
  "idToken": "firebase_id_token"
}

Processus:

  1. Vérification du token Firebase → récupération du uid
  2. Récupération de l'utilisateur via UserService.getUserByFirebaseUid(uid)
  3. Vérification de session active via SessionService.checkExistingSession(uid)
    • Si session active et socket connecté → retour 409 ALREADY_CONNECTED
  4. Création d'une nouvelle session via SessionService.createSession(uid)
  5. Construction de l'URL de l'avatar si présent
  6. Retour du sessionId et des informations utilisateur

Réponses:

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:

  1. Récupération de l'utilisateur via UserService.getUserByUsername(username)
  2. Retour de l'email (utilisé par le client pour connexion Firebase)

Réponses:

Endpoint: POST /auth/logout

Fonctionnalité: Déconnexion d'un utilisateur

Body:

{
  "idToken": "firebase_id_token"
}

Processus:

  1. Vérification du token Firebase → récupération du uid
  2. Suppression de la session via SessionService.clearSession(uid)
  3. Confirmation de déconnexion

Réponses:


FirebaseAuthService (firebase-auth.service.ts)

Service singleton gérant l'intégration avec Firebase Admin SDK.

Responsabilités:

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:

  1. Appel à admin.auth().verifyIdToken(idToken)
  2. Retour du token décodé avec informations utilisateur
  3. Retour null si erreur

Token décodé contient:

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:


SessionService (session.service.ts)

Service singleton gérant les sessions utilisateur actives et leur association aux sockets.

Responsabilités:

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:

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:

  1. Récupération de l'utilisateur via UserService.getUserByFirebaseUid()
  2. Si pas de activeSessionId → retour false
  3. Récupération du socket ID associé à la session
  4. Vérification si le socket est toujours connecté via isSocketConnected()
  5. Si socket déconnecté → nettoyage de la session et retour false
  6. 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:

  1. Génération d'un UUID v4 comme session ID
  2. Mise à jour du champ activeSessionId de l'utilisateur dans MongoDB
  3. Si socketId fourni → association session-socket
  4. 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:

  1. Récupération de l'utilisateur
  2. Si activeSessionId présent → nettoyage des mappings
  3. Suppression du champ activeSessionId dans MongoDB

Appelé lors:

Méthode: clearSessionBySocketId

Signature:

async clearSessionBySocketId(socketId: string): Promise<void>

Fonctionnalité: Supprime une session à partir d'un socket ID

Processus:

  1. Récupération du session ID via le mapping socketToSession
  2. Récupération de l'utilisateur via UserService.getUserByActiveSession()
  3. 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:

  1. Récupération de l'utilisateur via UserService.getUserByActiveSession()
  2. Si utilisateur introuvable → retour false
  3. Vérification que le socket associé est toujours connecté
  4. Si socket déconnecté → nettoyage et retour false
  5. Retour true si tout est OK

Méthode: associateSessionWithSocket

Signature:

associateSessionWithSocket(sessionId: string, socketId: string): void

Fonctionnalité: Associe une session à un socket spécifique

Processus:

  1. Si le socket a déjà une session → nettoyage de l'ancienne
  2. Si la session a déjà un socket → nettoyage de l'ancien
  3. 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:

  1. Récupération du session ID associé
  2. Suppression des mappings (mais conservation de activeSessionId dans MongoDB)
  3. 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:

  1. Récupération de l'utilisateur
  2. Validation de la session active
  3. 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:

  1. Extraction du token depuis le header Authorization: Bearer <token>
  2. Vérification du token via FirebaseAuthService.verifyIdToken()
  3. Si valide → ajout du firebaseUid dans req et passage au handler suivant
  4. 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:


AuthSocketService (auth-socket.service.ts)

Service gérant l'authentification des connexions Socket.IO.

Responsabilités:

Événement écouté: AuthenticateSocket

Payload:

{
  sessionId: string  // UUID de la session
}

Processus d'authentification:

  1. Réception de l'événement AuthenticateSocket avec sessionId
  2. Validation de la session via SessionService.validateSession()
  3. Si valide → association session-socket via SessionService.associateSessionWithSocket()
  4. Émission de confirmation au client
  5. 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:

  1. Chaque utilisateur a un seul activeSessionId dans MongoDB
  2. À la connexion, vérification si une session existe déjà
  3. Si session existe et socket connecté → refus de la nouvelle connexion
  4. Si session existe mais socket déconnecté → réutilisation ou nettoyage

Avantages:

Validation des Tokens Firebase

Méthode: Vérification cryptographique par Firebase Admin SDK

Vérifications:

Sécurité:

Session IDs

Format: UUID v4 (128 bits d'entropie)

Caractéristiques:

HTTPS Obligatoire

Configuration:

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:

Tests et Validation

Cas de Test

Test 1: Inscription réussie

Test 2: Username déjà pris

Test 3: Connexion réussie

Test 4: Connexion multiple bloquée

Test 5: Déconnexion et reconnexion

Test 6: Perte de connexion et reconnexion

Performance

Optimisations

Scalabilité

Limitations actuelles:

Améliorations futures:

Améliorations Futures