fix(config): harden default JWT secret

Replace known development JWT secret placeholders with an ephemeral runtime secret during config loading.

Return LoadInfo so startup can warn when token persistence depends on a stable configured secret.

Update config docs and tests for default, legacy placeholder, custom, env, and empty secret behavior.
This commit is contained in:
2026-07-05 13:25:44 +08:00
parent 7b6587a951
commit 5eeb37389b
6 changed files with 131 additions and 16 deletions
+59 -7
View File
@@ -9,7 +9,7 @@ import (
func TestDefaults(t *testing.T) {
v := New()
cfg, err := Load(v, "")
cfg, info, err := Load(v, "")
if err != nil {
t.Fatal(err)
}
@@ -36,6 +36,16 @@ func TestDefaults(t *testing.T) {
}
})
}
if !info.EphemeralJWTSecret {
t.Fatal("expected default jwt.secret to be replaced with an ephemeral secret")
}
if cfg.JWT.Secret == DefaultJWTSecret {
t.Fatal("default jwt.secret was not replaced")
}
if cfg.JWT.Secret == "" {
t.Fatal("ephemeral jwt.secret is empty")
}
}
func TestFromYAML(t *testing.T) {
@@ -67,10 +77,13 @@ jwt:
}
v := New()
cfg, err := Load(v, path)
cfg, info, err := Load(v, path)
if err != nil {
t.Fatal(err)
}
if info.EphemeralJWTSecret {
t.Fatal("custom jwt.secret should not be replaced")
}
if cfg.Server.Host != "127.0.0.1" {
t.Errorf("server.host = %q, want %q", cfg.Server.Host, "127.0.0.1")
@@ -102,10 +115,13 @@ func TestEnvOverride(t *testing.T) {
t.Setenv("MYGO_DATABASE_SQLITE_PATH", "/env/path/db.sqlite")
v := New()
cfg, err := Load(v, "")
cfg, info, err := Load(v, "")
if err != nil {
t.Fatal(err)
}
if info.EphemeralJWTSecret {
t.Fatal("env jwt.secret should not be replaced")
}
if cfg.Server.Port != 8080 {
t.Errorf("server.port = %d, want %d", cfg.Server.Port, 8080)
@@ -137,7 +153,7 @@ server:
t.Setenv("MYGO_SERVER_PORT", "9999")
v := New()
cfg, err := Load(v, path)
cfg, _, err := Load(v, path)
if err != nil {
t.Fatal(err)
}
@@ -164,7 +180,7 @@ server:
}
v := New()
_, err := Load(v, path)
_, _, err := Load(v, path)
if err == nil {
t.Fatal("expected error for port 0, got nil")
}
@@ -183,7 +199,7 @@ jwt:
}
v := New()
_, err := Load(v, path)
_, _, err := Load(v, path)
if err == nil {
t.Fatal("expected error for empty jwt.secret, got nil")
}
@@ -191,12 +207,48 @@ jwt:
func TestExplicitConfigFileNotFound(t *testing.T) {
v := New()
_, err := Load(v, "/nonexistent/path/config.yaml")
_, _, err := Load(v, "/nonexistent/path/config.yaml")
if err == nil {
t.Fatal("expected error when explicitly specifying a nonexistent config file")
}
}
func TestWeakJWTSecretsAreReplaced(t *testing.T) {
tests := []string{
DefaultJWTSecret,
"change-me-in-production",
"changeme-in-production",
}
for _, secret := range tests {
t.Run(secret, func(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")
yaml := "jwt:\n secret: " + secret + "\n"
if err := os.WriteFile(path, []byte(yaml), 0644); err != nil {
t.Fatal(err)
}
v := New()
cfg, info, err := Load(v, path)
if err != nil {
t.Fatal(err)
}
if !info.EphemeralJWTSecret {
t.Fatal("expected weak jwt.secret to be replaced")
}
if cfg.JWT.Secret == secret {
t.Fatal("weak jwt.secret was not replaced")
}
if cfg.JWT.Secret == "" {
t.Fatal("ephemeral jwt.secret is empty")
}
})
}
}
func TestJWTConfigAccessDuration(t *testing.T) {
j := JWTConfig{AccessTTL: 15 * time.Minute}
if j.AccessTTL != 15*time.Minute {