diff --git a/hub-server/.env.example b/hub-server/.env.example index e9b80d291..1c1ebf028 100644 --- a/hub-server/.env.example +++ b/hub-server/.env.example @@ -41,7 +41,7 @@ AGENTHUB_AGENT_TEAM_MAX_TEAM_RUN_BUDGET_USAGE_PCT=95 # d. Save the returned client_id and client_secret below # 3. If TokenDance ID is NOT running, use the seed SQL: # sqlite3 ../tokendance-id/data/tokendance.db < scripts/seed-tokendance-client.sql -# The seed uses client_id=agenthub-desktop with secret=agenthub-dev-secret-change-me +# The seed uses client_id=agenthub-desktop; copy the generated secret from the setup script output. AGENTHUB_TOKENDANCE_ID_ISSUER_URL=http://localhost:3000 # AGENTHUB_TOKENDANCE_ID_JWKS_URI= ← auto-derived from issuer_url + /oidc/jwks diff --git a/hub-server/internal/config/config_test.go b/hub-server/internal/config/config_test.go index 9dc82a07d..1c1fb7cc7 100644 --- a/hub-server/internal/config/config_test.go +++ b/hub-server/internal/config/config_test.go @@ -735,6 +735,72 @@ func TestValidateTokenDanceIDRequiresRedirectURI(t *testing.T) { } } +func TestValidateTokenDanceIDRejectsShortClientSecret(t *testing.T) { + cfg := &Config{ + DB: DBConfig{ + Host: "localhost", + Port: 5432, + User: "agenthub", + Name: "agenthub", + MaxOpenConns: 2, + MaxIdleConns: 1, + ConnMaxLifetime: 30 * time.Minute, + ConnMaxIdleTime: 5 * time.Minute, + }, + Redis: RedisConfig{Host: "localhost", Port: 6379}, + JWT: JWTConfig{ + Secret: "strong-agenthub-secret-padded-to-minimum-32-chars..", + }, + TokenDanceID: TokenDanceIDConfig{ + IssuerURL: "https://id.example", + ClientID: "agenthub-client", + ClientSecret: "short-but-present-secret", + RedirectURI: "http://127.0.0.1/callback", + }, + } + + err := cfg.Validate() + if err == nil { + t.Fatal("expected short TokenDance ID client secret to be rejected") + } + if !strings.Contains(err.Error(), "tokendance_id.client_secret too short") { + t.Fatalf("Validate() error = %q, want tokendance_id.client_secret too short", err.Error()) + } +} + +func TestValidateTokenDanceIDRejectsSeedSecret(t *testing.T) { + cfg := &Config{ + DB: DBConfig{ + Host: "localhost", + Port: 5432, + User: "agenthub", + Name: "agenthub", + MaxOpenConns: 2, + MaxIdleConns: 1, + ConnMaxLifetime: 30 * time.Minute, + ConnMaxIdleTime: 5 * time.Minute, + }, + Redis: RedisConfig{Host: "localhost", Port: 6379}, + JWT: JWTConfig{ + Secret: "strong-agenthub-secret-padded-to-minimum-32-chars..", + }, + TokenDanceID: TokenDanceIDConfig{ + IssuerURL: "https://id.example", + ClientID: "agenthub-client", + ClientSecret: weakSecretPrefixes[len(weakSecretPrefixes)-1] + strings.Repeat("x", 32), + RedirectURI: "http://127.0.0.1/callback", + }, + } + + err := cfg.Validate() + if err == nil { + t.Fatal("expected seed SQL TokenDance ID client secret to be rejected") + } + if !strings.Contains(err.Error(), "tokendance_id.client_secret must be a strong") { + t.Fatalf("Validate() error = %q, want tokendance_id.client_secret must be a strong", err.Error()) + } +} + // --- DSN / Addr edge cases --- func TestDBConfigDSNZeroValues(t *testing.T) { diff --git a/hub-server/internal/config/config_validate.go b/hub-server/internal/config/config_validate.go index 9603fb728..4b8471d74 100644 --- a/hub-server/internal/config/config_validate.go +++ b/hub-server/internal/config/config_validate.go @@ -84,6 +84,7 @@ var knownHardcodedSecrets = []string{ "password", "1234567890123456", "aaaaaaaaaaaaaaaa", + "agenthub-dev-secret-change-me", } // weakSecretPrefixes blocks the publicly-documented placeholder families. @@ -97,9 +98,12 @@ var knownHardcodedSecrets = []string{ // family (e.g. change-me-production-min-length-32-chars, 41 chars). // Both PG_PASSWORD and JWT_SECRET in .env.example used this prefix; the // JWT variant was long enough to pass the length gate before this entry. +// - "agenthub-dev-secret": the seed SQL OIDC client_secret family +// (agenthub-dev-secret-change-me and derivatives). var weakSecretPrefixes = []string{ "dev-secret-change-in-production", "change-me-production", + "agenthub-dev-secret", } // isKnownWeakSecret reports whether the given secret is a hardcoded default @@ -165,6 +169,17 @@ func (c *Config) validateTokenDanceID() error { if c.TokenDanceID.RedirectURI == "" { return fmt.Errorf("tokendance_id.redirect_uri is required when tokendance_id.client_id is set") } + // OIDC client_secret must meet the same strength bar as JWT secrets: + // 32-char minimum and no known hardcoded/placeholder values. The + // seed SQL default (agenthub-dev-secret-change-me) and the + // .env.example documented placeholders must be rejected so an + // operator cannot accidentally run production with the seed value. + if len(c.TokenDanceID.ClientSecret) < 32 { + return fmt.Errorf("tokendance_id.client_secret too short: minimum 32 characters required (got %d)", len(c.TokenDanceID.ClientSecret)) + } + if isKnownWeakSecret(c.TokenDanceID.ClientSecret) { + return errors.New("tokendance_id.client_secret must be a strong, non-default value; the seed SQL default and documented dev placeholders are rejected") + } } return nil }