perf: findings 6.1, 6.2, 2.6 — async Argon2, moka cache, full streaming migration

- 6.1: PasswordHasherPort now async_trait with spawn_blocking for Argon2
- 6.2: OIDC pending maps migrated from std::sync::Mutex to moka::sync::Cache with TTL
- 2.6: All file download paths migrated to 64KB streaming (get_file_stream / read_blob_stream)
  - WOPI, dedup, batch ZIP, file_retrieval_service consumers migrated
  - WebDAV COPY uses zero-copy dedup (copy_file)
  - Removed dead code: get_file_content, get_file_mmap, read_blob, read_blob_bytes
    from traits, impls, stubs, and mocks (18 files touched)
This commit is contained in:
Diocrafts
2026-02-23 00:51:46 +01:00
parent b501c4052b
commit 85908311dc
18 changed files with 235 additions and 305 deletions
@@ -10,29 +10,23 @@ use crate::common::config::OidcConfig;
use crate::common::errors::{DomainError, ErrorKind};
use crate::domain::entities::session::Session;
use crate::domain::entities::user::{User, UserRole};
use std::collections::HashMap;
use moka::sync::Cache;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;
use std::time::Instant;
/// Maximum age for pending OIDC flows (10 minutes)
const OIDC_FLOW_TTL_SECS: u64 = 600;
/// Maximum age for pending one-time token codes (60 seconds)
const OIDC_TOKEN_TTL_SECS: u64 = 60;
use std::time::Duration;
/// Tracks a pending OIDC authorization flow (CSRF + PKCE + nonce)
#[derive(Clone)]
struct PendingOidcFlow {
created_at: Instant,
pkce_verifier: String,
nonce: String,
}
/// Tracks a pending one-time token exchange after successful OIDC callback
#[derive(Clone)]
struct PendingOidcToken {
auth_response: AuthResponseDto,
created_at: Instant,
}
/// Interior state for OIDC — protected by RwLock for hot-reload.
@@ -54,10 +48,12 @@ pub struct AuthApplicationService {
/// Path to the storage directory, used for disk-space–aware quota calculation
storage_path: PathBuf,
oidc: RwLock<OidcState>,
/// Pending OIDC authorization flows keyed by state token (CSRF + PKCE + nonce)
pending_oidc_flows: Mutex<HashMap<String, PendingOidcFlow>>,
/// Pending one-time token codes for secure token delivery after OIDC callback
pending_oidc_tokens: Mutex<HashMap<String, PendingOidcToken>>,
/// Pending OIDC authorization flows keyed by state token (CSRF + PKCE + nonce).
/// Auto-expires after 10 minutes via moka TTL; max 10 000 entries for DoS protection.
pending_oidc_flows: Cache<String, PendingOidcFlow>,
/// Pending one-time token codes for secure token delivery after OIDC callback.
/// Auto-expires after 60 seconds via moka TTL; max 10 000 entries for DoS protection.
pending_oidc_tokens: Cache<String, PendingOidcToken>,
}
impl AuthApplicationService {
@@ -79,8 +75,14 @@ impl AuthApplicationService {
service: None,
config: None,
}),
pending_oidc_flows: Mutex::new(HashMap::new()),
pending_oidc_tokens: Mutex::new(HashMap::new()),
pending_oidc_flows: Cache::builder()
.max_capacity(10_000)
.time_to_live(Duration::from_secs(600))
.build(),
pending_oidc_tokens: Cache::builder()
.max_capacity(10_000)
.time_to_live(Duration::from_secs(60))
.build(),
}
}
@@ -300,7 +302,7 @@ impl AuthApplicationService {
}
// Hash the password using the infrastructure service
let password_hash = self.password_hasher.hash_password(&dto.password)?;
let password_hash = self.password_hasher.hash_password(&dto.password).await?;
// Create user with the pre-generated hash
let user = User::new(dto.username.clone(), dto.email, password_hash, role, quota).map_err(
@@ -346,7 +348,8 @@ impl AuthApplicationService {
// Verify password using the injected hasher
let is_valid = self
.password_hasher
.verify_password(&dto.password, user.password_hash())?;
.verify_password(&dto.password, user.password_hash())
.await?;
if !is_valid {
return Err(DomainError::new(
@@ -502,7 +505,8 @@ impl AuthApplicationService {
// Verify current password using the injected hasher
let is_valid = self
.password_hasher
.verify_password(&dto.current_password, user.password_hash())?;
.verify_password(&dto.current_password, user.password_hash())
.await?;
if !is_valid {
return Err(DomainError::new(
@@ -522,7 +526,7 @@ impl AuthApplicationService {
}
// Hash new password and update user
let new_hash = self.password_hasher.hash_password(&dto.new_password)?;
let new_hash = self.password_hasher.hash_password(&dto.new_password).await?;
user.update_password_hash(new_hash);
// Save updated user
@@ -641,13 +645,7 @@ impl AuthApplicationService {
let password_hash = self
.password_hasher
.hash_password(&dto.password)
.map_err(|e| {
DomainError::new(
ErrorKind::InternalError,
"User",
format!("Error hashing password: {}", e),
)
})?;
.await?;
// Create the new admin user
let user = User::new(
@@ -753,7 +751,7 @@ impl AuthApplicationService {
});
// Hash password
let password_hash = self.password_hasher.hash_password(&dto.password)?;
let password_hash = self.password_hasher.hash_password(&dto.password).await?;
// Create domain entity
let user =
@@ -806,7 +804,7 @@ impl AuthApplicationService {
"Password must be at least 8 characters long".to_string(),
));
}
let hash = self.password_hasher.hash_password(new_password)?;
let hash = self.password_hasher.hash_password(new_password).await?;
self.user_storage.change_password(user_id, &hash).await
}
@@ -917,22 +915,14 @@ impl AuthApplicationService {
base64_url_encode(&hash)
};
// Store pending flow
{
let mut flows = self.pending_oidc_flows.lock().unwrap();
// Cleanup expired entries
let now = Instant::now();
flows.retain(|_, f| now.duration_since(f.created_at).as_secs() < OIDC_FLOW_TTL_SECS);
flows.insert(
state_token.clone(),
PendingOidcFlow {
created_at: now,
pkce_verifier,
nonce: nonce.clone(),
},
);
}
// Store pending flow (auto-expires after 10 min via moka TTL)
self.pending_oidc_flows.insert(
state_token.clone(),
PendingOidcFlow {
pkce_verifier,
nonce: nonce.clone(),
},
);
// Build authorization URL with state, nonce, and PKCE challenge
let authorize_url = oidc
@@ -952,28 +942,15 @@ impl AuthApplicationService {
/// issue internal tokens, and return a one-time exchange code.
pub async fn oidc_callback(&self, code: &str, state: &str) -> Result<String, DomainError> {
// 0. Validate CSRF state and retrieve PKCE verifier + nonce
let (pkce_verifier, nonce) = {
let mut flows = self.pending_oidc_flows.lock().unwrap();
let flow = flows.remove(state).ok_or_else(|| {
tracing::warn!("OIDC callback with invalid/expired state token");
DomainError::new(
ErrorKind::AccessDenied, "OIDC",
"Invalid or expired OIDC state — possible CSRF attack. Please try logging in again.",
)
})?;
// Check TTL
if Instant::now().duration_since(flow.created_at).as_secs() >= OIDC_FLOW_TTL_SECS {
tracing::warn!("OIDC callback with expired state token");
return Err(DomainError::new(
ErrorKind::AccessDenied,
"OIDC",
"OIDC authorization flow expired. Please try logging in again.",
));
}
(flow.pkce_verifier, flow.nonce)
};
// (entry is auto-expired by moka TTL — remove returns None if expired)
let flow = self.pending_oidc_flows.remove(state).ok_or_else(|| {
tracing::warn!("OIDC callback with invalid/expired state token");
DomainError::new(
ErrorKind::AccessDenied, "OIDC",
"Invalid or expired OIDC state — possible CSRF attack. Please try logging in again.",
)
})?;
let (pkce_verifier, nonce) = (flow.pkce_verifier, flow.nonce);
// Clone the Arc and config out of the RwLock so we don't hold the lock across await points
let (oidc, oidc_config) = {
@@ -1178,20 +1155,11 @@ impl AuthApplicationService {
OsRng.fill_bytes(&mut code_bytes);
let exchange_code = hex::encode(code_bytes);
{
let mut tokens = self.pending_oidc_tokens.lock().unwrap();
// Cleanup expired entries
let now = Instant::now();
tokens.retain(|_, t| now.duration_since(t.created_at).as_secs() < OIDC_TOKEN_TTL_SECS);
tokens.insert(
exchange_code.clone(),
PendingOidcToken {
auth_response,
created_at: now,
},
);
}
// Store auth response (auto-expires after 60 s via moka TTL)
self.pending_oidc_tokens.insert(
exchange_code.clone(),
PendingOidcToken { auth_response },
);
tracing::info!("OIDC login successful, one-time exchange code generated");
@@ -1199,10 +1167,9 @@ impl AuthApplicationService {
}
/// Exchange a one-time code for the authentication tokens.
/// The code is single-use and expires after 60 seconds.
/// The code is single-use and expires after 60 seconds (moka TTL).
pub fn exchange_oidc_token(&self, one_time_code: &str) -> Result<AuthResponseDto, DomainError> {
let mut tokens = self.pending_oidc_tokens.lock().unwrap();
let pending = tokens.remove(one_time_code).ok_or_else(|| {
let pending = self.pending_oidc_tokens.remove(one_time_code).ok_or_else(|| {
DomainError::new(
ErrorKind::AccessDenied,
"OIDC",
@@ -1210,15 +1177,6 @@ impl AuthApplicationService {
)
})?;
// Check TTL
if Instant::now().duration_since(pending.created_at).as_secs() >= OIDC_TOKEN_TTL_SECS {
return Err(DomainError::new(
ErrorKind::AccessDenied,
"OIDC",
"Exchange code expired. Please try logging in again.",
));
}
Ok(pending.auth_response)
}