From 0a399bd232b4ff62cd3b3096d87c36ef9495bbf9 Mon Sep 17 00:00:00 2001 From: flmeyer Date: Wed, 26 Nov 2025 15:54:13 +0100 Subject: [PATCH 1/4] feat: Multiple SecurityRequirement annotations with AND condition #3556 --- .../security/SecurityRequirement.java | 10 ++++++ .../security/SecurityRequirementEntry.java | 33 +++++++++++++++++++ .../io/swagger/v3/jaxrs2/SecurityParser.java | 28 ++++++++++++---- 3 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java index 989a7386fc..6be57717f5 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java @@ -14,6 +14,8 @@ * The annotation may be applied at class or method level, or in {@link io.swagger.v3.oas.annotations.Operation#security()} ()} to define security requirements for the * single operation (when applied at method level) or for all operations of a class (when applied at class level). *

It can also be used in {@link io.swagger.v3.oas.annotations.OpenAPIDefinition#security()} to define spec level security.

+ *

{@link SecurityRequirement#entries()} can be used to define multiple security requirements at the same time, requiring each one of them. + * If only one of multiple security schemes is required, use multiple {@link SecurityRequirement} annotations.

* * @see Security Requirement (OpenAPI specification) * @see io.swagger.v3.oas.annotations.OpenAPIDefinition @@ -38,4 +40,12 @@ * @return String array of scopes */ String[] scopes() default {}; + + /** + * If multiple requirements apply at the same time, use this value instead of {@link SecurityRequirement#name()} and {@link SecurityRequirement#scopes()}. + * If any one of multiple security schemes is required, use multiple {@link SecurityRequirement} annotations instead. + * + * @return SecurityRequirementEntry array of requirements + */ + SecurityRequirementEntry[] entries() default {}; } diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java new file mode 100644 index 0000000000..64e4db62c8 --- /dev/null +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java @@ -0,0 +1,33 @@ +package io.swagger.v3.oas.annotations.security; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotation may be applied in {@link SecurityRequirement#entries()} to define combined security requirements for the + * single operation. + * + * @see Security Requirement (OpenAPI specification) + * @see io.swagger.v3.oas.annotations.security.SecurityRequirement + **/ +@Target({ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SecurityRequirementEntry { + /** + * This name must correspond to a declared SecurityRequirement. + * + * @return String name + */ + String name(); + + /** + * If the security scheme is of type "oauth2" or "openIdConnect", then the value is a list of scope names required for the execution. + * For other security scheme types, the array must be empty. + * + * @return String array of scopes + */ + String[] scopes() default {}; +} diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java index c0a6010b31..7b55141e7d 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java @@ -28,14 +28,28 @@ public static Optional> getSecurityRequirements(io.swa } List securityRequirements = new ArrayList<>(); for (io.swagger.v3.oas.annotations.security.SecurityRequirement securityRequirementApi : securityRequirementsApi) { - if (StringUtils.isBlank(securityRequirementApi.name())) { - continue; - } SecurityRequirement securityRequirement = new SecurityRequirement(); - if (securityRequirementApi.scopes().length > 0) { - securityRequirement.addList(securityRequirementApi.name(), Arrays.asList(securityRequirementApi.scopes())); - } else { - securityRequirement.addList(securityRequirementApi.name()); + if (securityRequirementApi.entries().length > 0) { + for (io.swagger.v3.oas.annotations.security.SecurityRequirementEntry entry : securityRequirementApi.entries()) { + if (StringUtils.isBlank(entry.name())) { + continue; + } + if (entry.scopes().length > 0) { + securityRequirement.addList(entry.name(), Arrays.asList(entry.scopes())); + } else { + securityRequirement.addList(entry.name()); + } + } + } + else { + if (StringUtils.isBlank(securityRequirementApi.name())) { + continue; + } + if (securityRequirementApi.scopes().length > 0) { + securityRequirement.addList(securityRequirementApi.name(), Arrays.asList(securityRequirementApi.scopes())); + } else { + securityRequirement.addList(securityRequirementApi.name()); + } } securityRequirements.add(securityRequirement); } From 904ddcbcc162af6ebcd724239219e26ce28a7f76 Mon Sep 17 00:00:00 2001 From: flmeyer Date: Wed, 26 Nov 2025 16:27:37 +0100 Subject: [PATCH 2/4] fix: make SecurityRequirement.name optional --- .../v3/oas/annotations/security/SecurityRequirement.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java index 6be57717f5..792f365776 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java @@ -11,7 +11,7 @@ import static java.lang.annotation.ElementType.METHOD; /** - * The annotation may be applied at class or method level, or in {@link io.swagger.v3.oas.annotations.Operation#security()} ()} to define security requirements for the + * The annotation may be applied at class or method level, or in {@link io.swagger.v3.oas.annotations.Operation#security()} to define security requirements for the * single operation (when applied at method level) or for all operations of a class (when applied at class level). *

It can also be used in {@link io.swagger.v3.oas.annotations.OpenAPIDefinition#security()} to define spec level security.

*

{@link SecurityRequirement#entries()} can be used to define multiple security requirements at the same time, requiring each one of them. @@ -28,10 +28,11 @@ public @interface SecurityRequirement { /** * This name must correspond to a declared SecurityRequirement. + *

Exactly one of this and {@link #entries()} must be set.

* * @return String name */ - String name(); + String name() default ""; /** * If the security scheme is of type "oauth2" or "openIdConnect", then the value is a list of scope names required for the execution. @@ -42,8 +43,9 @@ String[] scopes() default {}; /** - * If multiple requirements apply at the same time, use this value instead of {@link SecurityRequirement#name()} and {@link SecurityRequirement#scopes()}. + * If multiple requirements apply at the same time, use this value instead of {@link #name()} and {@link #scopes()}. * If any one of multiple security schemes is required, use multiple {@link SecurityRequirement} annotations instead. + *

Exactly one of this and {@link #name()} must be set.

* * @return SecurityRequirementEntry array of requirements */ From e94aa98067b3286e6c36ddbda2bb41fc7f17b46c Mon Sep 17 00:00:00 2001 From: flmeyer Date: Wed, 26 Nov 2025 16:31:02 +0100 Subject: [PATCH 3/4] chore: rename to SecurityRequirement.combine() --- .../v3/oas/annotations/security/SecurityRequirement.java | 6 +++--- .../oas/annotations/security/SecurityRequirementEntry.java | 2 +- .../src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java index 792f365776..3a1b9d144c 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirement.java @@ -14,7 +14,7 @@ * The annotation may be applied at class or method level, or in {@link io.swagger.v3.oas.annotations.Operation#security()} to define security requirements for the * single operation (when applied at method level) or for all operations of a class (when applied at class level). *

It can also be used in {@link io.swagger.v3.oas.annotations.OpenAPIDefinition#security()} to define spec level security.

- *

{@link SecurityRequirement#entries()} can be used to define multiple security requirements at the same time, requiring each one of them. + *

{@link SecurityRequirement#combine()} can be used to define multiple security requirements at the same time, requiring each one of them. * If only one of multiple security schemes is required, use multiple {@link SecurityRequirement} annotations.

* * @see Security Requirement (OpenAPI specification) @@ -28,7 +28,7 @@ public @interface SecurityRequirement { /** * This name must correspond to a declared SecurityRequirement. - *

Exactly one of this and {@link #entries()} must be set.

+ *

Exactly one of this and {@link #combine()} must be set.

* * @return String name */ @@ -49,5 +49,5 @@ * * @return SecurityRequirementEntry array of requirements */ - SecurityRequirementEntry[] entries() default {}; + SecurityRequirementEntry[] combine() default {}; } diff --git a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java index 64e4db62c8..4070c06c96 100644 --- a/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java +++ b/modules/swagger-annotations/src/main/java/io/swagger/v3/oas/annotations/security/SecurityRequirementEntry.java @@ -7,7 +7,7 @@ import java.lang.annotation.Target; /** - * The annotation may be applied in {@link SecurityRequirement#entries()} to define combined security requirements for the + * The annotation may be applied in {@link SecurityRequirement#combine()} to define combined security requirements for the * single operation. * * @see Security Requirement (OpenAPI specification) diff --git a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java index 7b55141e7d..0d3eba846e 100644 --- a/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java +++ b/modules/swagger-jaxrs2/src/main/java/io/swagger/v3/jaxrs2/SecurityParser.java @@ -29,8 +29,8 @@ public static Optional> getSecurityRequirements(io.swa List securityRequirements = new ArrayList<>(); for (io.swagger.v3.oas.annotations.security.SecurityRequirement securityRequirementApi : securityRequirementsApi) { SecurityRequirement securityRequirement = new SecurityRequirement(); - if (securityRequirementApi.entries().length > 0) { - for (io.swagger.v3.oas.annotations.security.SecurityRequirementEntry entry : securityRequirementApi.entries()) { + if (securityRequirementApi.combine().length > 0) { + for (io.swagger.v3.oas.annotations.security.SecurityRequirementEntry entry : securityRequirementApi.combine()) { if (StringUtils.isBlank(entry.name())) { continue; } From cfc4ffda604386a49e243de637bd2a7e09652568 Mon Sep 17 00:00:00 2001 From: flmeyer Date: Wed, 26 Nov 2025 16:32:10 +0100 Subject: [PATCH 4/4] test: implement tests for combined and disjunct requirements --- .../annotations/security/SecurityTest.java | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java index bdaba471fa..a1cecd845b 100644 --- a/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java +++ b/modules/swagger-jaxrs2/src/test/java/io/swagger/v3/jaxrs2/annotations/security/SecurityTest.java @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.security.OAuthFlows; import io.swagger.v3.oas.annotations.security.OAuthScope; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirementEntry; import io.swagger.v3.oas.annotations.security.SecurityScheme; import org.testng.annotations.Test; @@ -19,7 +20,7 @@ public class SecurityTest extends AbstractAnnotationTest { @Test - public void testSecuritySheme() { + public void testSecurityScheme() { String openApiYAML = readIntoYaml(SecurityTest.OAuth2SchemeOnClass.class); int start = openApiYAML.indexOf("components:"); String extractedYAML = openApiYAML.substring(start, openApiYAML.length() - 1); @@ -90,7 +91,7 @@ public void testSecurityRequirement() throws IOException { } @Test - public void testMultipleSecurityShemes() { + public void testMultipleSecuritySchemes() { String openApiYAML = readIntoYaml(SecurityTest.MultipleSchemesOnClass.class); int start = openApiYAML.indexOf("components:"); String extractedYAML = openApiYAML.substring(start, openApiYAML.length() - 1); @@ -112,6 +113,30 @@ public void testMultipleSecurityShemes() { } + @Test + public void testCombinedSecurityRequirements() { + String openApiYAML = readIntoYaml(SecurityTest.CombinedSecurityRequirementsOnClass.class); + int start = openApiYAML.indexOf("security:"); + int end = openApiYAML.indexOf("components:"); + String extractedYAML = openApiYAML.substring(start, end); + String expectedYAML = "security:\n" + + "- api_key: []\n" + + " myOauth2Security: []\n"; + assertEquals(extractedYAML, expectedYAML); + } + + @Test + public void testSecurityRequirementAlternatives() { + String openApiYAML = readIntoYaml(SecurityRequirementAlternativesOnClass.class); + int start = openApiYAML.indexOf("security:"); + int end = openApiYAML.indexOf("components:"); + String extractedYAML = openApiYAML.substring(start, end); + String expectedYAML = "security:\n" + + "- api_key: []\n" + + "- myOauth2Security: []\n"; + assertEquals(extractedYAML, expectedYAML); + } + @Test public void testTicket2767() { String openApiYAML = readIntoYaml(SecurityTest.Ticket2767.class); @@ -169,6 +194,34 @@ static class MultipleSchemesOnClass { } + @OpenAPIDefinition( + security = {@SecurityRequirement(combine = { @SecurityRequirementEntry(name = "api_key"), @SecurityRequirementEntry(name = "myOauth2Security") })} + ) + @SecurityScheme(name = "api_key", type = SecuritySchemeType.APIKEY, paramName = "API_KEY") + @SecurityScheme(name = "myOauth2Security", + type = SecuritySchemeType.OAUTH2, + in = SecuritySchemeIn.HEADER, + flows = @OAuthFlows( + implicit = @OAuthFlow(authorizationUrl = "http://url.com/auth", + scopes = @OAuthScope(name = "write:pets", description = "modify pets in your account")))) + static class CombinedSecurityRequirementsOnClass { + + } + + @OpenAPIDefinition( + security = {@SecurityRequirement(name = "api_key"), @SecurityRequirement(name = "myOauth2Security")} + ) + @SecurityScheme(name = "api_key", type = SecuritySchemeType.APIKEY, paramName = "API_KEY") + @SecurityScheme(name = "myOauth2Security", + type = SecuritySchemeType.OAUTH2, + in = SecuritySchemeIn.HEADER, + flows = @OAuthFlows( + implicit = @OAuthFlow(authorizationUrl = "http://url.com/auth", + scopes = @OAuthScope(name = "write:pets", description = "modify pets in your account")))) + static class SecurityRequirementAlternativesOnClass { + + } + @OpenAPIDefinition( security = {@SecurityRequirement(name = "basicAuth")},