🧪 test(bdd): admin metadata endpoint security property — no secret leak (#52)
All checks were successful
CI/CD Pipeline / Build Docker Cache (push) Successful in 11s
CI/CD Pipeline / CI Pipeline (push) Successful in 3m41s
CI/CD Pipeline / Trigger Docker Push (push) Successful in 5s

Co-authored-by: Gabriel Radureau <arcodange@gmail.com>
Co-committed-by: Gabriel Radureau <arcodange@gmail.com>
This commit was merged in pull request #52.
This commit is contained in:
2026-05-05 09:56:17 +02:00
committed by arcodange
parent f71495b6fc
commit e9d61a7fb0
3 changed files with 74 additions and 0 deletions

View File

@@ -40,6 +40,16 @@ Feature: JWT Secret Retention Policy
Then the primary secret should not be removed Then the primary secret should not be removed
And the primary secret should remain active And the primary secret should remain active
@critical @admin-introspection
Scenario: Admin metadata endpoint exposes structure without leaking secret values
Given a primary JWT secret exists
And I add a secondary JWT secret "test-secret-do-not-leak-please-12345"
When I request the JWT secrets metadata endpoint
Then the status code should be 200
And the metadata should contain 2 secrets
And the metadata should NOT contain the secret value "test-secret-do-not-leak-please-12345"
And every secret in the metadata should have a SHA-256 fingerprint
@todo @todo
Scenario: Multiple secrets with different ages Scenario: Multiple secrets with different ages
Given I have 3 JWT secrets of different ages Given I have 3 JWT secrets of different ages

View File

@@ -822,3 +822,61 @@ func (s *JWTRetentionSteps) andSuggestRemediationSteps() error {
// Verify remediation suggestions // Verify remediation suggestions
return godog.ErrPending return godog.ErrPending
} }
// =====================================================================
// Admin metadata introspection steps (PR #51 + this scenario)
// =====================================================================
// iAddASecondaryJWTSecretNamed adds a secret with a specific value via the
// admin API. Used by the admin-introspection scenario to verify that the
// metadata endpoint returns metadata only, not the secret value.
func (s *JWTRetentionSteps) iAddASecondaryJWTSecretNamed(secretValue string) error {
s.SetLastSecret(secretValue)
return s.client.Request("POST", "/api/v1/admin/jwt/secrets", map[string]string{
"secret": secretValue,
"is_primary": "false",
})
}
// iRequestTheJWTSecretsMetadataEndpoint hits GET /api/v1/admin/jwt/secrets.
func (s *JWTRetentionSteps) iRequestTheJWTSecretsMetadataEndpoint() error {
return s.client.Request("GET", "/api/v1/admin/jwt/secrets", nil)
}
// theMetadataShouldContainNSecrets verifies the response count field.
func (s *JWTRetentionSteps) theMetadataShouldContainNSecrets(expected int) error {
body := string(s.client.GetLastBody())
expectedFragment := `"count":` + strconv.Itoa(expected)
if !strings.Contains(body, expectedFragment) {
return fmt.Errorf("expected response to contain %q, got: %s", expectedFragment, body)
}
return nil
}
// theMetadataShouldNotContainTheSecretValue is the SECURITY-CRITICAL
// assertion. If the response contains the raw secret string anywhere,
// the endpoint has leaked. This is the property the metadata-only design
// is supposed to guarantee.
func (s *JWTRetentionSteps) theMetadataShouldNotContainTheSecretValue(secretValue string) error {
body := string(s.client.GetLastBody())
if strings.Contains(body, secretValue) {
return fmt.Errorf("SECURITY: response leaked the secret value %q (response body: %s)", secretValue, body)
}
return nil
}
// everySecretInTheMetadataShouldHaveASHA256Fingerprint asserts the
// secret_sha256 field is present and non-empty for each entry. Cheap
// regex-style check on the JSON body.
func (s *JWTRetentionSteps) everySecretInTheMetadataShouldHaveASHA256Fingerprint() error {
body := string(s.client.GetLastBody())
// Expect at least one occurrence of "secret_sha256":"<non-empty>"
if !strings.Contains(body, `"secret_sha256":"`) {
return fmt.Errorf("response does not include any secret_sha256 fingerprint: %s", body)
}
// Reject obviously-empty values
if strings.Contains(body, `"secret_sha256":""`) {
return fmt.Errorf("at least one secret_sha256 fingerprint is empty in response: %s", body)
}
return nil
}

View File

@@ -173,6 +173,12 @@ func InitializeAllSteps(ctx *godog.ScenarioContext, client *testserver.Client, s
ctx.Step(`^I should receive configuration validation error$`, sc.jwtRetentionSteps.iShouldReceiveConfigurationValidationError) ctx.Step(`^I should receive configuration validation error$`, sc.jwtRetentionSteps.iShouldReceiveConfigurationValidationError)
ctx.Step(`^the error should mention "([^"]*)"$`, sc.jwtRetentionSteps.theErrorShouldMention) ctx.Step(`^the error should mention "([^"]*)"$`, sc.jwtRetentionSteps.theErrorShouldMention)
ctx.Step(`^I have enabled Prometheus metrics$`, sc.jwtRetentionSteps.iHaveEnabledPrometheusMetrics) ctx.Step(`^I have enabled Prometheus metrics$`, sc.jwtRetentionSteps.iHaveEnabledPrometheusMetrics)
// Admin metadata introspection steps (PR #51 + admin-introspection scenario)
ctx.Step(`^I add a secondary JWT secret "([^"]*)"$`, sc.jwtRetentionSteps.iAddASecondaryJWTSecretNamed)
ctx.Step(`^I request the JWT secrets metadata endpoint$`, sc.jwtRetentionSteps.iRequestTheJWTSecretsMetadataEndpoint)
ctx.Step(`^the metadata should contain (\d+) secrets$`, sc.jwtRetentionSteps.theMetadataShouldContainNSecrets)
ctx.Step(`^the metadata should NOT contain the secret value "([^"]*)"$`, sc.jwtRetentionSteps.theMetadataShouldNotContainTheSecretValue)
ctx.Step(`^every secret in the metadata should have a SHA-256 fingerprint$`, sc.jwtRetentionSteps.everySecretInTheMetadataShouldHaveASHA256Fingerprint)
ctx.Step(`^I should see "([^"]*)" metric increment$`, sc.jwtRetentionSteps.iShouldSeeMetricIncrement) ctx.Step(`^I should see "([^"]*)" metric increment$`, sc.jwtRetentionSteps.iShouldSeeMetricIncrement)
ctx.Step(`^I should see "([^"]*)" metric decrease$`, sc.jwtRetentionSteps.iShouldSeeMetricDecrease) ctx.Step(`^I should see "([^"]*)" metric decrease$`, sc.jwtRetentionSteps.iShouldSeeMetricDecrease)
ctx.Step(`^I should see "([^"]*)" histogram update$`, sc.jwtRetentionSteps.iShouldSeeHistogramUpdate) ctx.Step(`^I should see "([^"]*)" histogram update$`, sc.jwtRetentionSteps.iShouldSeeHistogramUpdate)