2025-12-03 19:33:26 +00:00
|
|
|
//! Gestionnaire JWT avancé avec refresh tokens et rotation
|
|
|
|
|
//!
|
|
|
|
|
//! Ce module fournit une gestion complète des tokens JWT avec:
|
|
|
|
|
//! - Access tokens (courte durée)
|
|
|
|
|
//! - Refresh tokens (longue durée)
|
|
|
|
|
//! - Rotation automatique des tokens
|
|
|
|
|
//! - Blacklist des tokens révoqués
|
|
|
|
|
//! - Validation robuste avec métriques
|
|
|
|
|
|
|
|
|
|
use crate::config::SecurityConfig;
|
|
|
|
|
use crate::error::{ChatError, Result};
|
|
|
|
|
use chrono::{DateTime, Duration, Utc};
|
|
|
|
|
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
use sqlx::PgPool;
|
2025-12-03 19:33:26 +00:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
/// Claims pour les access tokens
|
|
|
|
|
/// MIGRATION UUID: user_id est maintenant String (UUID serialisé)
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct AccessTokenClaims {
|
|
|
|
|
/// ID de l'utilisateur (UUID en string)
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
#[serde(rename = "sub")]
|
2025-12-03 19:33:26 +00:00
|
|
|
pub user_id: String,
|
|
|
|
|
/// Nom d'utilisateur
|
|
|
|
|
pub username: String,
|
|
|
|
|
/// Rôle de l'utilisateur
|
|
|
|
|
pub role: String,
|
|
|
|
|
/// Type de token
|
|
|
|
|
pub token_type: String,
|
|
|
|
|
/// Audience
|
2025-12-06 16:21:59 +00:00
|
|
|
#[serde(deserialize_with = "deserialize_audience")]
|
|
|
|
|
pub aud: Vec<String>,
|
2025-12-03 19:33:26 +00:00
|
|
|
/// Issuer
|
|
|
|
|
pub iss: String,
|
|
|
|
|
/// Expiration
|
|
|
|
|
pub exp: usize,
|
|
|
|
|
/// Émis à
|
|
|
|
|
pub iat: usize,
|
|
|
|
|
/// JTI (JWT ID) pour la révocation
|
|
|
|
|
pub jti: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Claims pour les refresh tokens
|
|
|
|
|
/// MIGRATION UUID: user_id est maintenant String (UUID serialisé)
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct RefreshTokenClaims {
|
|
|
|
|
/// ID de l'utilisateur (UUID en string)
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
#[serde(rename = "sub")]
|
2025-12-03 19:33:26 +00:00
|
|
|
pub user_id: String,
|
|
|
|
|
/// Type de token
|
|
|
|
|
pub token_type: String,
|
|
|
|
|
/// Audience
|
2025-12-06 16:21:59 +00:00
|
|
|
#[serde(deserialize_with = "deserialize_audience")]
|
|
|
|
|
pub aud: Vec<String>,
|
2025-12-03 19:33:26 +00:00
|
|
|
/// Issuer
|
|
|
|
|
pub iss: String,
|
|
|
|
|
/// Expiration
|
|
|
|
|
pub exp: usize,
|
|
|
|
|
/// Émis à
|
|
|
|
|
pub iat: usize,
|
|
|
|
|
/// JTI (JWT ID) pour la révocation
|
|
|
|
|
pub jti: String,
|
|
|
|
|
/// Version de la famille de tokens
|
|
|
|
|
pub token_family: String,
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
fn deserialize_audience<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
|
|
|
|
|
where
|
|
|
|
|
D: serde::Deserializer<'de>,
|
|
|
|
|
{
|
|
|
|
|
struct AudienceVisitor;
|
|
|
|
|
|
|
|
|
|
impl<'de> serde::de::Visitor<'de> for AudienceVisitor {
|
|
|
|
|
type Value = Vec<String>;
|
|
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
|
|
|
formatter.write_str("a string or an array of strings")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn visit_str<E>(self, v: &str) -> std::result::Result<Self::Value, E>
|
|
|
|
|
where
|
|
|
|
|
E: serde::de::Error,
|
|
|
|
|
{
|
|
|
|
|
Ok(vec![v.to_owned()])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
|
|
|
|
|
where
|
|
|
|
|
A: serde::de::SeqAccess<'de>,
|
|
|
|
|
{
|
|
|
|
|
let mut res = Vec::new();
|
|
|
|
|
while let Some(el) = seq.next_element()? {
|
|
|
|
|
res.push(el);
|
|
|
|
|
}
|
|
|
|
|
Ok(res)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
deserializer.deserialize_any(AudienceVisitor)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:33:26 +00:00
|
|
|
/// Paire de tokens (access + refresh)
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct TokenPair {
|
|
|
|
|
pub access_token: String,
|
|
|
|
|
pub refresh_token: String,
|
|
|
|
|
pub expires_in: u64,
|
|
|
|
|
pub token_type: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Informations sur un token révoqué
|
|
|
|
|
/// MIGRATION UUID: user_id est maintenant String
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct RevokedToken {
|
|
|
|
|
pub jti: String,
|
|
|
|
|
pub user_id: String, // UUID as string
|
|
|
|
|
pub revoked_at: DateTime<Utc>,
|
|
|
|
|
pub reason: RevocationReason,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Raison de révocation d'un token
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub enum RevocationReason {
|
|
|
|
|
UserLogout,
|
|
|
|
|
TokenRefresh,
|
|
|
|
|
SecurityViolation,
|
|
|
|
|
AdminRevocation,
|
|
|
|
|
Expired,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gestionnaire JWT avec rotation et blacklist
|
|
|
|
|
pub struct JwtManager {
|
|
|
|
|
config: SecurityConfig,
|
|
|
|
|
encoding_key: EncodingKey,
|
|
|
|
|
decoding_key: DecodingKey,
|
|
|
|
|
validation: Validation,
|
|
|
|
|
/// Blacklist des tokens révoqués
|
|
|
|
|
revoked_tokens: Arc<RwLock<HashSet<String>>>,
|
|
|
|
|
/// Cache des familles de tokens actives
|
|
|
|
|
active_token_families: Arc<RwLock<HashSet<String>>>,
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
/// Pool de base de données optionnel pour récupérer les infos utilisateur
|
|
|
|
|
db_pool: Option<PgPool>,
|
2025-12-03 19:33:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl JwtManager {
|
|
|
|
|
/// Crée un nouveau gestionnaire JWT
|
|
|
|
|
pub fn new(config: SecurityConfig) -> Result<Self> {
|
|
|
|
|
let algorithm = match config.jwt_algorithm.as_str() {
|
|
|
|
|
"HS256" => Algorithm::HS256,
|
|
|
|
|
"HS384" => Algorithm::HS384,
|
|
|
|
|
"HS512" => Algorithm::HS512,
|
|
|
|
|
"RS256" => Algorithm::RS256,
|
|
|
|
|
"RS384" => Algorithm::RS384,
|
|
|
|
|
"RS512" => Algorithm::RS512,
|
|
|
|
|
_ => return Err(ChatError::configuration_error("Algorithme JWT invalide")),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let encoding_key = EncodingKey::from_secret(config.jwt_secret.as_bytes());
|
|
|
|
|
let decoding_key = DecodingKey::from_secret(config.jwt_secret.as_bytes());
|
|
|
|
|
|
|
|
|
|
let mut validation = Validation::new(algorithm);
|
|
|
|
|
validation.set_audience(&[&config.jwt_audience]);
|
|
|
|
|
validation.set_issuer(&[&config.jwt_issuer]);
|
|
|
|
|
validation.set_required_spec_claims(&["exp", "iat", "sub", "aud", "iss", "jti"]);
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
config,
|
|
|
|
|
encoding_key,
|
|
|
|
|
decoding_key,
|
|
|
|
|
validation,
|
|
|
|
|
revoked_tokens: Arc::new(RwLock::new(HashSet::new())),
|
|
|
|
|
active_token_families: Arc::new(RwLock::new(HashSet::new())),
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
db_pool: None,
|
2025-12-03 19:33:26 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
/// Crée un nouveau gestionnaire JWT avec un pool de base de données
|
|
|
|
|
pub fn with_pool(config: SecurityConfig, pool: PgPool) -> Result<Self> {
|
|
|
|
|
let mut manager = Self::new(config)?;
|
|
|
|
|
manager.db_pool = Some(pool);
|
|
|
|
|
Ok(manager)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:33:26 +00:00
|
|
|
/// Génère une paire de tokens (access + refresh)
|
|
|
|
|
/// MIGRATION UUID: user_id est maintenant String (UUID)
|
|
|
|
|
pub async fn generate_token_pair(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: String, // UUID as string
|
|
|
|
|
username: String,
|
|
|
|
|
role: String,
|
|
|
|
|
) -> Result<TokenPair> {
|
|
|
|
|
let now = Utc::now();
|
|
|
|
|
let access_exp = now + Duration::seconds(self.config.jwt_access_duration.as_secs() as i64);
|
|
|
|
|
let refresh_exp =
|
|
|
|
|
now + Duration::seconds(self.config.jwt_refresh_duration.as_secs() as i64);
|
|
|
|
|
|
|
|
|
|
// Générer des JTI uniques
|
|
|
|
|
let access_jti = Uuid::new_v4().to_string();
|
|
|
|
|
let refresh_jti = Uuid::new_v4().to_string();
|
|
|
|
|
let token_family = Uuid::new_v4().to_string();
|
|
|
|
|
|
|
|
|
|
// Claims pour access token
|
|
|
|
|
let access_claims = AccessTokenClaims {
|
|
|
|
|
user_id: user_id.clone(),
|
|
|
|
|
username: username.clone(),
|
|
|
|
|
role: role.clone(),
|
|
|
|
|
token_type: "access".to_string(),
|
2025-12-06 16:21:59 +00:00
|
|
|
aud: vec![self.config.jwt_audience.clone()],
|
2025-12-03 19:33:26 +00:00
|
|
|
iss: self.config.jwt_issuer.clone(),
|
|
|
|
|
exp: access_exp.timestamp() as usize,
|
|
|
|
|
iat: now.timestamp() as usize,
|
|
|
|
|
jti: access_jti.clone(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Claims pour refresh token
|
|
|
|
|
let refresh_claims = RefreshTokenClaims {
|
|
|
|
|
user_id: user_id.clone(),
|
|
|
|
|
token_type: "refresh".to_string(),
|
2025-12-06 16:21:59 +00:00
|
|
|
aud: vec![self.config.jwt_audience.clone()],
|
2025-12-03 19:33:26 +00:00
|
|
|
iss: self.config.jwt_issuer.clone(),
|
|
|
|
|
exp: refresh_exp.timestamp() as usize,
|
|
|
|
|
iat: now.timestamp() as usize,
|
|
|
|
|
jti: refresh_jti.clone(),
|
|
|
|
|
token_family: token_family.clone(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Encoder les tokens
|
|
|
|
|
let access_token =
|
|
|
|
|
encode(&Header::default(), &access_claims, &self.encoding_key).map_err(|e| {
|
|
|
|
|
ChatError::validation_error(&format!("Erreur encodage access token: {e}"))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let refresh_token = encode(&Header::default(), &refresh_claims, &self.encoding_key)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
ChatError::validation_error(&format!("Erreur encodage refresh token: {e}"))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// Enregistrer la famille de tokens comme active
|
|
|
|
|
{
|
|
|
|
|
let mut families = self.active_token_families.write().await;
|
|
|
|
|
families.insert(token_family);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!(
|
|
|
|
|
user_id = %user_id,
|
|
|
|
|
username = %username,
|
|
|
|
|
role = %role,
|
|
|
|
|
access_jti = %access_jti,
|
|
|
|
|
refresh_jti = %refresh_jti,
|
|
|
|
|
"🔐 Paire de tokens générée"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(TokenPair {
|
|
|
|
|
access_token,
|
|
|
|
|
refresh_token,
|
|
|
|
|
expires_in: self.config.jwt_access_duration.as_secs(),
|
|
|
|
|
token_type: "Bearer".to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Valide un access token
|
|
|
|
|
pub async fn validate_access_token(&self, token: &str) -> Result<AccessTokenClaims> {
|
|
|
|
|
// Vérifier si le token est dans la blacklist
|
|
|
|
|
{
|
|
|
|
|
let revoked = self.revoked_tokens.read().await;
|
|
|
|
|
if revoked.contains(token) {
|
|
|
|
|
return Err(ChatError::unauthorized("Token révoqué"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Décoder et valider le token
|
|
|
|
|
let token_data = decode::<AccessTokenClaims>(token, &self.decoding_key, &self.validation)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::warn!(error = %e, "❌ Échec validation access token");
|
|
|
|
|
ChatError::unauthorized("Token invalide")
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let claims = token_data.claims;
|
|
|
|
|
|
|
|
|
|
// Vérifier le type de token
|
|
|
|
|
if claims.token_type != "access" {
|
|
|
|
|
return Err(ChatError::unauthorized("Type de token invalide"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier l'expiration
|
|
|
|
|
let now = Utc::now().timestamp() as usize;
|
|
|
|
|
if claims.exp < now {
|
|
|
|
|
return Err(ChatError::unauthorized("Token expiré"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
user_id = %claims.user_id,
|
|
|
|
|
username = %claims.username,
|
|
|
|
|
jti = %claims.jti,
|
|
|
|
|
"✅ Access token validé"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(claims)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Valide un refresh token et génère une nouvelle paire
|
|
|
|
|
pub async fn refresh_tokens(&self, refresh_token: &str) -> Result<TokenPair> {
|
|
|
|
|
// Vérifier si le token est dans la blacklist
|
|
|
|
|
{
|
|
|
|
|
let revoked = self.revoked_tokens.read().await;
|
|
|
|
|
if revoked.contains(refresh_token) {
|
|
|
|
|
return Err(ChatError::unauthorized("Refresh token révoqué"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Décoder et valider le refresh token
|
|
|
|
|
let token_data =
|
|
|
|
|
decode::<RefreshTokenClaims>(refresh_token, &self.decoding_key, &self.validation)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
tracing::warn!(error = %e, "❌ Échec validation refresh token");
|
|
|
|
|
ChatError::unauthorized("Refresh token invalide")
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let claims = token_data.claims;
|
|
|
|
|
|
|
|
|
|
// Vérifier le type de token
|
|
|
|
|
if claims.token_type != "refresh" {
|
|
|
|
|
return Err(ChatError::unauthorized("Type de token invalide"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier l'expiration
|
|
|
|
|
let now = Utc::now().timestamp() as usize;
|
|
|
|
|
if claims.exp < now {
|
|
|
|
|
return Err(ChatError::unauthorized("Refresh token expiré"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier que la famille de tokens est toujours active
|
|
|
|
|
{
|
|
|
|
|
let families = self.active_token_families.read().await;
|
|
|
|
|
if !families.contains(&claims.token_family) {
|
|
|
|
|
return Err(ChatError::unauthorized("Famille de tokens révoquée"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Révocation de l'ancien refresh token
|
|
|
|
|
self.revoke_token(refresh_token, RevocationReason::TokenRefresh)
|
|
|
|
|
.await?;
|
|
|
|
|
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
// Récupérer les informations utilisateur depuis la DB
|
|
|
|
|
let (username, role) = if let Some(ref pool) = self.db_pool {
|
|
|
|
|
// Parser user_id depuis String vers Uuid
|
|
|
|
|
let user_uuid = Uuid::parse_str(&claims.user_id).map_err(|e| {
|
|
|
|
|
ChatError::validation_error(&format!("Invalid user UUID in token: {}", e))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// Récupérer username et role depuis la DB
|
|
|
|
|
let user_info: Option<(String, Option<String>)> = sqlx::query_as(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT username, role FROM users
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
"#,
|
|
|
|
|
)
|
|
|
|
|
.bind(user_uuid)
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| ChatError::from_sqlx_error("get_user_info_for_refresh", e))?
|
|
|
|
|
.map(|row: (String, Option<String>)| row);
|
|
|
|
|
|
|
|
|
|
match user_info {
|
|
|
|
|
Some((username, role_opt)) => {
|
|
|
|
|
let role = role_opt.unwrap_or_else(|| "user".to_string());
|
|
|
|
|
(username, role)
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
user_id = %claims.user_id,
|
|
|
|
|
"Utilisateur non trouvé dans la DB lors du refresh token, utilisation de valeurs par défaut"
|
|
|
|
|
);
|
|
|
|
|
// Fallback si utilisateur non trouvé (ne devrait pas arriver en production)
|
|
|
|
|
("user".to_string(), "user".to_string())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback si pas de pool DB (mode dégradé)
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
user_id = %claims.user_id,
|
|
|
|
|
"Pas de pool DB disponible, utilisation de valeurs par défaut pour refresh token"
|
|
|
|
|
);
|
|
|
|
|
("user".to_string(), "user".to_string())
|
|
|
|
|
};
|
2025-12-03 19:33:26 +00:00
|
|
|
|
|
|
|
|
// MIGRATION UUID: Cloner user_id avant de le move
|
|
|
|
|
let user_id_clone = claims.user_id.clone();
|
|
|
|
|
|
|
|
|
|
// Générer une nouvelle paire de tokens
|
|
|
|
|
let new_tokens = self
|
|
|
|
|
.generate_token_pair(claims.user_id, username, role)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
tracing::info!(
|
|
|
|
|
user_id = %user_id_clone,
|
|
|
|
|
old_jti = %claims.jti,
|
|
|
|
|
"🔄 Tokens rafraîchis"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(new_tokens)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Révoque un token
|
|
|
|
|
pub async fn revoke_token(&self, token: &str, reason: RevocationReason) -> Result<()> {
|
|
|
|
|
// Extraire le JTI du token (sans validation complète pour la révocation)
|
|
|
|
|
let parts: Vec<&str> = token.split('.').collect();
|
|
|
|
|
if parts.len() != 3 {
|
|
|
|
|
return Err(ChatError::validation_error("Format de token invalide"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Décoder le payload pour obtenir le JTI
|
|
|
|
|
let payload = parts[1];
|
|
|
|
|
let decoded =
|
|
|
|
|
base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, payload)
|
|
|
|
|
.map_err(|e| {
|
|
|
|
|
ChatError::validation_error(&format!("Erreur décodage payload: {e}"))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let claims: serde_json::Value = serde_json::from_slice(&decoded)
|
|
|
|
|
.map_err(|e| ChatError::validation_error(&format!("Erreur parsing claims: {e}")))?;
|
|
|
|
|
|
|
|
|
|
let jti = claims["jti"]
|
|
|
|
|
.as_str()
|
|
|
|
|
.ok_or_else(|| ChatError::validation_error("JTI manquant"))?;
|
|
|
|
|
|
|
|
|
|
let user_id = claims["sub"].as_str().unwrap_or("unknown").to_string();
|
|
|
|
|
|
|
|
|
|
// Ajouter à la blacklist
|
|
|
|
|
{
|
|
|
|
|
let mut revoked = self.revoked_tokens.write().await;
|
|
|
|
|
revoked.insert(token.to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Si c'est un refresh token, révoquer toute la famille
|
|
|
|
|
if let Some(token_type) = claims["token_type"].as_str() {
|
|
|
|
|
if token_type == "refresh" {
|
|
|
|
|
if let Some(family) = claims["token_family"].as_str() {
|
|
|
|
|
let mut families = self.active_token_families.write().await;
|
|
|
|
|
families.remove(family);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tracing::info!(
|
|
|
|
|
jti = %jti,
|
|
|
|
|
user_id = %user_id,
|
|
|
|
|
reason = ?reason,
|
|
|
|
|
"🚫 Token révoqué"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Révoque tous les tokens d'un utilisateur
|
|
|
|
|
/// MIGRATION UUID: user_id est String
|
|
|
|
|
pub async fn revoke_user_tokens(&self, user_id: String) -> Result<()> {
|
|
|
|
|
// En production, on devrait maintenir une liste des familles de tokens par utilisateur
|
|
|
|
|
// Pour l'instant, on nettoie toutes les familles actives
|
|
|
|
|
let mut families = self.active_token_families.write().await;
|
|
|
|
|
families.clear();
|
|
|
|
|
|
|
|
|
|
tracing::info!(user_id = %user_id, "🚫 Tous les tokens de l'utilisateur révoqués");
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Nettoie les tokens expirés de la blacklist
|
|
|
|
|
pub async fn cleanup_expired_tokens(&self) -> Result<usize> {
|
|
|
|
|
// En production, on devrait vérifier l'expiration de chaque token
|
|
|
|
|
// Pour l'instant, on limite la taille de la blacklist
|
|
|
|
|
let mut revoked = self.revoked_tokens.write().await;
|
|
|
|
|
let initial_size = revoked.len();
|
|
|
|
|
|
|
|
|
|
if revoked.len() > 10000 {
|
|
|
|
|
// Garder seulement les 5000 plus récents (simulation)
|
|
|
|
|
let tokens: Vec<String> = revoked.iter().take(5000).cloned().collect();
|
|
|
|
|
revoked.clear();
|
|
|
|
|
revoked.extend(tokens);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cleaned = initial_size - revoked.len();
|
|
|
|
|
|
|
|
|
|
if cleaned > 0 {
|
|
|
|
|
tracing::debug!(cleaned = %cleaned, "🧹 Tokens expirés nettoyés de la blacklist");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(cleaned)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Vérifie si un token est révoqué
|
|
|
|
|
pub async fn is_token_revoked(&self, token: &str) -> bool {
|
|
|
|
|
let revoked = self.revoked_tokens.read().await;
|
|
|
|
|
revoked.contains(token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Obtient les statistiques des tokens
|
|
|
|
|
pub async fn get_token_stats(&self) -> TokenStats {
|
|
|
|
|
let revoked_count = self.revoked_tokens.read().await.len();
|
|
|
|
|
let active_families = self.active_token_families.read().await.len();
|
|
|
|
|
|
|
|
|
|
TokenStats {
|
|
|
|
|
revoked_tokens: revoked_count,
|
|
|
|
|
active_token_families: active_families,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Statistiques des tokens
|
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
|
|
|
pub struct TokenStats {
|
|
|
|
|
pub revoked_tokens: usize,
|
|
|
|
|
pub active_token_families: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Fonction utilitaire pour extraire le token du header Authorization
|
|
|
|
|
pub fn extract_token_from_header(auth_header: &str) -> Result<&str> {
|
|
|
|
|
if !auth_header.starts_with("Bearer ") {
|
|
|
|
|
return Err(ChatError::unauthorized("Format d'autorisation invalide"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(&auth_header[7..]) // Retirer "Bearer "
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
fn create_test_config() -> SecurityConfig {
|
|
|
|
|
SecurityConfig {
|
|
|
|
|
jwt_secret: "test_secret_key_32_chars_minimum_required".to_string(),
|
|
|
|
|
jwt_access_duration: Duration::from_secs(3600), // 1 heure
|
|
|
|
|
jwt_refresh_duration: Duration::from_secs(86400), // 24 heures
|
|
|
|
|
jwt_algorithm: "HS256".to_string(),
|
|
|
|
|
jwt_audience: "test".to_string(),
|
|
|
|
|
jwt_issuer: "test".to_string(),
|
|
|
|
|
enable_2fa: false,
|
|
|
|
|
totp_window: 1,
|
|
|
|
|
content_filtering: false,
|
|
|
|
|
password_min_length: 8,
|
|
|
|
|
bcrypt_cost: 12,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_generate_and_validate_tokens() {
|
|
|
|
|
let config = create_test_config();
|
|
|
|
|
let manager = JwtManager::new(config).unwrap();
|
|
|
|
|
|
|
|
|
|
// UUID de test
|
|
|
|
|
let test_user_id = Uuid::new_v4().to_string();
|
|
|
|
|
|
|
|
|
|
// Générer une paire de tokens
|
|
|
|
|
let tokens = manager
|
|
|
|
|
.generate_token_pair(
|
|
|
|
|
test_user_id.clone(),
|
|
|
|
|
"testuser".to_string(),
|
|
|
|
|
"user".to_string(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Valider l'access token
|
|
|
|
|
let claims = manager
|
|
|
|
|
.validate_access_token(&tokens.access_token)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(claims.user_id, test_user_id);
|
|
|
|
|
assert_eq!(claims.username, "testuser");
|
|
|
|
|
assert_eq!(claims.role, "user");
|
|
|
|
|
assert_eq!(claims.token_type, "access");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_token_revocation() {
|
|
|
|
|
let config = create_test_config();
|
|
|
|
|
let manager = JwtManager::new(config).unwrap();
|
|
|
|
|
|
|
|
|
|
let test_user_id = Uuid::new_v4().to_string();
|
|
|
|
|
|
|
|
|
|
// Générer des tokens
|
|
|
|
|
let tokens = manager
|
|
|
|
|
.generate_token_pair(test_user_id, "testuser".to_string(), "user".to_string())
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Valider avant révocation
|
|
|
|
|
assert!(manager
|
|
|
|
|
.validate_access_token(&tokens.access_token)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok());
|
|
|
|
|
|
|
|
|
|
// Révoquer le token
|
|
|
|
|
manager
|
|
|
|
|
.revoke_token(&tokens.access_token, RevocationReason::UserLogout)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Vérifier que le token est révoqué
|
|
|
|
|
assert!(manager
|
|
|
|
|
.validate_access_token(&tokens.access_token)
|
|
|
|
|
.await
|
|
|
|
|
.is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_token_refresh() {
|
|
|
|
|
let config = create_test_config();
|
|
|
|
|
let manager = JwtManager::new(config).unwrap();
|
|
|
|
|
|
|
|
|
|
let test_user_id = Uuid::new_v4().to_string();
|
|
|
|
|
|
|
|
|
|
// Générer des tokens
|
|
|
|
|
let tokens = manager
|
|
|
|
|
.generate_token_pair(
|
|
|
|
|
test_user_id.clone(),
|
|
|
|
|
"testuser".to_string(),
|
|
|
|
|
"user".to_string(),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
// Rafraîchir les tokens
|
|
|
|
|
let new_tokens = manager.refresh_tokens(&tokens.refresh_token).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Vérifier que les nouveaux tokens fonctionnent
|
|
|
|
|
let claims = manager
|
|
|
|
|
.validate_access_token(&new_tokens.access_token)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(claims.user_id, test_user_id);
|
|
|
|
|
|
|
|
|
|
// Vérifier que l'ancien refresh token est révoqué
|
|
|
|
|
assert!(manager.refresh_tokens(&tokens.refresh_token).await.is_err());
|
|
|
|
|
}
|
|
|
|
|
}
|