fix(chat): ensure WebSocket auth token from query or cookie
- Chat server: accept token from ?token= or access_token cookie (httpOnly) - Frontend: append token to WS URL when available (TokenStorage)
This commit is contained in:
parent
98f6db3a1d
commit
a53dc358e6
4 changed files with 42 additions and 12 deletions
|
|
@ -29,6 +29,7 @@ interface WebSocketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
import { logger } from '@/utils/logger';
|
import { logger } from '@/utils/logger';
|
||||||
|
import { TokenStorage } from '@/services/tokenStorage';
|
||||||
|
|
||||||
class WebSocketServiceImpl implements WebSocketService {
|
class WebSocketServiceImpl implements WebSocketService {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
|
|
@ -45,26 +46,26 @@ class WebSocketServiceImpl implements WebSocketService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORRECTION: Le serveur Rust expose le WebSocket sur /ws
|
// CORRECTION: Le serveur Rust expose le WebSocket sur /ws et requiert token en query (v0.101)
|
||||||
const wsUrl = (() => {
|
const baseUrl = (() => {
|
||||||
const url = import.meta.env.VITE_WS_URL;
|
const url = import.meta.env.VITE_WS_URL;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
if (import.meta.env.PROD) {
|
if (import.meta.env.PROD) {
|
||||||
throw new Error('VITE_WS_URL must be defined in production');
|
throw new Error('VITE_WS_URL must be defined in production');
|
||||||
}
|
}
|
||||||
// Fallback uniquement en développement - utiliser chemin relatif par défaut
|
|
||||||
// SECURITY: Ne jamais utiliser localhost hardcodé, utiliser chemin relatif
|
|
||||||
// Convertir chemin relatif en URL WebSocket
|
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
return `${protocol}//${window.location.host}/ws`;
|
return `${protocol}//${window.location.host}/ws`;
|
||||||
}
|
}
|
||||||
// Convertir URL relative en URL absolue pour WebSocket
|
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith('/')) {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
return `${protocol}//${window.location.host}${url}`;
|
return `${protocol}//${window.location.host}${url}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
})();
|
})();
|
||||||
|
const token = TokenStorage.getAccessToken();
|
||||||
|
const wsUrl = token
|
||||||
|
? `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}`
|
||||||
|
: baseUrl;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,13 @@ REDIS_ENABLE=true
|
||||||
RABBITMQ_URL=amqp://veza:password@localhost:5672/%2f
|
RABBITMQ_URL=amqp://veza:password@localhost:5672/%2f
|
||||||
RABBITMQ_ENABLE=true
|
RABBITMQ_ENABLE=true
|
||||||
|
|
||||||
|
# ClamAV (v0.101 - virus scanning for uploads)
|
||||||
|
# Dev (Docker): ENABLE_CLAMAV=true, CLAMAV_REQUIRED=false (accepts uploads if ClamAV slow/down)
|
||||||
|
# Prod: CLAMAV_REQUIRED=true (startup fails if ClamAV unavailable)
|
||||||
|
ENABLE_CLAMAV=true
|
||||||
|
CLAMAV_REQUIRED=false
|
||||||
|
CLAMAV_ADDRESS=clamav:3310
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
JWT_SECRET=your-secret-key-here-change-in-production
|
JWT_SECRET=your-secret-key-here-change-in-production
|
||||||
CORS_ALLOWED_ORIGINS=http://localhost:3000
|
CORS_ALLOWED_ORIGINS=http://localhost:3000
|
||||||
|
|
|
||||||
|
|
@ -322,9 +322,10 @@ async fn main() -> Result<(), ChatError> {
|
||||||
let ws_state_clone = ws_state.clone();
|
let ws_state_clone = ws_state.clone();
|
||||||
move |ws: WebSocketUpgrade,
|
move |ws: WebSocketUpgrade,
|
||||||
query: Query<HashMap<String, String>>,
|
query: Query<HashMap<String, String>>,
|
||||||
|
headers: axum::http::HeaderMap,
|
||||||
request_id: Option<Extension<Uuid>>|
|
request_id: Option<Extension<Uuid>>|
|
||||||
async move {
|
async move {
|
||||||
websocket_handler(ws, query, State(ws_state_clone), request_id).await
|
websocket_handler(ws, query, headers, State(ws_state_clone), request_id).await
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
use axum::extract::ws::{Message, WebSocket};
|
use axum::extract::ws::{Message, WebSocket};
|
||||||
use axum::extract::{Query, State, WebSocketUpgrade};
|
use axum::extract::{Query, State, WebSocketUpgrade};
|
||||||
|
use axum::http::HeaderMap;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
|
@ -45,6 +46,27 @@ pub struct WebSocketState {
|
||||||
pub rate_limiter: Arc<crate::security::RateLimiter>,
|
pub rate_limiter: Arc<crate::security::RateLimiter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract access token from query param (?token=) or Cookie header (access_token)
|
||||||
|
fn extract_token(params: &HashMap<String, String>, headers: &HeaderMap) -> Option<String> {
|
||||||
|
if let Some(t) = params.get("token") {
|
||||||
|
return Some(t.clone());
|
||||||
|
}
|
||||||
|
if let Some(cookie_header) = headers.get(axum::http::header::COOKIE) {
|
||||||
|
if let Ok(cookie_str) = cookie_header.to_str() {
|
||||||
|
for part in cookie_str.split(';') {
|
||||||
|
let part = part.trim();
|
||||||
|
if part.starts_with("access_token=") {
|
||||||
|
let value = part.trim_start_matches("access_token=").trim();
|
||||||
|
if !value.is_empty() {
|
||||||
|
return Some(value.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Handler principal pour les connexions WebSocket
|
/// Handler principal pour les connexions WebSocket
|
||||||
///
|
///
|
||||||
/// Cette fonction gère la mise à niveau de la connexion HTTP vers WebSocket
|
/// Cette fonction gère la mise à niveau de la connexion HTTP vers WebSocket
|
||||||
|
|
@ -53,8 +75,8 @@ pub struct WebSocketState {
|
||||||
pub async fn websocket_handler(
|
pub async fn websocket_handler(
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
Query(params): Query<HashMap<String, String>>,
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
headers: HeaderMap,
|
||||||
State(state): State<WebSocketState>,
|
State(state): State<WebSocketState>,
|
||||||
// FIX #13: Extraire le request_id depuis les extensions (ajouté par request_id_middleware)
|
|
||||||
request_id: Option<axum::extract::Extension<Uuid>>,
|
request_id: Option<axum::extract::Extension<Uuid>>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// FIX #13: Extraire le request_id pour la corrélation
|
// FIX #13: Extraire le request_id pour la corrélation
|
||||||
|
|
@ -69,16 +91,15 @@ pub async fn websocket_handler(
|
||||||
async move {
|
async move {
|
||||||
info!(request_id = %request_id, "🔌 Nouvelle connexion WebSocket demandée");
|
info!(request_id = %request_id, "🔌 Nouvelle connexion WebSocket demandée");
|
||||||
|
|
||||||
let token = match params.get("token") {
|
let token = match extract_token(¶ms, &headers) {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
None => {
|
None => {
|
||||||
error!(request_id = %request_id, "❌ Token manquant");
|
error!(request_id = %request_id, "❌ Token manquant (query ?token= ou cookie access_token)");
|
||||||
return (StatusCode::UNAUTHORIZED, "Missing token").into_response();
|
return (StatusCode::UNAUTHORIZED, "Missing token").into_response();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate token
|
match state.jwt_manager.validate_access_token(&token).await {
|
||||||
match state.jwt_manager.validate_access_token(token).await {
|
|
||||||
Ok(claims) => {
|
Ok(claims) => {
|
||||||
info!(
|
info!(
|
||||||
request_id = %request_id,
|
request_id = %request_id,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue