package auth import ( "crypto/hmac" "crypto/sha256" "encoding/json" "net/http" "net/http/httptest" "os" "strings" "testing" "ds2api/internal/config" ) // stubStore implements AdminConfigReader for testing without pulling in the // full config store. We intentionally use a struct (not a mock) so the test // exercises the real auth code paths. type stubStore struct { passwordHash string expireHours int validAfter int64 } func (s stubStore) AdminPasswordHash() string { return s.passwordHash } func (s stubStore) AdminJWTExpireHours() int { return s.expireHours } func (s stubStore) AdminJWTValidAfterUnix() int64 { return s.validAfter } func TestEffectiveAdminKeyNeverReturnsHardcodedDefault(t *testing.T) { // Security: ensure the historical "admin" default is gone. If this test // ever fails, it means a regression reintroduced a publicly-known default // credential that would let anyone log in to deployments without // DS2API_ADMIN_KEY set. t.Setenv("DS2API_ADMIN_KEY", "") t.Setenv("DS2API_CONFIG_JSON", "") stub := stubStore{} if got := effectiveAdminKey(stub); got != "" { t.Fatalf("effectiveAdminKey with no env and no password hash must be empty, got %q", got) } if got := AdminKey(); got != "" { t.Fatalf("AdminKey() with no env must be empty, got %q", got) } } func TestVerifyAdminCredentialRejectsEmptyAndDefaultAdmin(t *testing.T) { // Security: with no admin key configured and no password hash, login // MUST be impossible — even if an attacker tries "admin" or empty string. t.Setenv("DS2API_ADMIN_KEY", "") t.Setenv("DS2API_CONFIG_JSON", "") stub := stubStore{} for _, candidate := range []string{"", "admin", "password", "123456", "default"} { if VerifyAdminCredential(candidate, stub) { t.Fatalf("VerifyAdminCredential must reject %q when no credentials configured", candidate) } } } func TestVerifyAdminCredentialRejectsKnownDefaultAdminWithEnvSet(t *testing.T) { // Security: when DS2API_ADMIN_KEY is set to a strong value, the literal // "admin" must NOT authenticate. t.Setenv("DS2API_ADMIN_KEY", "a-strong-and-random-key-12345") t.Setenv("DS2API_CONFIG_JSON", "") stub := stubStore{} if VerifyAdminCredential("admin", stub) { t.Fatal("VerifyAdminCredential must reject literal \"admin\" when env key is set to something else") } if !VerifyAdminCredential("a-strong-and-random-key-12345", stub) { t.Fatal("VerifyAdminCredential must accept the configured env key") } } func TestJWTSecretNotHardcodedAdminWhenUnconfigured(t *testing.T) { // Security: when no DS2API_JWT_SECRET, no password hash, and no // DS2API_ADMIN_KEY are configured, the JWT signing secret must NOT be // the historical hardcoded "admin". A process-local random secret is // used instead so attackers cannot forge tokens. t.Setenv("DS2API_ADMIN_KEY", "") t.Setenv("DS2API_JWT_SECRET", "") t.Setenv("DS2API_CONFIG_JSON", "") secret := jwtSecret(nil) if secret == "admin" { t.Fatal("jwtSecret must not fall back to hardcoded \"admin\"") } if secret == "" { t.Fatal("jwtSecret must be non-empty so token signing is non-trivial") } // The fallback secret must be process-stable (same value across calls). if secret != jwtSecret(nil) { t.Fatal("jwtSecret fallback must be process-stable so minted tokens verify") } } func TestJWTForgedWithAdminSecretFailsVerification(t *testing.T) { // Security: an attacker who knows the historical default "admin" cannot // forge a token that verifies when no credentials are configured, because // the actual signing secret is a process-local random value. t.Setenv("DS2API_ADMIN_KEY", "") t.Setenv("DS2API_JWT_SECRET", "") // Forge a token signed with the historical "admin" secret. header := map[string]any{"alg": "HS256", "typ": "JWT"} payload := map[string]any{ "iat": 0, "exp": 9999999999, "role": "admin", } h, _ := json.Marshal(header) p, _ := json.Marshal(payload) msg := rawB64Encode(h) + "." + rawB64Encode(p) mac := hmac.New(sha256.New, []byte("admin")) _, _ = mac.Write([]byte(msg)) forgedSig := mac.Sum(nil) forged := msg + "." + rawB64Encode(forgedSig) if _, err := VerifyJWT(forged); err == nil { t.Fatal("forged token signed with hardcoded \"admin\" secret must NOT verify") } } func TestAdminLoginDisabledWithoutCredentials(t *testing.T) { // Security: end-to-end check that VerifyAdminRequestWithStore rejects // Bearer tokens when no credentials are configured, regardless of what // string the attacker supplies. t.Setenv("DS2API_ADMIN_KEY", "") t.Setenv("DS2API_CONFIG_JSON", "") stub := stubStore{} for _, token := range []string{"admin", "", "Bearer admin", "anything"} { req := httptest.NewRequest(http.MethodGet, "/admin/config", nil) req.Header.Set("Authorization", "Bearer "+token) if err := VerifyAdminRequestWithStore(req, stub); err == nil { t.Fatalf("VerifyAdminRequestWithStore must reject token %q when no credentials configured", token) } } } func TestAdminLoginWithConfiguredPasswordHash(t *testing.T) { // Regression: ensure password-hash based login still works after the // default-removal change. t.Setenv("DS2API_ADMIN_KEY", "") t.Setenv("DS2API_JWT_SECRET", "") stub := stubStore{passwordHash: HashAdminPassword("super-secret-pw")} if !VerifyAdminCredential("super-secret-pw", stub) { t.Fatal("VerifyAdminCredential must accept the configured password") } if VerifyAdminCredential("wrong-password", stub) { t.Fatal("VerifyAdminCredential must reject wrong password") } // JWT secret must derive from the password hash, not the empty admin key. secret := jwtSecret(stub) if secret != strings.TrimSpace(stub.passwordHash) { t.Fatalf("jwtSecret must be the password hash when no env secret set, got %q", secret) } } func TestAdminLoginWithEnvKey(t *testing.T) { // Regression: ensure env-key based login still works after the change. t.Setenv("DS2API_ADMIN_KEY", "env-key-12345") t.Setenv("DS2API_JWT_SECRET", "") stub := stubStore{} if !VerifyAdminCredential("env-key-12345", stub) { t.Fatal("VerifyAdminCredential must accept the configured env key") } if VerifyAdminCredential("env-key-99999", stub) { t.Fatal("VerifyAdminCredential must reject wrong env key") } } func TestAdminLoginWithJWTSecretEnv(t *testing.T) { // Regression: ensure an explicit JWT secret takes precedence and works // for both signing and verifying. t.Setenv("DS2API_ADMIN_KEY", "env-key-12345") t.Setenv("DS2API_JWT_SECRET", "explicit-jwt-secret") stub := stubStore{} if jwtSecret(stub) != "explicit-jwt-secret" { t.Fatalf("jwtSecret must use explicit env value, got %q", jwtSecret(stub)) } token, err := CreateJWTWithStore(1, stub) if err != nil { t.Fatalf("CreateJWTWithStore failed: %v", err) } if _, err := VerifyJWTWithStore(token, stub); err != nil { t.Fatalf("VerifyJWTWithStore failed: %v", err) } } // usingRealStoreHelper wraps a minimal config.Store so we exercise the same // store code paths used in production. We avoid pulling in the full server. func TestUsingRealStoreNoCredentials(t *testing.T) { if os.Getenv("DS2API_RUN_REAL_STORE_TEST") == "" { t.Skip("skipping real-store test; set DS2API_RUN_REAL_STORE_TEST=1 to run") } t.Setenv("DS2API_ADMIN_KEY", "") t.Setenv("DS2API_JWT_SECRET", "") t.Setenv("DS2API_CONFIG_JSON", "{}") store := config.LoadStore() if !UsingDefaultAdminKey(store) { t.Fatal("UsingDefaultAdminKey must be true when nothing is configured") } if VerifyAdminCredential("admin", store) { t.Fatal("VerifyAdminCredential must reject \"admin\" with no credentials configured") } }