Aller au contenu principal

Authentification

Documentation detaillee des mecanismes d'authentification de MyTelevision API.

Vue d'ensemble

MyTelevision API utilise une architecture d'authentification multi-couches :

  1. Firebase Authentication -- Identity Provider (IdP) pour le login social
  2. JWT Tokens -- Authentification interne de l'API
  3. Session Management -- Suivi de sessions via Redis
  4. Account/Profile System -- Acces multi-profil style Netflix

Architecture d'authentification

Configuration JWT

Structure des tokens

// Access Token Payload
interface AccessTokenPayload {
sub: string; // User ID
email: string;
role: UserRole;
accountId?: string; // Systeme Account
profileId?: string; // Profil courant
sessionId: string;
iat: number; // Issued at
exp: number; // Expiration
iss: string; // Issuer: 'mytv-api'
}

// Refresh Token Payload
interface RefreshTokenPayload {
sub: string;
sessionId: string;
tokenFamily: string; // Pour le tracking de rotation
iat: number;
exp: number;
}

Parametres des tokens

ParametreValeurJustification
AlgorithmHS256Symetrique pour usage interne
Access TTL1hLimite la fenetre d'exposition
Refresh TTL7 joursEquilibre entre commodite et securite
Issuermytv-apiValidation du token
Correction securite

Le TTL de l'access token a ete reduit de 1 jour a 1 heure (fix SEC-006) pour limiter la fenetre d'exposition en cas de compromission.

Validation JWT

// Parametres de validation JWT
{
algorithms: ['HS256'],
issuer: 'mytv-api',
ignoreExpiration: false,
clockTolerance: 30, // secondes
}

Stockage des tokens

Les tokens sont hashes en SHA256 avant stockage en base de donnees. Les tokens ne sont jamais stockes en clair.

// Hashage SHA256 avant stockage
import { createHash } from 'crypto';

function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}

Flux de token refresh

Rotation des tokens

Les refresh tokens sont rotates a chaque utilisation pour detecter le vol de tokens :

// Logique de rotation des tokens
async refreshTokens(refreshToken: string) {
const decoded = this.verifyRefreshToken(refreshToken);

// Verifier si le token a deja ete utilise (detection de vol)
const wasUsed = await this.redis.get(`used:${decoded.jti}`);
if (wasUsed) {
// Reutilisation detectee - invalider toute la famille
await this.invalidateTokenFamily(decoded.tokenFamily);
throw new UnauthorizedException('Token reuse detected');
}

// Marquer le token comme utilise
await this.redis.set(`used:${decoded.jti}`, '1', 'EX', 86400);

// Generer de nouveaux tokens avec la meme famille
return this.generateTokenPair(decoded.sub, decoded.tokenFamily);
}
Rate limiting

L'endpoint /refresh est protege par un rate limiting de 5 requetes/minute pour prevenir les abus (fix MED-005).

Gestion des sessions

Structure de session

interface Session {
id: string;
userId: string;
accountId?: string;
profileId?: string;
deviceId: string;
deviceInfo: {
type: DeviceType;
name: string;
os: string;
browser?: string;
};
ipAddress: string;
userAgent: string;
createdAt: Date;
lastActivityAt: Date;
expiresAt: Date;
status: 'ACTIVE' | 'EXPIRED' | 'REVOKED';
}

Limites de sessions

Type de compteSessions simultanees max
Free1
Basic2
Premium4
Ultimate6

Operations sur les sessions

// Logout de tous les appareils
async logoutAll(userId: string) {
const sessions = await this.getActiveSessions(userId);
for (const session of sessions) {
await this.revokeSession(session.id);
}
// Blacklister tous les tokens existants
await this.blacklistUserTokens(userId);
}

// Logout d'une session specifique
async logoutSession(sessionId: string) {
await this.redis.set(`revoked:session:${sessionId}`, '1', 'EX', 86400 * 7);
await this.updateSessionStatus(sessionId, 'REVOKED');
}

Isolation des sessions

Les sessions sont isolees par la combinaison (tenant_id, account_id, profile_id, device_id), garantissant une separation stricte entre les tenants et les profils.

Integration Firebase

Verification des tokens Firebase

@Injectable()
export class FirebaseService {
private firebaseApp: admin.app.App;

async verifyIdToken(idToken: string): Promise<DecodedIdToken> {
try {
// Verification avec Firebase Admin SDK
// Verifie : signature, expiration, audience, issuer
const decoded = await this.firebaseApp
.auth()
.verifyIdToken(idToken, true);

// Validation supplementaire
if (!decoded.email_verified && this.requireEmailVerification) {
throw new UnauthorizedException('Email not verified');
}

return decoded;
} catch (error) {
if (error.code === 'auth/id-token-expired') {
throw new UnauthorizedException('Firebase token expired');
}
throw new UnauthorizedException('Invalid Firebase token');
}
}
}
Configuration Firebase securisee

Le parsing de FIREBASE_PRIVATE_KEY a ete securise avec validation du format PEM et support multi-format (fix SEC-008).

Providers sociaux supportes

ProviderEndpoint
GooglePOST /api/v2/auth/social/google
ApplePOST /api/v2/auth/social/apple
FacebookPOST /api/v2/auth/social/facebook
GenericPOST /api/v2/auth/firebase

Gestion des comptes sociaux

EndpointDescription
POST /api/v2/auth/social/linkLier un nouveau provider
DELETE /api/v2/auth/social/unlink/:providerRetirer un provider lie
GET /api/v2/auth/social/accountsLister les providers lies

Strategie de liaison de comptes (ordre de priorite)

  1. Recherche par provider + providerUserId -- Compte social existant
  2. Recherche par firebaseUid -- Utilisateurs legacy Firebase (migration)
  3. Recherche par email -- Liaison au compte existant base sur l'email
  4. Creation nouveau compte -- Premier login social
async linkSocialAccount(firebaseToken: DecodedIdToken): Promise<User> {
// 1. Verifier si le compte social existe deja
const existingSocial = await this.findSocialAccount(
firebaseToken.firebase.sign_in_provider,
firebaseToken.uid,
);
if (existingSocial) return existingSocial.user;

// 2. Verifier par Firebase UID (migration legacy)
const legacyUser = await this.findByFirebaseUid(firebaseToken.uid);
if (legacyUser) {
await this.createSocialAccount(legacyUser.id, firebaseToken);
return legacyUser;
}

// 3. Verifier par email (liaison au compte existant)
if (firebaseToken.email) {
const emailUser = await this.findByEmail(firebaseToken.email);
if (emailUser) {
await this.createSocialAccount(emailUser.id, firebaseToken);
return emailUser;
}
}

// 4. Creer un nouvel utilisateur
return this.createUserFromFirebase(firebaseToken);
}

Securite des mots de passe

Politique de mots de passe

const passwordPolicy = {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumber: true,
requireSpecialChar: true,
disallowCommonPasswords: true,
disallowUserInfo: true, // Pas d'email/nom dans le mot de passe
};

// Regex de validation
const passwordRegex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;

Hashage des mots de passe

import * as bcrypt from 'bcrypt';

const BCRYPT_ROUNDS = 12; // ~250ms sur hardware moderne

async hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, BCRYPT_ROUNDS);
}

async verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}

Protection contre le brute force

// Rate limiting sur les tentatives de login
@Throttle({
short: { ttl: 60000, limit: 5 }, // 5 tentatives par minute
long: { ttl: 3600000, limit: 20 }, // 20 tentatives par heure
})
@Post('login')
async login(@Body() dto: LoginDto) { ... }

// Verrouillage du compte apres echecs
async recordFailedAttempt(email: string) {
const key = `login:failed:${email}`;
const attempts = await this.redis.incr(key);
await this.redis.expire(key, 900); // 15 minutes

if (attempts >= 5) {
await this.lockAccount(email, 15 * 60); // Verrouillage 15 minutes
throw new UnauthorizedException('Account temporarily locked');
}
}
Messages d'erreur securises

Les messages de login sont generiques ("Invalid credentials") pour empecher l'enumeration des utilisateurs (fix LOW-002).

Securite des PIN de profil

Exigences PIN

  • 4 a 6 chiffres uniquement
  • Stocke en hash bcrypt
  • Non reutilisable pour les 3 derniers PIN
  • Obligatoire pour l'acces aux profils enfants

Protection brute force PIN

const PIN_MAX_ATTEMPTS = 5;
const PIN_LOCKOUT_DURATION = 15 * 60; // 15 minutes

async verifyPin(profileId: string, pin: string): Promise<boolean> {
const profile = await this.findProfile(profileId);

// Verifier le verrouillage
const lockoutKey = `pin:lockout:${profileId}`;
const isLocked = await this.redis.get(lockoutKey);
if (isLocked) {
throw new ForbiddenException(
'Profile locked due to too many PIN attempts',
);
}

// Verifier le PIN
const isValid = await bcrypt.compare(pin, profile.pinHash);

if (!isValid) {
const attemptsKey = `pin:attempts:${profileId}`;
const attempts = await this.redis.incr(attemptsKey);
await this.redis.expire(attemptsKey, 900);

if (attempts >= PIN_MAX_ATTEMPTS) {
await this.redis.set(lockoutKey, '1', 'EX', PIN_LOCKOUT_DURATION);
await this.redis.del(attemptsKey);
}

throw new UnauthorizedException('Invalid PIN');
}

// Effacer les tentatives en cas de succes
await this.redis.del(`pin:attempts:${profileId}`);
return true;
}

Security Headers

L'API configure les headers de securite via Helmet.js :

app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
frameguard: { action: 'deny' },
noSniff: true,
xssFilter: true,
}),
);

Audit des evenements d'authentification

Tous les evenements d'authentification sont logues :

interface AuthAuditLog {
timestamp: Date;
event: AuthEvent;
userId?: string;
email?: string;
ipAddress: string;
userAgent: string;
success: boolean;
failureReason?: string;
metadata?: Record<string, any>;
}

enum AuthEvent {
LOGIN_ATTEMPT = 'LOGIN_ATTEMPT',
LOGIN_SUCCESS = 'LOGIN_SUCCESS',
LOGIN_FAILURE = 'LOGIN_FAILURE',
LOGOUT = 'LOGOUT',
TOKEN_REFRESH = 'TOKEN_REFRESH',
PASSWORD_RESET_REQUEST = 'PASSWORD_RESET_REQUEST',
PASSWORD_CHANGED = 'PASSWORD_CHANGED',
SOCIAL_LOGIN = 'SOCIAL_LOGIN',
ACCOUNT_LOCKED = 'ACCOUNT_LOCKED',
SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY',
}

Flux d'authentification multi-tenant (Two-Step Login)

Le systeme multi-tenant utilise un flux de login en deux etapes :

EtapeEndpointDescription
1POST /api/v2/account-auth/loginValidation des credentials
2POST /api/v2/account-auth/select-profileSelection du profil + tokens

Autres endpoints du systeme :

EndpointDescription
POST /api/v2/account-auth/registerCreer un nouveau compte
POST /api/v2/account-auth/refreshRafraichir l'access token
POST /api/v2/account-auth/logoutRevoquer la session
POST /api/v2/account-auth/logout-allRevoquer toutes les sessions
GET /api/v2/account-auth/meCompte courant
GET /api/v2/account-auth/me/profilesProfils pour la selection

Variables d'environnement

# Configuration JWT
JWT_SECRET=votre-secret-minimum-32-caracteres
JWT_REFRESH_SECRET=autre-secret-minimum-32-caracteres
JWT_EXPIRATION=1h
JWT_REFRESH_EXPIRATION=7d

# Firebase Admin SDK
FIREBASE_PROJECT_ID=votre-projet-firebase
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
FIREBASE_CLIENT_EMAIL=[email protected]

# Parametres de securite
BCRYPT_ROUNDS=12
MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION=900
TRUST_PROXY=true # En staging/production
Secrets obligatoires

JWT_SECRET et JWT_REFRESH_SECRET sont obligatoires sans valeur par defaut. L'application refuse de demarrer si ces variables sont manquantes (fail-fast, fix SEC-001).