feat: auto-persist JWT secret, remove setup token requirement
- JWT secret auto-generates and persists to <STORAGE_PATH>/.jwt_secret - Remove setup token: first admin setup is open until system initialized - Fix schema.sql: move CREATE EXTENSION pg_trgm/ltree to top - Update login UI and auth.js to remove setup token fields
This commit is contained in:
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+1
-2
@@ -49,13 +49,12 @@ pub struct RegisterDto {
|
||||
}
|
||||
|
||||
/// DTO for the one-time initial admin setup endpoint (`/api/setup`).
|
||||
/// Requires the setup token printed to the server log on first boot.
|
||||
/// Available only when the system is not yet initialized (no admin exists).
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct SetupAdminDto {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
pub setup_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+83
-33
@@ -605,44 +605,94 @@ impl AppConfig {
|
||||
|
||||
// Auth configuration
|
||||
if let Ok(jwt_secret) = env::var("OXICLOUD_JWT_SECRET") {
|
||||
// SECURITY: Validate JWT secret minimum entropy (RFC 7518 §3.2
|
||||
// recommends ≥256 bits for HS256). Panic on dangerously short
|
||||
// secrets, warn on sub-optimal ones.
|
||||
let len = jwt_secret.len();
|
||||
if config.features.enable_auth && len < 16 {
|
||||
panic!(
|
||||
"FATAL: OXICLOUD_JWT_SECRET is dangerously short ({} bytes). \
|
||||
Minimum: 32 bytes (256 bits) for HS256. \
|
||||
Generate a secure secret with: openssl rand -hex 32",
|
||||
len
|
||||
);
|
||||
} else if config.features.enable_auth && len < 32 {
|
||||
tracing::warn!("==========================================================");
|
||||
tracing::warn!(
|
||||
"OXICLOUD_JWT_SECRET is only {} bytes — recommended minimum is 32 (256 bits).",
|
||||
len
|
||||
);
|
||||
tracing::warn!("Generate a stronger secret with: openssl rand -hex 32");
|
||||
tracing::warn!("==========================================================");
|
||||
if !jwt_secret.is_empty() {
|
||||
// SECURITY: Validate JWT secret minimum entropy (RFC 7518 §3.2
|
||||
// recommends ≥256 bits for HS256). Panic on dangerously short
|
||||
// secrets, warn on sub-optimal ones.
|
||||
let len = jwt_secret.len();
|
||||
if config.features.enable_auth && len < 16 {
|
||||
panic!(
|
||||
"FATAL: OXICLOUD_JWT_SECRET is dangerously short ({} bytes). \
|
||||
Minimum: 32 bytes (256 bits) for HS256. \
|
||||
Generate a secure secret with: openssl rand -hex 32",
|
||||
len
|
||||
);
|
||||
} else if config.features.enable_auth && len < 32 {
|
||||
tracing::warn!("==========================================================");
|
||||
tracing::warn!(
|
||||
"OXICLOUD_JWT_SECRET is only {} bytes — recommended minimum is 32 (256 bits).",
|
||||
len
|
||||
);
|
||||
tracing::warn!("Generate a stronger secret with: openssl rand -hex 32");
|
||||
tracing::warn!("==========================================================");
|
||||
}
|
||||
config.auth.jwt_secret = jwt_secret;
|
||||
}
|
||||
config.auth.jwt_secret = jwt_secret;
|
||||
}
|
||||
|
||||
// SECURITY: Generate ephemeral secret when none is provided
|
||||
// SECURITY: Auto-persist JWT secret to storage so it survives restarts.
|
||||
// Priority: env var > persisted file > generate new.
|
||||
if config.features.enable_auth && config.auth.jwt_secret.is_empty() {
|
||||
// Generate a random secret for this session and warn loudly
|
||||
use rand_core::{OsRng, RngCore};
|
||||
let mut key = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut key);
|
||||
let generated_secret: String = key.iter().map(|b| format!("{:02x}", b)).collect();
|
||||
config.auth.jwt_secret = generated_secret;
|
||||
let secret_file = config.storage_path.join(".jwt_secret");
|
||||
|
||||
tracing::warn!("==========================================================");
|
||||
tracing::warn!("OXICLOUD_JWT_SECRET is not set.");
|
||||
tracing::warn!("A random secret has been generated for this session.");
|
||||
tracing::warn!("All tokens will be INVALIDATED on restart.");
|
||||
tracing::warn!("Set OXICLOUD_JWT_SECRET env var for production use.");
|
||||
tracing::warn!("==========================================================");
|
||||
if secret_file.exists() {
|
||||
// Read persisted secret from previous run
|
||||
match std::fs::read_to_string(&secret_file) {
|
||||
Ok(persisted) => {
|
||||
let persisted = persisted.trim().to_string();
|
||||
if persisted.len() >= 32 {
|
||||
config.auth.jwt_secret = persisted;
|
||||
tracing::info!(
|
||||
"JWT secret loaded from {}",
|
||||
secret_file.display()
|
||||
);
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Persisted JWT secret too short ({}B), regenerating",
|
||||
persisted.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to read {}: {}", secret_file.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Still empty → generate and persist
|
||||
if config.auth.jwt_secret.is_empty() {
|
||||
use rand_core::{OsRng, RngCore};
|
||||
let mut key = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut key);
|
||||
let generated_secret: String =
|
||||
key.iter().map(|b| format!("{:02x}", b)).collect();
|
||||
|
||||
// Persist to storage volume so it survives container restarts
|
||||
if let Err(e) = std::fs::write(&secret_file, &generated_secret) {
|
||||
tracing::error!(
|
||||
"Failed to persist JWT secret to {}: {}. \
|
||||
Tokens will be invalidated on restart!",
|
||||
secret_file.display(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
// Restrict file permissions (owner-only read/write)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(
|
||||
&secret_file,
|
||||
std::fs::Permissions::from_mode(0o600),
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
"JWT secret auto-generated and persisted to {}",
|
||||
secret_file.display()
|
||||
);
|
||||
}
|
||||
|
||||
config.auth.jwt_secret = generated_secret;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(access_token_expiry) =
|
||||
|
||||
Regular → Executable
+5
-18
@@ -612,7 +612,7 @@ impl AppServiceFactory {
|
||||
path_resolver: None,
|
||||
webdav_lock_store:
|
||||
crate::infrastructure::services::webdav_lock_service::create_webdav_lock_store(),
|
||||
setup_token: None,
|
||||
|
||||
};
|
||||
|
||||
// 9b. Wire admin settings service when auth is available
|
||||
@@ -663,24 +663,14 @@ impl AppServiceFactory {
|
||||
|
||||
app_state.admin_settings_service = Some(admin_svc.clone());
|
||||
|
||||
// 9b-2. Generate one-time setup token if system is NOT yet initialized
|
||||
// 9b-2. Log whether system needs first-time admin setup
|
||||
if !admin_svc.is_system_initialized().await {
|
||||
use rand_core::{OsRng, RngCore};
|
||||
let mut token_bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut token_bytes);
|
||||
let token = hex::encode(token_bytes);
|
||||
|
||||
tracing::warn!("╔══════════════════════════════════════════════════════════╗");
|
||||
tracing::warn!("║ SYSTEM NOT INITIALIZED — first admin setup required ║");
|
||||
tracing::warn!("║ ║");
|
||||
tracing::warn!("║ POST /api/setup with this one-time token: ║");
|
||||
tracing::warn!("║ {} ║", token);
|
||||
tracing::warn!("║ ║");
|
||||
tracing::warn!("║ This token is valid until the server restarts or the ║");
|
||||
tracing::warn!("║ first admin is created. Keep it secret! ║");
|
||||
tracing::warn!("║ Open the web UI to create the first admin account. ║");
|
||||
tracing::warn!("║ The setup page is available until an admin is created. ║");
|
||||
tracing::warn!("╚══════════════════════════════════════════════════════════╝");
|
||||
|
||||
app_state.setup_token = Some(token);
|
||||
} else {
|
||||
tracing::info!("System already initialized — setup endpoint disabled");
|
||||
}
|
||||
@@ -903,10 +893,7 @@ pub struct AppState {
|
||||
Option<Arc<crate::infrastructure::services::path_resolver_service::PathResolverService>>,
|
||||
pub webdav_lock_store:
|
||||
Arc<crate::infrastructure::services::webdav_lock_service::WebDavLockStore>,
|
||||
/// One-time setup token generated on startup when the system is not yet
|
||||
/// initialized. Printed to the server log so the operator can create the
|
||||
/// first admin user via `POST /api/setup`.
|
||||
pub setup_token: Option<String>,
|
||||
|
||||
}
|
||||
|
||||
// All AppState construction is done via struct literal in build_app_state().
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user