Files
quoter/src/auth.rs
Untone 3ff469c8a1
Some checks failed
Deploy on push / deploy (push) Failing after 4s
connection-pool-fix
2025-09-22 01:23:16 +03:00

305 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use actix_web::error::ErrorInternalServerError;
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
use log::{info, warn};
use redis::{AsyncCommands, aio::MultiplexedConnection};
use serde::{Deserialize, Serialize};
use std::{error::Error, time::Duration};
// Структуры для JWT токенов
#[derive(Debug, Deserialize)]
struct TokenClaims {
user_id: String,
username: Option<String>,
exp: Option<usize>,
iat: Option<usize>,
}
// Структура для данных пользователя из Redis сессии
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Author {
pub user_id: String,
pub username: Option<String>,
pub token_type: Option<String>,
pub created_at: Option<String>,
pub last_activity: Option<String>,
pub auth_data: Option<String>,
pub device_info: Option<String>,
}
/// Декодирует JWT токен и извлекает claims с проверкой истечения
fn decode_jwt_token(token: &str) -> Result<TokenClaims, Box<dyn Error>> {
// В реальном приложении здесь должен быть настоящий секретный ключ
let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "your-secret-key".to_string());
let key = DecodingKey::from_secret(secret.as_ref());
let mut validation = Validation::new(Algorithm::HS256);
validation.validate_exp = true; // Включаем проверку истечения срока действия
match decode::<TokenClaims>(token, &key, &validation) {
Ok(token_data) => {
let claims = token_data.claims;
// Дополнительная проверка exp если поле присутствует
if let Some(exp) = claims.exp {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
if exp < current_time {
warn!("JWT token expired: exp={}, current={}", exp, current_time);
return Err(Box::new(std::io::Error::other("Token expired")));
}
info!("JWT token valid until: {} (current: {})", exp, current_time);
}
info!(
"Successfully decoded and validated JWT token for user: {}",
claims.user_id
);
Ok(claims)
}
Err(e) => {
warn!("Failed to decode JWT token: {}", e);
Err(Box::new(e))
}
}
}
/// Извлекает токен из HTTP запроса (поддерживает Bearer, X-Session-Token, Cookie)
pub fn extract_token_from_request(req: &actix_web::HttpRequest) -> Option<String> {
// 1. Bearer токен в Authorization header
if let Some(auth_header) = req.headers().get("authorization") {
if let Ok(auth_str) = auth_header.to_str() {
if let Some(stripped) = auth_str.strip_prefix("Bearer ") {
return Some(stripped.trim().to_string());
}
}
}
// 2. Кастомный заголовок X-Session-Token
if let Some(session_token) = req.headers().get("x-session-token") {
if let Ok(token_str) = session_token.to_str() {
return Some(token_str.trim().to_string());
}
}
// 3. Cookie session_token (для веб-приложений)
if let Some(cookie_header) = req.headers().get("cookie") {
if let Ok(cookie_str) = cookie_header.to_str() {
for cookie in cookie_str.split(';') {
let cookie = cookie.trim();
if let Some(stripped) = cookie.strip_prefix("session_token=") {
return Some(stripped.to_string());
}
}
}
}
None
}
/// Безопасная валидация токена с проверкой Redis сессий
pub async fn secure_token_validation(
token: &str,
mut redis: Option<&mut MultiplexedConnection>,
timeout: Duration,
) -> Result<Author, Box<dyn Error>> {
// Базовая проверка формата токена
if token.is_empty() || token.len() < 10 {
return Err(Box::new(std::io::Error::other("Invalid token format")));
}
// 1. Проверяем JWT структуру и подпись
let claims = decode_jwt_token(token)?;
let user_id = &claims.user_id;
info!("JWT token validated for user: {}", user_id);
// 2. Проверяем существование сессии в Redis
let session_exists = if let Some(ref mut redis) = redis {
let session_key = format!("session:{}:{}", user_id, token);
match tokio::time::timeout(timeout, redis.exists(&session_key)).await {
Ok(Ok(exists)) => exists,
Ok(Err(e)) => {
warn!("Redis error checking session: {}", e);
false
}
Err(_) => {
warn!("Redis timeout checking session");
false
}
}
} else {
warn!("⚠️ Redis not available, skipping session validation");
false
};
if !session_exists {
info!("Session not found in Redis for user: {}", user_id);
// В соответствии с руководством, можем продолжить с JWT-only данными
// или вернуть ошибку в зависимости от политики безопасности
}
// 3. Проверяем TTL сессии если она существует
let ttl = if session_exists && redis.is_some() {
let session_key = format!("session:{}:{}", user_id, token);
if let Some(ref mut redis) = redis {
match tokio::time::timeout(timeout, redis.ttl(&session_key)).await {
Ok(Ok(ttl_value)) => {
if ttl_value <= 0 {
return Err(Box::new(std::io::Error::other("Session expired")));
}
ttl_value
}
Ok(Err(e)) => {
warn!("Redis error getting TTL: {}", e);
-1
}
Err(_) => {
warn!("Redis timeout getting TTL");
-1
}
}
} else {
-1
}
} else {
-1
};
// 4. Обновляем last_activity если сессия активна
if session_exists && redis.is_some() {
let current_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let session_key = format!("session:{}:{}", user_id, token);
if let Some(redis) = redis {
let _: Result<(), _> = tokio::time::timeout(
timeout,
redis.hset(&session_key, "last_activity", current_time.to_string()),
)
.await
.map_err(|_| warn!("Redis timeout updating last_activity"))
.unwrap_or(Ok(()))
.map_err(|e| warn!("Failed to update last_activity: {}", e));
}
info!("Updated last_activity for session: {}", user_id);
}
// Создаем объект Author с расширенными данными
let author = Author {
user_id: user_id.clone(),
username: claims.username.clone(),
token_type: Some("jwt".to_string()),
created_at: claims.iat.map(|ts| ts.to_string()),
last_activity: Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string(),
),
auth_data: if session_exists {
Some(format!("redis_session_ttl:{}", ttl))
} else {
Some("jwt_only".to_string())
},
device_info: None,
};
info!(
"Successfully validated token for user: {} (session_exists: {})",
user_id, session_exists
);
Ok(author)
}
/// Универсальная функция аутентификации для всех handlers
/// Извлекает токен из запроса и выполняет полную валидацию
pub async fn authenticate_request(
req: &actix_web::HttpRequest,
redis: Option<&mut MultiplexedConnection>,
timeout: Duration,
) -> Result<Author, actix_web::Error> {
// Извлекаем токен из запроса (поддерживает Bearer, X-Session-Token, Cookie)
let token = extract_token_from_request(req).ok_or_else(|| {
warn!("No authorization token provided");
actix_web::error::ErrorUnauthorized("Authorization token required")
})?;
// Безопасная валидация токена с проверкой Redis сессий
secure_token_validation(&token, redis, timeout)
.await
.map_err(|e| {
warn!("Token validation failed: {}", e);
actix_web::error::ErrorUnauthorized("Invalid or expired token")
})
}
/// Универсальная функция аутентификации с использованием connection pool
/// Автоматически получает и возвращает соединения из пула
pub async fn authenticate_request_with_pool(
req: &actix_web::HttpRequest,
app_state: &crate::app_state::AppState,
) -> Result<Author, actix_web::Error> {
// Извлекаем токен из запроса
let token = extract_token_from_request(req).ok_or_else(|| {
warn!("No authorization token provided");
actix_web::error::ErrorUnauthorized("Authorization token required")
})?;
// Получаем соединение из пула
match app_state.get_redis_connection().await {
Ok(mut conn) => {
// Валидируем токен с Redis соединением
let result =
secure_token_validation(&token, Some(&mut conn), app_state.request_timeout).await;
// Возвращаем соединение в пул
app_state.return_redis_connection(conn).await;
result.map_err(|e| {
warn!("Token validation failed: {}", e);
actix_web::error::ErrorUnauthorized("Invalid or expired token")
})
}
Err(_) => {
// Fallback на JWT-only валидацию если Redis недоступен
warn!("Redis pool unavailable, falling back to JWT-only validation");
secure_token_validation(&token, None, app_state.request_timeout)
.await
.map_err(|e| {
warn!("Token validation failed: {}", e);
actix_web::error::ErrorUnauthorized("Invalid or expired token")
})
}
}
}
/// Сохраняет имя файла в Redis для пользователя
pub async fn user_added_file(
redis: Option<&mut MultiplexedConnection>,
user_id: &str,
filename: &str,
) -> Result<(), actix_web::Error> {
let Some(redis) = redis else {
log::warn!(
"⚠️ Redis not available, skipping file tracking for user {}",
user_id
);
return Ok(());
};
redis
.sadd::<&str, &str, ()>(user_id, filename)
.await
.map_err(|_| ErrorInternalServerError(format!("Failed to save {} in Redis", filename)))?; // Добавляем имя файла в набор пользователя
Ok(())
}