This is an automated email from the ASF dual-hosted git repository.

piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new 8b5a775e6 fix(server): hash passwords before persisting to state log 
(#2724)
8b5a775e6 is described below

commit 8b5a775e63d9c9f0bf5b5828e87e50db44609667
Author: Piotr Gankiewicz <[email protected]>
AuthorDate: Thu Feb 12 06:57:22 2026 +0100

    fix(server): hash passwords before persisting to state log (#2724)
    
    The shard and HTTP handlers for CreateUser and ChangePassword
    stored plaintext passwords in the state log instead of Argon2
    hashes. On restart, state replay loaded the plaintext as a
    password hash, causing PasswordHash::new() to panic with
    "Failed to parse password hash: PhcStringField".
    
    The root user was unaffected because its password was hashed
    before being written to the state entry.
    
    Fixes #2720
---
 core/integration/tests/data_integrity/mod.rs       |  1 +
 .../verify_user_login_after_restart.rs             | 65 ++++++++++++++++++++++
 core/server/src/http/users.rs                      |  6 +-
 core/server/src/shard/handlers.rs                  |  7 ++-
 4 files changed, 75 insertions(+), 4 deletions(-)

diff --git a/core/integration/tests/data_integrity/mod.rs 
b/core/integration/tests/data_integrity/mod.rs
index 5cf911c94..742e3128e 100644
--- a/core/integration/tests/data_integrity/mod.rs
+++ b/core/integration/tests/data_integrity/mod.rs
@@ -17,3 +17,4 @@
  */
 
 mod verify_after_server_restart;
+mod verify_user_login_after_restart;
diff --git 
a/core/integration/tests/data_integrity/verify_user_login_after_restart.rs 
b/core/integration/tests/data_integrity/verify_user_login_after_restart.rs
new file mode 100644
index 000000000..d6015f420
--- /dev/null
+++ b/core/integration/tests/data_integrity/verify_user_login_after_restart.rs
@@ -0,0 +1,65 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use iggy::prelude::*;
+use integration::harness::{USER_PASSWORD, create_user, login_user};
+use integration::iggy_harness;
+
+#[iggy_harness(test_client_transport = [Tcp, Http, Quic, WebSocket])]
+async fn should_login_non_root_user_after_restart(harness: &mut TestHarness) {
+    let root_client = harness.root_client().await.unwrap();
+    create_user(&root_client, "testuser").await;
+
+    let client = harness.new_client().await.unwrap();
+    login_user(&client, "testuser").await;
+    drop(client);
+    drop(root_client);
+
+    harness.restart_server().await.unwrap();
+
+    let root_client = harness.root_client().await.unwrap();
+    let users = root_client.get_users().await.unwrap();
+    assert_eq!(users.len(), 2, "Expected root + testuser after restart");
+    drop(root_client);
+
+    let client = harness.new_client().await.unwrap();
+    login_user(&client, "testuser").await;
+}
+
+#[iggy_harness(test_client_transport = [Tcp, Http, Quic, WebSocket])]
+async fn should_login_after_password_change_and_restart(harness: &mut 
TestHarness) {
+    let root_client = harness.root_client().await.unwrap();
+    create_user(&root_client, "testuser").await;
+
+    let new_password = "new_secret_password";
+    let user_id = Identifier::named("testuser").unwrap();
+    root_client
+        .change_password(&user_id, USER_PASSWORD, new_password)
+        .await
+        .unwrap();
+
+    let client = harness.new_client().await.unwrap();
+    client.login_user("testuser", new_password).await.unwrap();
+    drop(client);
+    drop(root_client);
+
+    harness.restart_server().await.unwrap();
+
+    let client = harness.new_client().await.unwrap();
+    client.login_user("testuser", new_password).await.unwrap();
+}
diff --git a/core/server/src/http/users.rs b/core/server/src/http/users.rs
index 8e4dcd1b9..b2b0c1fba 100644
--- a/core/server/src/http/users.rs
+++ b/core/server/src/http/users.rs
@@ -298,7 +298,11 @@ async fn change_password(
         })?;
 
     {
-        let entry_command = EntryCommand::ChangePassword(command);
+        let entry_command = EntryCommand::ChangePassword(ChangePassword {
+            user_id: command.user_id,
+            current_password: "".into(),
+            new_password: crypto::hash_password(&command.new_password),
+        });
         let future = SendWrapper::new(
             state
                 .shard
diff --git a/core/server/src/shard/handlers.rs 
b/core/server/src/shard/handlers.rs
index 53756d5f8..f48e3f91c 100644
--- a/core/server/src/shard/handlers.rs
+++ b/core/server/src/shard/handlers.rs
@@ -26,6 +26,7 @@ use crate::{
             message::{ShardMessage, ShardRequest, ShardRequestPayload},
         },
     },
+    streaming::utils::crypto,
     tcp::{
         connection_handler::{ConnectionAction, handle_connection, 
handle_error},
         tcp_listener::cleanup_connection,
@@ -406,7 +407,7 @@ async fn handle_request(
 
             let command = iggy_common::create_user::CreateUser {
                 username,
-                password,
+                password: crypto::hash_password(&password),
                 status,
                 permissions,
             };
@@ -560,8 +561,8 @@ async fn handle_request(
 
             let command = iggy_common::change_password::ChangePassword {
                 user_id,
-                current_password,
-                new_password,
+                current_password: "".into(),
+                new_password: crypto::hash_password(&new_password),
             };
             shard
                 .state

Reply via email to