fix: derive jwt signing key from users in a stable order (#4886)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
TowyTowy
2026-08-04 20:46:28 +02:00
committed by GitHub
parent b9df31356f
commit a9395e9f73
2 changed files with 58 additions and 1 deletions
+8 -1
View File
@@ -3,7 +3,9 @@ package auth
import (
"crypto/sha256"
"errors"
"maps"
"net/http"
"slices"
"time"
"github.com/go-chi/jwtauth/v5"
@@ -18,8 +20,13 @@ type simpleAuthContext struct {
var ErrInvalidCredentials = errors.New("invalid credentials")
func NewSimpleAuth(userDatabase UserDatabase, ttl time.Duration) *simpleAuthContext {
// Hash the users in a stable order. Ranging over the map directly makes the
// digest depend on Go's randomized map iteration order, so any users.yml with
// more than one user derives a different signing key on every start and
// silently invalidates every session on restart.
h := sha256.New()
for _, user := range userDatabase.Users {
for _, username := range slices.Sorted(maps.Keys(userDatabase.Users)) {
user := userDatabase.Users[username]
h.Write([]byte(user.Password))
h.Write([]byte(user.RolesConfigured))
}
+50
View File
@@ -0,0 +1,50 @@
package auth
import (
"testing"
"github.com/stretchr/testify/require"
)
// The JWT signing key is derived from the users in users.yml so that a token
// stays valid across restarts and is only invalidated when a password or role
// actually changes. Deriving it by ranging over the user map made the digest
// depend on Go's randomized map iteration order, so every restart of a
// multi-user setup rolled the key and logged everyone out.
func TestSimpleAuthSigningKeyIsStableAcrossRestarts(t *testing.T) {
users := UserDatabase{
Users: map[string]*User{
"alice": {Username: "alice", Password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", RolesConfigured: "all"},
"bob": {Username: "bob", Password: "$2a$11$bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RolesConfigured: "shell"},
"carol": {Username: "carol", Password: "$2a$11$cccccccccccccccccccccccccccccccccccccccccccccccccccccc", RolesConfigured: "actions"},
"dave": {Username: "dave", Password: "$2a$11$dddddddddddddddddddddddddddddddddddddddddddddddddddddd", RolesConfigured: "download"},
},
}
_, token, err := NewSimpleAuth(users, 0).tokenAuth.Encode(map[string]any{"username": "alice"})
require.NoError(t, err)
// Each iteration stands in for a Dozzle restart against an unchanged users.yml.
for i := range 50 {
if _, err := NewSimpleAuth(users, 0).tokenAuth.Decode(token); err != nil {
t.Fatalf("token issued before restart %d was rejected: %v", i+1, err)
}
}
}
// Changing a password or a role must still roll the key so old tokens stop working.
func TestSimpleAuthSigningKeyChangesWhenCredentialsChange(t *testing.T) {
users := UserDatabase{
Users: map[string]*User{
"alice": {Username: "alice", Password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", RolesConfigured: "all"},
"bob": {Username: "bob", Password: "$2a$11$bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RolesConfigured: "shell"},
},
}
_, token, err := NewSimpleAuth(users, 0).tokenAuth.Encode(map[string]any{"username": "alice"})
require.NoError(t, err)
users.Users["bob"].RolesConfigured = "shell,actions"
_, err = NewSimpleAuth(users, 0).tokenAuth.Decode(token)
require.Error(t, err)
}