diff --git a/openapi.go b/openapi.go index e769e1c..3278b82 100644 --- a/openapi.go +++ b/openapi.go @@ -36,6 +36,7 @@ type SpecBuilder struct { Servers []string LicenseName string LicenseURL string + SecuritySchemes map[string]any ExternalDocsDescription string ExternalDocsURL string PprofEnabled bool @@ -179,14 +180,8 @@ func (sb *SpecBuilder) Build(groups []RouteGroup) ([]byte, error) { }, }, }, - "securitySchemes": map[string]any{ - "bearerAuth": map[string]any{ - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - }, - }, - "headers": deprecationHeaderComponents(), + "securitySchemes": securitySchemeComponents(sb.SecuritySchemes), + "headers": deprecationHeaderComponents(), } return json.MarshalIndent(spec, "", " ") @@ -636,6 +631,29 @@ func deprecationHeaderComponents() map[string]any { } } +// securitySchemeComponents builds the OpenAPI security scheme registry. +// bearerAuth stays available by default, while callers can add or override +// additional scheme definitions for custom security requirements. +func securitySchemeComponents(overrides map[string]any) map[string]any { + schemes := map[string]any{ + "bearerAuth": map[string]any{ + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + }, + } + + for name, scheme := range overrides { + name = strings.TrimSpace(name) + if name == "" || scheme == nil { + continue + } + schemes[name] = scheme + } + + return schemes +} + // buildTags generates the tags array from all RouteGroups. func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any { tags := []map[string]any{ diff --git a/openapi_test.go b/openapi_test.go index 14620fb..f726ca0 100644 --- a/openapi_test.go +++ b/openapi_test.go @@ -208,6 +208,55 @@ func TestSpecBuilder_Good_EmptyGroups(t *testing.T) { } } +func TestSpecBuilder_Good_CustomSecuritySchemesAreMerged(t *testing.T) { + sb := &api.SpecBuilder{ + Title: "Test", + Version: "1.0.0", + SecuritySchemes: map[string]any{ + "apiKeyAuth": map[string]any{ + "type": "apiKey", + "in": "header", + "name": "X-API-Key", + }, + }, + } + + data, err := sb.Build(nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var spec map[string]any + if err := json.Unmarshal(data, &spec); err != nil { + t.Fatalf("invalid JSON: %v", err) + } + + components := spec["components"].(map[string]any) + schemes := components["securitySchemes"].(map[string]any) + + bearerAuth, ok := schemes["bearerAuth"].(map[string]any) + if !ok { + t.Fatal("expected default bearerAuth security scheme to remain present") + } + if bearerAuth["scheme"] != "bearer" { + t.Fatalf("expected bearerAuth scheme to stay bearer, got %v", bearerAuth["scheme"]) + } + + apiKeyAuth, ok := schemes["apiKeyAuth"].(map[string]any) + if !ok { + t.Fatal("expected custom apiKeyAuth security scheme to be merged") + } + if apiKeyAuth["type"] != "apiKey" { + t.Fatalf("expected apiKeyAuth.type=apiKey, got %v", apiKeyAuth["type"]) + } + if apiKeyAuth["in"] != "header" { + t.Fatalf("expected apiKeyAuth.in=header, got %v", apiKeyAuth["in"]) + } + if apiKeyAuth["name"] != "X-API-Key" { + t.Fatalf("expected apiKeyAuth.name=X-API-Key, got %v", apiKeyAuth["name"]) + } +} + func TestSpecBuilder_Good_SwaggerUIPathExtension(t *testing.T) { sb := &api.SpecBuilder{ Title: "Test",