From 0f75f7cf357a9aa7abef1f50bdd7252b3f01ce98 Mon Sep 17 00:00:00 2001 From: bsaurusrex <82356519+bsaurusrex@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:12:03 +0800 Subject: [PATCH] fix: reject duplicate usernames in the user configuration When the same username appeared more than once (e.g. an env user and a users-file user, or twice in one list) the duplicates were accepted silently and the first occurrence won. An admin editing a second entry to change a password or add TOTP would not see it take effect. GetUsers now fails startup with an error naming the duplicated username instead of silently ignoring the later entry. Refs #685 Co-Authored-By: Claude Opus 5.5 --- internal/utils/user_utils.go | 5 +++++ internal/utils/user_utils_test.go | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/internal/utils/user_utils.go b/internal/utils/user_utils.go index d94b3a20d..636d61c1f 100644 --- a/internal/utils/user_utils.go +++ b/internal/utils/user_utils.go @@ -11,6 +11,7 @@ import ( func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) { var users []model.LocalUser + seen := make(map[string]struct{}) if len(usersStr) == 0 { return nil, nil @@ -24,6 +25,10 @@ func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttribute if err != nil { return nil, err } + if _, dup := seen[parsed.Username]; dup { + return nil, fmt.Errorf("duplicate user %q, each username must be configured only once", parsed.Username) + } + seen[parsed.Username] = struct{}{} if attrs, ok := userAttributes[parsed.Username]; ok { parsed.Attributes = attrs } diff --git a/internal/utils/user_utils_test.go b/internal/utils/user_utils_test.go index 973be9183..bde8bde81 100644 --- a/internal/utils/user_utils_test.go +++ b/internal/utils/user_utils_test.go @@ -10,6 +10,20 @@ import ( "github.com/tinyauthapp/tinyauth/internal/utils" ) +func TestGetUsersRejectsDuplicates(t *testing.T) { + hash := "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G" + noAttrs := map[string]model.UserAttributes{} + + // The same username twice is rejected instead of silently keeping the first + _, err := utils.GetUsers([]string{"alice:" + hash, "alice:" + hash}, "", noAttrs) + assert.ErrorContains(t, err, `duplicate user "alice"`) + + // Distinct usernames still load + users, err := utils.GetUsers([]string{"alice:" + hash, "bob:" + hash}, "", noAttrs) + assert.NoError(t, err) + assert.Len(t, *users, 2) +} + func TestGetUsers(t *testing.T) { tmpDir := t.TempDir()