Authentification
Documentation detaillee des mecanismes d'authentification de MyTelevision API.
Vue d'ensemble
MyTelevision API utilise une architecture d'authentification multi-couches :
- Firebase Authentication -- Identity Provider (IdP) pour le login social
- JWT Tokens -- Authentification interne de l'API
- Session Management -- Suivi de sessions via Redis
- 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
| Parametre | Valeur | Justification |
|---|---|---|
| Algorithm | HS256 | Symetrique pour usage interne |
| Access TTL | 1h | Limite la fenetre d'exposition |
| Refresh TTL | 7 jours | Equilibre entre commodite et securite |
| Issuer | mytv-api | Validation du token |
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);
}
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 compte | Sessions simultanees max |
|---|---|
| Free | 1 |
| Basic | 2 |
| Premium | 4 |
| Ultimate | 6 |
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');
}
}
}
Le parsing de FIREBASE_PRIVATE_KEY a ete securise avec validation du format PEM et support multi-format (fix SEC-008).
Providers sociaux supportes
| Provider | Endpoint |
|---|---|
POST /api/v2/auth/social/google | |
| Apple | POST /api/v2/auth/social/apple |
POST /api/v2/auth/social/facebook | |
| Generic | POST /api/v2/auth/firebase |
Gestion des comptes sociaux
| Endpoint | Description |
|---|---|
POST /api/v2/auth/social/link | Lier un nouveau provider |
DELETE /api/v2/auth/social/unlink/:provider | Retirer un provider lie |
GET /api/v2/auth/social/accounts | Lister les providers lies |
Strategie de liaison de comptes (ordre de priorite)
- Recherche par
provider + providerUserId-- Compte social existant - Recherche par
firebaseUid-- Utilisateurs legacy Firebase (migration) - Recherche par
email-- Liaison au compte existant base sur l'email - 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');
}
}
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 :
| Etape | Endpoint | Description |
|---|---|---|
| 1 | POST /api/v2/account-auth/login | Validation des credentials |
| 2 | POST /api/v2/account-auth/select-profile | Selection du profil + tokens |
Autres endpoints du systeme :
| Endpoint | Description |
|---|---|
POST /api/v2/account-auth/register | Creer un nouveau compte |
POST /api/v2/account-auth/refresh | Rafraichir l'access token |
POST /api/v2/account-auth/logout | Revoquer la session |
POST /api/v2/account-auth/logout-all | Revoquer toutes les sessions |
GET /api/v2/account-auth/me | Compte courant |
GET /api/v2/account-auth/me/profiles | Profils 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
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).