From 18f43222a853ed236f357fc62700f1723abaf839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20C=C3=B4t=C3=A9?= Date: Tue, 21 Jul 2026 10:57:42 -0400 Subject: [PATCH 1/5] feat(spring): generate PreAuthorize from OpenAPI security scopes Add opt-in Spring Boot support to translate OAuth2/OIDC security requirements into @PreAuthorize expressions, preserving OpenAPI AND/OR semantics. Also: enable method security with @EnableMethodSecurity add Spring Security dependency when enabled support configurable authority prefix (SCOPE_ by default) add documentation, tests, and a generated sample --- bin/configs/spring-boot-pre-authorize.yaml | 11 +++ docs/generators/spring.md | 2 + .../codegen/languages/SpringCodegen.java | 92 +++++++++++++++++++ .../main/resources/JavaSpring/api.mustache | 6 ++ .../spring-boot/openapi2SpringBoot.mustache | 8 +- .../libraries/spring-boot/pom-sb3.mustache | 6 ++ .../libraries/spring-boot/pom-sb4.mustache | 6 ++ .../libraries/spring-boot/pom.mustache | 6 ++ .../java/spring/SpringCodegenTest.java | 72 ++++++++++++++- .../3_0/spring/preauthorize-scopes.yaml | 43 +++++++++ .../.openapi-generator-ignore | 23 +++++ .../.openapi-generator/FILES | 10 ++ .../.openapi-generator/VERSION | 1 + .../others/springboot-pre-authorize/README.md | 12 +++ .../others/springboot-pre-authorize/pom.xml | 74 +++++++++++++++ .../OpenApiGeneratorApplication.java | 32 +++++++ .../org/openapitools/RFC3339DateFormat.java | 38 ++++++++ .../java/org/openapitools/api/ApiApi.java | 69 ++++++++++++++ .../openapitools/api/ApiApiController.java | 45 +++++++++ .../java/org/openapitools/api/ApiUtil.java | 21 +++++ .../configuration/HomeController.java | 13 +++ .../src/main/resources/application.properties | 3 + .../src/main/resources/openapi.yaml | 54 +++++++++++ .../OpenApiGeneratorApplicationTests.java | 13 +++ 24 files changed, 658 insertions(+), 2 deletions(-) create mode 100644 bin/configs/spring-boot-pre-authorize.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml create mode 100644 samples/server/others/springboot-pre-authorize/.openapi-generator-ignore create mode 100644 samples/server/others/springboot-pre-authorize/.openapi-generator/FILES create mode 100644 samples/server/others/springboot-pre-authorize/.openapi-generator/VERSION create mode 100644 samples/server/others/springboot-pre-authorize/README.md create mode 100644 samples/server/others/springboot-pre-authorize/pom.xml create mode 100644 samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java create mode 100644 samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/RFC3339DateFormat.java create mode 100644 samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java create mode 100644 samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApiController.java create mode 100644 samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiUtil.java create mode 100644 samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/configuration/HomeController.java create mode 100644 samples/server/others/springboot-pre-authorize/src/main/resources/application.properties create mode 100644 samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml create mode 100644 samples/server/others/springboot-pre-authorize/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java diff --git a/bin/configs/spring-boot-pre-authorize.yaml b/bin/configs/spring-boot-pre-authorize.yaml new file mode 100644 index 000000000000..cddc04a3b276 --- /dev/null +++ b/bin/configs/spring-boot-pre-authorize.yaml @@ -0,0 +1,11 @@ +generatorName: spring +outputDir: samples/server/others/springboot-pre-authorize +library: spring-boot +inputSpec: modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml +templateDir: modules/openapi-generator/src/main/resources/JavaSpring +additionalProperties: + documentationProvider: none + annotationLibrary: none + useSwaggerUI: "false" + useSpringSecurityPreAuthorize: "true" + hideGenerationTimestamp: "true" diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 31f5f55ff4a5..4f5979324188 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -98,6 +98,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src/main/java| |springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null| +|springSecurityAuthorityPrefix|Prefix added to OAuth2/OpenID Connect scopes when generating Spring Security authorities.| |SCOPE_| |substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation.| |false| |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |title|server title name or client service name| |OpenAPI Spring| @@ -119,6 +120,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot3|Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |true| |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false| |useSpringBuiltInValidation|Disable `@Validated` at the class level when using built-in validation.| |false| +|useSpringSecurityPreAuthorize|Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes. Only applies to the spring-boot library.| |false| |useSpringController|Annotate the generated API as a Spring Controller| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 1ab10fd19d09..73fba593e077 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -24,6 +24,8 @@ import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.tags.Tag; import io.swagger.v3.parser.util.SchemaTypeUtil; import lombok.Getter; @@ -123,6 +125,8 @@ public class SpringCodegen extends AbstractJavaCodegen public static final String GENERATE_PAGEABLE_CONSTRAINT_VALIDATION = "generatePageableConstraintValidation"; public static final String SUBSTITUTE_GENERIC_PAGED_MODEL = "substituteGenericPagedModel"; public static final String CLIENT_REGISTRATION_ID = "clientRegistrationId"; + public static final String USE_SPRING_SECURITY_PRE_AUTHORIZE = "useSpringSecurityPreAuthorize"; + public static final String SPRING_SECURITY_AUTHORITY_PREFIX = "springSecurityAuthorityPrefix"; @Getter public enum RequestMappingMode { @@ -199,6 +203,9 @@ public enum RequestMappingMode { @Setter protected boolean substituteGenericPagedModel = false; @Getter @Setter protected String clientRegistrationId = null; + @Setter protected boolean useSpringSecurityPreAuthorize = false; + @Getter @Setter + protected String springSecurityAuthorityPrefix = "SCOPE_"; @Setter protected boolean useEnumValueInterface = false; private String valuedEnumClassName = "ValuedEnum"; @@ -347,6 +354,12 @@ public SpringCodegen() { ); cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks", useJspecify)); cliOptions.add(CliOption.newString(CLIENT_REGISTRATION_ID, "Client registration ID for OAuth2 in Spring HTTP Interface (@ClientRegistrationId annotation). Requires library=spring-http-interface and useSpringBoot4=true (Spring Security 7).")); + cliOptions.add(CliOption.newBoolean(USE_SPRING_SECURITY_PRE_AUTHORIZE, + "Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes.", + useSpringSecurityPreAuthorize)); + cliOptions.add(CliOption.newString(SPRING_SECURITY_AUTHORITY_PREFIX, + "Prefix added to OAuth2/OpenID Connect scopes when generating Spring Security authorities.", + springSecurityAuthorityPrefix)); supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application."); supportedLibraries.put(SPRING_CLOUD_LIBRARY, "Spring-Cloud-Feign client with Spring-Boot auto-configured settings."); @@ -573,6 +586,8 @@ public void processOpts() { convertPropertyToBooleanAndWriteBack(USE_SPRING_BUILT_IN_VALIDATION, this::setUseSpringBuiltInValidation); convertPropertyToBooleanAndWriteBack(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, this::setUseDeductionForOneOfInterfaces); convertPropertyToStringAndWriteBack(CLIENT_REGISTRATION_ID, this::setClientRegistrationId); + convertPropertyToBooleanAndWriteBack(USE_SPRING_SECURITY_PRE_AUTHORIZE, this::setUseSpringSecurityPreAuthorize); + convertPropertyToStringAndWriteBack(SPRING_SECURITY_AUTHORITY_PREFIX, this::setSpringSecurityAuthorityPrefix); additionalProperties.put("springHttpStatus", new SpringHttpStatusLambda()); @@ -592,6 +607,10 @@ public void processOpts() { throw new IllegalArgumentException(CLIENT_REGISTRATION_ID + " requires " + USE_SPRING_BOOT4 + "=true because @ClientRegistrationId is provided by Spring Security 7"); } } + if (useSpringSecurityPreAuthorize && !SPRING_BOOT.equals(library)) { + throw new IllegalArgumentException(USE_SPRING_SECURITY_PRE_AUTHORIZE + + " is only supported with the " + SPRING_BOOT + " library"); + } if (isUseSpringBoot3() || isUseSpringBoot4()) { if (AnnotationLibrary.SWAGGER1.equals(getAnnotationLibrary())) { @@ -1029,6 +1048,10 @@ public void setIsVoid(boolean isVoid) { normalizeVendorExtensionWithStringList(operation.vendorExtensions, VendorExtension.X_OPERATION_EXTRA_ANNOTATION.getName()); normalizeOperationParameterVendorExtensions(operation, VendorExtension.X_FIELD_EXTRA_ANNOTATION.getName()); + if (useSpringSecurityPreAuthorize) { + addSpringSecurityPreAuthorize(operation); + } + if (isLibrary(SPRING_HTTP_INTERFACE) || isLibrary(SPRING_BOOT)) { if (operation.isArray && "string".equalsIgnoreCase(operation.returnBaseType)) { operation.vendorExtensions.put(VendorExtension.X_REACTIVE_RETURN_EXCEPT_LIST_OF_STRING.getName(), true); @@ -1054,6 +1077,75 @@ public void setIsVoid(boolean isVoid) { return objs; } + /** + * Adds a Spring Security expression that preserves the OpenAPI security requirement semantics: + * entries in a {@code security} array are OR alternatives, while schemes and scopes in one + * entry are combined with AND. + */ + private void addSpringSecurityPreAuthorize(CodegenOperation codegenOperation) { + Operation rawOperation = findOperation(codegenOperation.operationId); + if (rawOperation == null) { + LOGGER.warn("Could not find OpenAPI operation '{}' while generating @PreAuthorize.", codegenOperation.operationId); + return; + } + + List requirements = rawOperation.getSecurity(); + if (requirements == null) { + requirements = openAPI.getSecurity(); + } + if (requirements == null || requirements.isEmpty()) { + return; + } + + Map schemes = openAPI.getComponents() != null + ? openAPI.getComponents().getSecuritySchemes() : null; + if (schemes == null || schemes.isEmpty()) { + return; + } + + List alternatives = new ArrayList<>(); + for (SecurityRequirement requirement : requirements) { + if (requirement.isEmpty()) { + return; + } + List groupAuthorities = new ArrayList<>(); + for (Map.Entry> entry : requirement.entrySet()) { + SecurityScheme scheme = schemes.get(entry.getKey()); + if (scheme == null || (scheme.getType() != SecurityScheme.Type.OAUTH2 + && scheme.getType() != SecurityScheme.Type.OPENIDCONNECT) + || entry.getValue() == null || entry.getValue().isEmpty()) { + return; + } + for (String scope : entry.getValue()) { + groupAuthorities.add("hasAuthority('" + springSecurityAuthorityPrefix + scope + "')"); + } + } + String group = String.join(" and ", groupAuthorities); + alternatives.add(groupAuthorities.size() > 1 && requirements.size() > 1 ? "(" + group + ")" : group); + } + + if (!alternatives.isEmpty()) { + codegenOperation.vendorExtensions.put("x-spring-security-pre-authorize", String.join(" or ", alternatives)); + } + } + + private Operation findOperation(String operationId) { + if (openAPI.getPaths() == null || operationId == null) { + return null; + } + for (PathItem pathItem : openAPI.getPaths().values()) { + if (pathItem.readOperations() == null) { + continue; + } + for (Operation operation : pathItem.readOperations()) { + if (operationId.equals(operation.getOperationId())) { + return operation; + } + } + } + return null; + } + private interface DataTypeAssigner { void setReturnType(String returnType); diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache index 77ce7fd59757..39394a4f1991 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/api.mustache @@ -40,6 +40,9 @@ import org.springframework.http.HttpStatus; {{#useResponseEntity}} import org.springframework.http.ResponseEntity; {{/useResponseEntity}} +{{#useSpringSecurityPreAuthorize}} +import org.springframework.security.access.prepost.PreAuthorize; +{{/useSpringSecurityPreAuthorize}} {{#useBeanValidation}}{{^useSpringBuiltInValidation}}import org.springframework.validation.annotation.Validated;{{/useSpringBuiltInValidation}}{{/useBeanValidation}} {{#useSpringController}} {{#useResponseEntity}} @@ -272,6 +275,9 @@ public interface {{classname}} { {{#vendorExtensions.x-operation-extra-annotation}} {{{.}}} {{/vendorExtensions.x-operation-extra-annotation}} + {{#vendorExtensions.x-spring-security-pre-authorize}} + @PreAuthorize("{{{.}}}") + {{/vendorExtensions.x-spring-security-pre-authorize}} {{#vendorExtensions.x-sse}} @ResponseBody {{/vendorExtensions.x-sse}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache index 4f69d937e63e..cdcaccba9ffd 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache @@ -6,6 +6,9 @@ import org.openapitools.jackson.nullable.JsonNullableModule; {{/openApiNullable}} import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +{{#useSpringSecurityPreAuthorize}} +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +{{/useSpringSecurityPreAuthorize}} import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; @@ -18,6 +21,9 @@ import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGe basePackages = {"{{basePackage}}", "{{apiPackage}}" , "{{configPackage}}"}, nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class ) +{{#useSpringSecurityPreAuthorize}} +@EnableMethodSecurity +{{/useSpringSecurityPreAuthorize}} public class OpenApiGeneratorApplication { public static void main(String[] args) { @@ -31,4 +37,4 @@ public class OpenApiGeneratorApplication { } {{/openApiNullable}} -} \ No newline at end of file +} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache index 8a439c186300..77a6b67e90e6 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache @@ -111,6 +111,12 @@ + {{#useSpringSecurityPreAuthorize}} + + org.springframework.boot + spring-boot-starter-security + + {{/useSpringSecurityPreAuthorize}} org.springframework.boot spring-boot-starter-web{{#reactive}}flux{{/reactive}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache index 4df090a4a326..12e7b4d61d03 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache @@ -111,6 +111,12 @@ + {{#useSpringSecurityPreAuthorize}} + + org.springframework.boot + spring-boot-starter-security + + {{/useSpringSecurityPreAuthorize}} org.springframework.boot spring-boot-starter-web{{#reactive}}flux{{/reactive}}{{^reactive}}mvc{{/reactive}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache index 0c4fd4d0885b..867333fa1646 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache @@ -119,6 +119,12 @@ + {{#useSpringSecurityPreAuthorize}} + + org.springframework.boot + spring-boot-starter-security + + {{/useSpringSecurityPreAuthorize}} org.springframework.boot spring-boot-starter-web{{#reactive}}flux{{/reactive}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 2193ee5c9f9f..e85294401910 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -4053,6 +4053,76 @@ public void testHasOperationExtraAnnotation_issue12219_array() throws IOExceptio .containsWithName("org.springframework.security.access.prepost.PreAuthorize"); } + @Test + public void testGeneratePreAuthorizeFromOAuth2Scopes() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/spring/preauthorize-scopes.yaml"); + final SpringCodegen codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SPRING_SECURITY_PRE_AUTHORIZE, true); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true"); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("ApiApi.java")) + .assertMethod("getOverview") + .assertMethodAnnotations() + .containsWithNameAndAttributes("PreAuthorize", ImmutableMap.of("value", + "\"hasAuthority('SCOPE_client-data:read') or hasAuthority('SCOPE_utility-data:read')\"")); + JavaFileAssert.assertThat(files.get("ApiApi.java")) + .assertMethod("getCombined") + .assertMethodAnnotations() + .containsWithNameAndAttributes("PreAuthorize", ImmutableMap.of("value", + "\"(hasAuthority('SCOPE_client-data:read') and hasAuthority('SCOPE_utility-data:read')) or hasAuthority('SCOPE_other-data:read')\"")); + assertThat(Files.readString(files.get("ApiApi.java").toPath())) + .contains("import org.springframework.security.access.prepost.PreAuthorize;"); + assertThat(Files.readString(files.get("OpenApiGeneratorApplication.java").toPath())) + .contains("import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;") + .contains("@EnableMethodSecurity"); + } + + @Test + public void testGeneratePreAuthorizeWithCustomAuthorityPrefix() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/spring/preauthorize-scopes.yaml"); + final SpringCodegen codegen = new SpringCodegen(); + codegen.setOpenAPI(openAPI); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(USE_SPRING_SECURITY_PRE_AUTHORIZE, true); + codegen.additionalProperties().put(SPRING_SECURITY_AUTHORITY_PREFIX, "PERMISSION_"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGenerateMetadata(false); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true"); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + JavaFileAssert.assertThat(files.get("ApiApi.java")) + .assertMethod("getOverview") + .assertMethodAnnotations() + .containsWithNameAndAttributes("PreAuthorize", ImmutableMap.of("value", + "\"hasAuthority('PERMISSION_client-data:read') or hasAuthority('PERMISSION_utility-data:read')\"")); + } + @Test public void doCallFluentParentSettersFromChildModel() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); @@ -8529,4 +8599,4 @@ public void testReactiveSpringHttpInterfaceSupportListOfStringReturnTypeNoRespon "Mono> getUserIdSet" ); } -} \ No newline at end of file +} diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml new file mode 100644 index 000000000000..35cc38adaade --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml @@ -0,0 +1,43 @@ +openapi: 3.0.3 +info: + title: PreAuthorize scopes + version: 1.0.0 +paths: + /api/overview: + get: + operationId: getOverview + tags: + - Overview + security: + - auth0: + - client-data:read + - auth0: + - utility-data:read + responses: + '200': + description: Ok + /api/combined: + get: + operationId: getCombined + tags: + - Overview + security: + - auth0: + - client-data:read + - utility-data:read + - auth0: + - other-data:read + responses: + '200': + description: Ok +components: + securitySchemes: + auth0: + type: oauth2 + flows: + clientCredentials: + tokenUrl: https://example.test/oauth/token + scopes: + client-data:read: Read client data + utility-data:read: Read utility data + other-data:read: Read other data diff --git a/samples/server/others/springboot-pre-authorize/.openapi-generator-ignore b/samples/server/others/springboot-pre-authorize/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/others/springboot-pre-authorize/.openapi-generator/FILES b/samples/server/others/springboot-pre-authorize/.openapi-generator/FILES new file mode 100644 index 000000000000..8a22682c7627 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/.openapi-generator/FILES @@ -0,0 +1,10 @@ +README.md +pom.xml +src/main/java/org/openapitools/OpenApiGeneratorApplication.java +src/main/java/org/openapitools/RFC3339DateFormat.java +src/main/java/org/openapitools/api/ApiApi.java +src/main/java/org/openapitools/api/ApiUtil.java +src/main/java/org/openapitools/configuration/HomeController.java +src/main/resources/application.properties +src/main/resources/openapi.yaml +src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java diff --git a/samples/server/others/springboot-pre-authorize/.openapi-generator/VERSION b/samples/server/others/springboot-pre-authorize/.openapi-generator/VERSION new file mode 100644 index 000000000000..8fc8df61083a --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.25.0-SNAPSHOT diff --git a/samples/server/others/springboot-pre-authorize/README.md b/samples/server/others/springboot-pre-authorize/README.md new file mode 100644 index 000000000000..7e955e89350e --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/README.md @@ -0,0 +1,12 @@ +# OpenAPI generated server + +Spring Boot Server + +## Overview +This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. +By using the [OpenAPI-Spec](https://openapis.org), you can easily generate a server stub. +This is an example of building a OpenAPI-enabled server in Java using the SpringBoot framework. + + +Start your server as a simple java application +Change default port value in application.properties \ No newline at end of file diff --git a/samples/server/others/springboot-pre-authorize/pom.xml b/samples/server/others/springboot-pre-authorize/pom.xml new file mode 100644 index 000000000000..c3d743205ff2 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/pom.xml @@ -0,0 +1,74 @@ + + 4.0.0 + org.openapitools + openapi-spring + jar + openapi-spring + 1.0.0 + + 17 + ${java.version} + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.13 + + + + + src/main/java + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.data + spring-data-commons + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + org.openapitools + jackson-databind-nullable + 0.2.10 + + + + org.springframework.boot + spring-boot-starter-validation + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java new file mode 100644 index 000000000000..08a5f51fa304 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java @@ -0,0 +1,32 @@ +package org.openapitools; + +import com.fasterxml.jackson.databind.Module; +import org.openapitools.jackson.nullable.JsonNullableModule; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGenerator; + +@SpringBootApplication( + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +@ComponentScan( + basePackages = {"org.openapitools", "org.openapitools.api" , "org.openapitools.configuration"}, + nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class +) +@EnableMethodSecurity +public class OpenApiGeneratorApplication { + + public static void main(String[] args) { + SpringApplication.run(OpenApiGeneratorApplication.class, args); + } + + @Bean(name = "org.openapitools.OpenApiGeneratorApplication.jsonNullableModule") + public Module jsonNullableModule() { + return new JsonNullableModule(); + } + +} diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/RFC3339DateFormat.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/RFC3339DateFormat.java new file mode 100644 index 000000000000..bcd3936d8b34 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/RFC3339DateFormat.java @@ -0,0 +1,38 @@ +package org.openapitools; + +import com.fasterxml.jackson.databind.util.StdDateFormat; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +public class RFC3339DateFormat extends DateFormat { + private static final long serialVersionUID = 1L; + private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC"); + + private final StdDateFormat fmt = new StdDateFormat() + .withTimeZone(TIMEZONE_Z) + .withColonInTimeZone(true); + + public RFC3339DateFormat() { + this.calendar = new GregorianCalendar(); + } + + @Override + public Date parse(String source, ParsePosition pos) { + return fmt.parse(source, pos); + } + + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + return fmt.format(date, toAppendTo, fieldPosition); + } + + @Override + public Object clone() { + return this; + } +} \ No newline at end of file diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java new file mode 100644 index 000000000000..80cbd34bc8bb --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java @@ -0,0 +1,69 @@ +/* + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (7.25.0-SNAPSHOT). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +package org.openapitools.api; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.*; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import jakarta.annotation.Generated; + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.25.0-SNAPSHOT") +@Validated +public interface ApiApi { + + default Optional getRequest() { + return Optional.empty(); + } + + String PATH_GET_COMBINED = "/api/combined"; + /** + * GET /api/combined + * + * @return Ok (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = ApiApi.PATH_GET_COMBINED + ) + @PreAuthorize("(hasAuthority('SCOPE_client-data:read') and hasAuthority('SCOPE_utility-data:read')) or hasAuthority('SCOPE_other-data:read')") + default ResponseEntity getCombined( + + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + String PATH_GET_OVERVIEW = "/api/overview"; + /** + * GET /api/overview + * + * @return Ok (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = ApiApi.PATH_GET_OVERVIEW + ) + @PreAuthorize("hasAuthority('SCOPE_client-data:read') or hasAuthority('SCOPE_utility-data:read')") + default ResponseEntity getOverview( + + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + +} diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApiController.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApiController.java new file mode 100644 index 000000000000..df5095dd4770 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApiController.java @@ -0,0 +1,45 @@ +package org.openapitools.api; + + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.context.request.NativeWebRequest; + +import jakarta.validation.constraints.*; +import jakarta.validation.Valid; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import jakarta.annotation.Generated; + +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.25.0-SNAPSHOT") +@Controller +@RequestMapping("${openapi.preAuthorizeScopes.base-path:}") +public class ApiApiController implements ApiApi { + + private final NativeWebRequest request; + + @Autowired + public ApiApiController(NativeWebRequest request) { + this.request = request; + } + + @Override + public Optional getRequest() { + return Optional.ofNullable(request); + } + +} diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiUtil.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiUtil.java new file mode 100644 index 000000000000..44bf770ccc47 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiUtil.java @@ -0,0 +1,21 @@ +package org.openapitools.api; + +import org.springframework.web.context.request.NativeWebRequest; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ApiUtil { + public static void setExampleResponse(NativeWebRequest req, String contentType, String example) { + try { + HttpServletResponse res = req.getNativeResponse(HttpServletResponse.class); + if (res != null) { + res.setCharacterEncoding("UTF-8"); + res.addHeader("Content-Type", contentType); + res.getWriter().print(example); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/configuration/HomeController.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/configuration/HomeController.java new file mode 100644 index 000000000000..707313504790 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/configuration/HomeController.java @@ -0,0 +1,13 @@ +package org.openapitools.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Home redirection to OpenAPI api documentation + */ +@Controller +public class HomeController { + +} \ No newline at end of file diff --git a/samples/server/others/springboot-pre-authorize/src/main/resources/application.properties b/samples/server/others/springboot-pre-authorize/src/main/resources/application.properties new file mode 100644 index 000000000000..7e90813e59b2 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/resources/application.properties @@ -0,0 +1,3 @@ +server.port=8080 +spring.jackson.date-format=org.openapitools.RFC3339DateFormat +spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false diff --git a/samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml b/samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml new file mode 100644 index 000000000000..1e0a7344861d --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml @@ -0,0 +1,54 @@ +openapi: 3.0.3 +info: + title: PreAuthorize scopes + version: 1.0.0 +servers: +- url: / +paths: + /api/overview: + get: + operationId: getOverview + responses: + "200": + description: Ok + security: + - auth0: + - client-data:read + - auth0: + - utility-data:read + tags: + - Overview + x-accepts: + - application/json + x-tags: + - tag: Overview + /api/combined: + get: + operationId: getCombined + responses: + "200": + description: Ok + security: + - auth0: + - client-data:read + - utility-data:read + - auth0: + - other-data:read + tags: + - Overview + x-accepts: + - application/json + x-tags: + - tag: Overview +components: + schemas: {} + securitySchemes: + auth0: + flows: + clientCredentials: + scopes: + client-data:read: Read client data + utility-data:read: Read utility data + other-data:read: Read other data + tokenUrl: https://example.test/oauth/token + type: oauth2 diff --git a/samples/server/others/springboot-pre-authorize/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java b/samples/server/others/springboot-pre-authorize/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java new file mode 100644 index 000000000000..3681f67e7705 --- /dev/null +++ b/samples/server/others/springboot-pre-authorize/src/test/java/org/openapitools/OpenApiGeneratorApplicationTests.java @@ -0,0 +1,13 @@ +package org.openapitools; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class OpenApiGeneratorApplicationTests { + + @Test + void contextLoads() { + } + +} \ No newline at end of file From 1b32d7ec2b61815d22a9bd83e963d5d369c77bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20C=C3=B4t=C3=A9?= Date: Tue, 21 Jul 2026 11:21:39 -0400 Subject: [PATCH 2/5] Update for CI --- docs/generators/java-camel.md | 2 ++ docs/generators/spring.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/generators/java-camel.md b/docs/generators/java-camel.md index 2e2d5191f02f..136d8f17bae8 100644 --- a/docs/generators/java-camel.md +++ b/docs/generators/java-camel.md @@ -105,6 +105,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |src/main/java| |springApiVersion|Value for 'version' attribute in @RequestMapping (for Spring 7 and above).| |null| +|springSecurityAuthorityPrefix|Prefix added to OAuth2/OpenID Connect scopes when generating Spring Security authorities.| |SCOPE_| |substituteGenericPagedModel|Detect schemas that represent paginated responses (an object with a 'content' array property and a 'page' pagination-metadata property) and replace their generated references with PagedModel<T>. By default this uses a generated type in the config package (default 'org.openapitools.configuration'), but `importMappings.PagedModel` can override it to a custom/FQCN-mapped type. The detected page schemas and the pagination metadata schema are suppressed from code generation.| |false| |testOutput|Set output folder for models and APIs tests| |${project.build.directory}/generated-test-sources/openapi| |title|server title name or client service name| |OpenAPI Spring| @@ -127,6 +128,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false| |useSpringBuiltInValidation|Disable `@Validated` at the class level when using built-in validation.| |false| |useSpringController|Annotate the generated API as a Spring Controller| |false| +|useSpringSecurityPreAuthorize|Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes.| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| diff --git a/docs/generators/spring.md b/docs/generators/spring.md index 4f5979324188..ad7198ddac37 100644 --- a/docs/generators/spring.md +++ b/docs/generators/spring.md @@ -120,8 +120,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useSpringBoot3|Generate code and provide dependencies for use with Spring Boot ≥ 3 (use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |true| |useSpringBoot4|Generate code and provide dependencies for use with Spring Boot 4.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false| |useSpringBuiltInValidation|Disable `@Validated` at the class level when using built-in validation.| |false| -|useSpringSecurityPreAuthorize|Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes. Only applies to the spring-boot library.| |false| |useSpringController|Annotate the generated API as a Spring Controller| |false| +|useSpringSecurityPreAuthorize|Generate Spring Security @PreAuthorize annotations from OAuth2/OpenID Connect security scopes.| |false| |useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true| |useTags|use tags for creating interface and controller classnames| |false| |virtualService|Generates the virtual service. For more details refer - https://github.com/virtualansoftware/virtualan/wiki| |false| From ddb451eed080b46acf9226d7491dbe02b12bbd04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20C=C3=B4t=C3=A9?= Date: Tue, 21 Jul 2026 12:26:48 -0400 Subject: [PATCH 3/5] Fix review from comments --- .../codegen/languages/SpringCodegen.java | 32 ++++++++--- .../libraries/spring-boot/pom-sb3.mustache | 4 +- .../libraries/spring-boot/pom-sb4.mustache | 4 +- .../libraries/spring-boot/pom.mustache | 4 +- .../java/spring/SpringCodegenTest.java | 15 ++++- .../3_0/spring/preauthorize-scopes.yaml | 33 +++++++++++ .../others/springboot-pre-authorize/pom.xml | 4 +- .../java/org/openapitools/api/ApiApi.java | 57 +++++++++++++++++++ .../src/main/resources/openapi.yaml | 45 +++++++++++++++ 9 files changed, 182 insertions(+), 16 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java index 73fba593e077..1db92a683a12 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java @@ -1083,9 +1083,12 @@ public void setIsVoid(boolean isVoid) { * entry are combined with AND. */ private void addSpringSecurityPreAuthorize(CodegenOperation codegenOperation) { - Operation rawOperation = findOperation(codegenOperation.operationId); + String originalOperationId = codegenOperation.operationIdOriginal != null + ? codegenOperation.operationIdOriginal + : codegenOperation.operationId; + Operation rawOperation = findOperation(originalOperationId); if (rawOperation == null) { - LOGGER.warn("Could not find OpenAPI operation '{}' while generating @PreAuthorize.", codegenOperation.operationId); + LOGGER.warn("Could not find OpenAPI operation '{}' while generating @PreAuthorize.", originalOperationId); return; } @@ -1112,12 +1115,15 @@ private void addSpringSecurityPreAuthorize(CodegenOperation codegenOperation) { for (Map.Entry> entry : requirement.entrySet()) { SecurityScheme scheme = schemes.get(entry.getKey()); if (scheme == null || (scheme.getType() != SecurityScheme.Type.OAUTH2 - && scheme.getType() != SecurityScheme.Type.OPENIDCONNECT) - || entry.getValue() == null || entry.getValue().isEmpty()) { + && scheme.getType() != SecurityScheme.Type.OPENIDCONNECT)) { return; } - for (String scope : entry.getValue()) { - groupAuthorities.add("hasAuthority('" + springSecurityAuthorityPrefix + scope + "')"); + if (entry.getValue() == null || entry.getValue().isEmpty()) { + groupAuthorities.add("isAuthenticated()"); + } else { + for (String scope : entry.getValue()) { + groupAuthorities.add("hasAuthority('" + escapeSpelStringLiteral(springSecurityAuthorityPrefix + scope) + "')"); + } } } String group = String.join(" and ", groupAuthorities); @@ -1125,10 +1131,22 @@ private void addSpringSecurityPreAuthorize(CodegenOperation codegenOperation) { } if (!alternatives.isEmpty()) { - codegenOperation.vendorExtensions.put("x-spring-security-pre-authorize", String.join(" or ", alternatives)); + codegenOperation.vendorExtensions.put("x-spring-security-pre-authorize", + escapeJavaStringLiteral(String.join(" or ", alternatives))); } } + private String escapeSpelStringLiteral(String value) { + return value.replace("'", "''"); + } + + private String escapeJavaStringLiteral(String value) { + return value.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\r", "\\r") + .replace("\n", "\\n"); + } + private Operation findOperation(String operationId) { if (openAPI.getPaths() == null || operationId == null) { return null; diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache index 77a6b67e90e6..3766bc1f776b 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache @@ -113,8 +113,8 @@ {{#useSpringSecurityPreAuthorize}} - org.springframework.boot - spring-boot-starter-security + org.springframework.security + spring-security-config {{/useSpringSecurityPreAuthorize}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache index 12e7b4d61d03..8522c5c3808b 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache @@ -113,8 +113,8 @@ {{#useSpringSecurityPreAuthorize}} - org.springframework.boot - spring-boot-starter-security + org.springframework.security + spring-security-config {{/useSpringSecurityPreAuthorize}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache index 867333fa1646..e189f85e4624 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache @@ -121,8 +121,8 @@ {{#useSpringSecurityPreAuthorize}} - org.springframework.boot - spring-boot-starter-security + org.springframework.security + spring-security-config {{/useSpringSecurityPreAuthorize}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index e85294401910..6f6e264f85d8 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -4086,11 +4086,24 @@ public void testGeneratePreAuthorizeFromOAuth2Scopes() throws IOException { .assertMethodAnnotations() .containsWithNameAndAttributes("PreAuthorize", ImmutableMap.of("value", "\"(hasAuthority('SCOPE_client-data:read') and hasAuthority('SCOPE_utility-data:read')) or hasAuthority('SCOPE_other-data:read')\"")); + JavaFileAssert.assertThat(files.get("ApiApi.java")) + .assertMethod("getAuthenticated") + .assertMethodAnnotations() + .containsWithNameAndAttributes("PreAuthorize", ImmutableMap.of("value", "\"isAuthenticated()\"")); + JavaFileAssert.assertThat(files.get("ApiApi.java")) + .assertMethod("getNormalized") + .assertMethodAnnotations() + .containsWithNameAndAttributes("PreAuthorize", ImmutableMap.of("value", + "\"hasAuthority('SCOPE_client-data:read')\"")); assertThat(Files.readString(files.get("ApiApi.java").toPath())) - .contains("import org.springframework.security.access.prepost.PreAuthorize;"); + .contains("import org.springframework.security.access.prepost.PreAuthorize;") + .contains("@PreAuthorize(\"hasAuthority('SCOPE_client-data:read''quoted')\")"); assertThat(Files.readString(files.get("OpenApiGeneratorApplication.java").toPath())) .contains("import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;") .contains("@EnableMethodSecurity"); + assertThat(Files.readString(files.get("pom.xml").toPath())) + .contains("spring-security-config") + .doesNotContain("spring-boot-starter-security"); } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml b/modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml index 35cc38adaade..49782478a831 100644 --- a/modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/spring/preauthorize-scopes.yaml @@ -30,6 +30,38 @@ paths: responses: '200': description: Ok + /api/authenticated: + get: + operationId: getAuthenticated + tags: + - Overview + security: + - auth0: [] + responses: + '200': + description: Ok + /api/normalized: + get: + operationId: get-normalized + tags: + - Overview + security: + - auth0: + - client-data:read + responses: + '200': + description: Ok + /api/escaped: + get: + operationId: getEscaped + tags: + - Overview + security: + - auth0: + - "client-data:read'quoted" + responses: + '200': + description: Ok components: securitySchemes: auth0: @@ -39,5 +71,6 @@ components: tokenUrl: https://example.test/oauth/token scopes: client-data:read: Read client data + "client-data:read'quoted": Read quoted client data utility-data:read: Read utility data other-data:read: Read other data diff --git a/samples/server/others/springboot-pre-authorize/pom.xml b/samples/server/others/springboot-pre-authorize/pom.xml index c3d743205ff2..000da963efe1 100644 --- a/samples/server/others/springboot-pre-authorize/pom.xml +++ b/samples/server/others/springboot-pre-authorize/pom.xml @@ -30,8 +30,8 @@ - org.springframework.boot - spring-boot-starter-security + org.springframework.security + spring-security-config org.springframework.boot diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java index 80cbd34bc8bb..63ac5c01c186 100644 --- a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/api/ApiApi.java @@ -29,6 +29,25 @@ default Optional getRequest() { return Optional.empty(); } + String PATH_GET_AUTHENTICATED = "/api/authenticated"; + /** + * GET /api/authenticated + * + * @return Ok (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = ApiApi.PATH_GET_AUTHENTICATED + ) + @PreAuthorize("isAuthenticated()") + default ResponseEntity getAuthenticated( + + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + String PATH_GET_COMBINED = "/api/combined"; /** * GET /api/combined @@ -48,6 +67,44 @@ default ResponseEntity getCombined( } + String PATH_GET_ESCAPED = "/api/escaped"; + /** + * GET /api/escaped + * + * @return Ok (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = ApiApi.PATH_GET_ESCAPED + ) + @PreAuthorize("hasAuthority('SCOPE_client-data:read''quoted')") + default ResponseEntity getEscaped( + + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + + String PATH_GET_NORMALIZED = "/api/normalized"; + /** + * GET /api/normalized + * + * @return Ok (status code 200) + */ + @RequestMapping( + method = RequestMethod.GET, + value = ApiApi.PATH_GET_NORMALIZED + ) + @PreAuthorize("hasAuthority('SCOPE_client-data:read')") + default ResponseEntity getNormalized( + + ) { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + + } + + String PATH_GET_OVERVIEW = "/api/overview"; /** * GET /api/overview diff --git a/samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml b/samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml index 1e0a7344861d..2696f3b9d62f 100644 --- a/samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml +++ b/samples/server/others/springboot-pre-authorize/src/main/resources/openapi.yaml @@ -40,6 +40,50 @@ paths: - application/json x-tags: - tag: Overview + /api/authenticated: + get: + operationId: getAuthenticated + responses: + "200": + description: Ok + security: + - auth0: [] + tags: + - Overview + x-accepts: + - application/json + x-tags: + - tag: Overview + /api/normalized: + get: + operationId: get-normalized + responses: + "200": + description: Ok + security: + - auth0: + - client-data:read + tags: + - Overview + x-accepts: + - application/json + x-tags: + - tag: Overview + /api/escaped: + get: + operationId: getEscaped + responses: + "200": + description: Ok + security: + - auth0: + - client-data:read'quoted + tags: + - Overview + x-accepts: + - application/json + x-tags: + - tag: Overview components: schemas: {} securitySchemes: @@ -48,6 +92,7 @@ components: clientCredentials: scopes: client-data:read: Read client data + client-data:read'quoted: Read quoted client data utility-data:read: Read utility data other-data:read: Read other data tokenUrl: https://example.test/oauth/token From 00fc13d7f413884bba8923312f4140d7b79f97e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20C=C3=B4t=C3=A9?= Date: Tue, 21 Jul 2026 12:49:41 -0400 Subject: [PATCH 4/5] Tentative fix --- .../libraries/spring-boot/openapi2SpringBoot.mustache | 2 +- .../main/java/org/openapitools/OpenApiGeneratorApplication.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache index cdcaccba9ffd..6305e41cf8e0 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache @@ -37,4 +37,4 @@ public class OpenApiGeneratorApplication { } {{/openApiNullable}} -} +} \ No newline at end of file diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java index 08a5f51fa304..07667c95ae9c 100644 --- a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java @@ -29,4 +29,4 @@ public Module jsonNullableModule() { return new JsonNullableModule(); } -} +} \ No newline at end of file From fd9d7b177cfb021f319395cf7133757819854978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20C=C3=B4t=C3=A9?= Date: Tue, 21 Jul 2026 13:18:36 -0400 Subject: [PATCH 5/5] Tentative fix --- .../spring-boot/openapi2SpringBoot.mustache | 15 +++++++++++++++ .../libraries/spring-boot/pom-sb3.mustache | 4 ++-- .../libraries/spring-boot/pom-sb4.mustache | 4 ++-- .../JavaSpring/libraries/spring-boot/pom.mustache | 4 ++-- .../codegen/java/spring/SpringCodegenTest.java | 8 +++++--- .../others/springboot-pre-authorize/pom.xml | 4 ++-- .../openapitools/OpenApiGeneratorApplication.java | 13 +++++++++++++ 7 files changed, 41 insertions(+), 11 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache index 6305e41cf8e0..f9d557d60daa 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/openapi2SpringBoot.mustache @@ -7,7 +7,11 @@ import org.openapitools.jackson.nullable.JsonNullableModule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; {{#useSpringSecurityPreAuthorize}} +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; {{/useSpringSecurityPreAuthorize}} import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -26,6 +30,17 @@ import org.springframework.context.annotation.FullyQualifiedAnnotationBeanNameGe {{/useSpringSecurityPreAuthorize}} public class OpenApiGeneratorApplication { +{{#useSpringSecurityPreAuthorize}} + @Bean + @ConditionalOnMissingBean(SecurityFilterChain.class) + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) + .build(); + } + +{{/useSpringSecurityPreAuthorize}} public static void main(String[] args) { SpringApplication.run(OpenApiGeneratorApplication.class, args); } diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache index 3766bc1f776b..77a6b67e90e6 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb3.mustache @@ -113,8 +113,8 @@ {{#useSpringSecurityPreAuthorize}} - org.springframework.security - spring-security-config + org.springframework.boot + spring-boot-starter-security {{/useSpringSecurityPreAuthorize}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache index 8522c5c3808b..12e7b4d61d03 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom-sb4.mustache @@ -113,8 +113,8 @@ {{#useSpringSecurityPreAuthorize}} - org.springframework.security - spring-security-config + org.springframework.boot + spring-boot-starter-security {{/useSpringSecurityPreAuthorize}} diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache index e189f85e4624..867333fa1646 100644 --- a/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache +++ b/modules/openapi-generator/src/main/resources/JavaSpring/libraries/spring-boot/pom.mustache @@ -121,8 +121,8 @@ {{#useSpringSecurityPreAuthorize}} - org.springframework.security - spring-security-config + org.springframework.boot + spring-boot-starter-security {{/useSpringSecurityPreAuthorize}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 6f6e264f85d8..f28a8f038195 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -4100,10 +4100,12 @@ public void testGeneratePreAuthorizeFromOAuth2Scopes() throws IOException { .contains("@PreAuthorize(\"hasAuthority('SCOPE_client-data:read''quoted')\")"); assertThat(Files.readString(files.get("OpenApiGeneratorApplication.java").toPath())) .contains("import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;") - .contains("@EnableMethodSecurity"); + .contains("@EnableMethodSecurity") + .contains("@ConditionalOnMissingBean(SecurityFilterChain.class)") + .contains(".authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())"); assertThat(Files.readString(files.get("pom.xml").toPath())) - .contains("spring-security-config") - .doesNotContain("spring-boot-starter-security"); + .contains("spring-boot-starter-security") + .doesNotContain("spring-security-config"); } @Test diff --git a/samples/server/others/springboot-pre-authorize/pom.xml b/samples/server/others/springboot-pre-authorize/pom.xml index 000da963efe1..c3d743205ff2 100644 --- a/samples/server/others/springboot-pre-authorize/pom.xml +++ b/samples/server/others/springboot-pre-authorize/pom.xml @@ -30,8 +30,8 @@ - org.springframework.security - spring-security-config + org.springframework.boot + spring-boot-starter-security org.springframework.boot diff --git a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java index 07667c95ae9c..527d920357ff 100644 --- a/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java +++ b/samples/server/others/springboot-pre-authorize/src/main/java/org/openapitools/OpenApiGeneratorApplication.java @@ -4,7 +4,11 @@ import org.openapitools.jackson.nullable.JsonNullableModule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.FilterType; @@ -20,6 +24,15 @@ @EnableMethodSecurity public class OpenApiGeneratorApplication { + @Bean + @ConditionalOnMissingBean(SecurityFilterChain.class) + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) + .build(); + } + public static void main(String[] args) { SpringApplication.run(OpenApiGeneratorApplication.class, args); }