From cf38b49e4db2e839f4d84c3b8caa65aca2bf3a48 Mon Sep 17 00:00:00 2001 From: RamXX Date: Thu, 19 Mar 2026 18:12:37 -0700 Subject: [PATCH] Add `openfang auth hash-password` CLI command and startup warning Addresses review feedback: - Add `openfang auth hash-password` subcommand so users can generate Argon2id hashes after upgrading (the command referenced in docs). - Emit a tracing::warn at daemon startup when auth is enabled but the password_hash is not in Argon2id format, so users know why login fails. --- crates/openfang-api/src/server.rs | 9 ++++++++ crates/openfang-cli/src/main.rs | 34 +++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/crates/openfang-api/src/server.rs b/crates/openfang-api/src/server.rs index fa65bbc9..6e65b412 100644 --- a/crates/openfang-api/src/server.rs +++ b/crates/openfang-api/src/server.rs @@ -104,6 +104,15 @@ pub async fn build_router( .allow_headers(tower_http::cors::Any) }; + // Warn if dashboard auth is enabled but the password hash is not Argon2id. + let ph = &state.kernel.config.auth.password_hash; + if state.kernel.config.auth.enabled && !ph.is_empty() && !ph.starts_with("$argon2") { + tracing::warn!( + "Dashboard auth password_hash is not in Argon2id format. \ + Login will fail. Regenerate with: openfang auth hash-password" + ); + } + // Trim whitespace so `api_key = ""` or `api_key = " "` both disable auth. let api_key = state.kernel.config.api_key.trim().to_string(); let auth_state = crate::middleware::AuthState { diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index 7fbdabf4..e9c09b12 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -237,6 +237,9 @@ enum Commands { #[arg(long)] json: bool, }, + /// Dashboard authentication [*]. + #[command(subcommand)] + Auth(AuthCommands), /// Security tools and audit trail [*]. #[command(subcommand)] Security(SecurityCommands), @@ -679,6 +682,12 @@ enum CronCommands { }, } +#[derive(Subcommand)] +enum AuthCommands { + /// Generate an Argon2id password hash for dashboard authentication. + HashPassword, +} + #[derive(Subcommand)] enum SecurityCommands { /// Show security status summary. @@ -1057,6 +1066,9 @@ fn main() { Some(Commands::Sessions { agent, json }) => cmd_sessions(agent.as_deref(), json), Some(Commands::Logs { lines, follow }) => cmd_logs(lines, follow), Some(Commands::Health { json }) => cmd_health(json), + Some(Commands::Auth(sub)) => match sub { + AuthCommands::HashPassword => cmd_auth_hash_password(), + }, Some(Commands::Security(sub)) => match sub { SecurityCommands::Status { json } => cmd_security_status(json), SecurityCommands::Audit { limit, json } => cmd_security_audit(limit, json), @@ -5959,6 +5971,28 @@ fn cmd_health(json: bool) { } } +fn cmd_auth_hash_password() { + let password = prompt_input("Enter password: "); + if password.is_empty() { + ui::error("Empty password."); + std::process::exit(1); + } + let confirm = prompt_input("Confirm password: "); + if password != confirm { + ui::error("Passwords do not match."); + std::process::exit(1); + } + let hash = openfang_api::session_auth::hash_password(&password); + println!(); + ui::success("Argon2id hash generated. Add this to your config.toml:"); + println!(); + println!(" [auth]"); + println!(" enabled = true"); + println!(" password_hash = \"{}\"", hash); + println!(); + ui::hint("Restart the daemon after updating config.toml"); +} + fn cmd_security_status(json: bool) { let base = require_daemon("security status"); let client = daemon_client();