Skip to content

Commit deb74fb

Browse files
committed
fix(kotlin-spring): use modern OAuth2 client config for Spring Boot 4 spring-cloud library
Add useSpringBoot4 conditionals to clientConfiguration.mustache so SB4 uses OAuth2AuthorizedClientManager instead of legacy OAuth2FeignRequestInterceptor and *ResourceDetails classes that don't exist in Spring Boot 4.
1 parent 3824e66 commit deb74fb

2 files changed

Lines changed: 127 additions & 5 deletions

File tree

modules/openapi-generator/src/main/resources/kotlin-spring/libraries/spring-cloud/clientConfiguration.mustache

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@ import feign.auth.BasicAuthRequestInterceptor
88
import org.springframework.beans.factory.annotation.Value
99
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
1010
{{/-first}}
11+
{{^useSpringBoot4}}
1112
{{#isOAuth}}
1213
import org.springframework.boot.context.properties.ConfigurationProperties
1314
{{/isOAuth}}
15+
{{/useSpringBoot4}}
1416
{{/authMethods}}
17+
{{^useSpringBoot4}}
1518
import org.springframework.boot.context.properties.EnableConfigurationProperties
16-
{{#authMethods}}
17-
{{#-first}}
19+
{{/useSpringBoot4}}
20+
{{#hasAuthMethods}}
1821
import org.springframework.context.annotation.Bean
19-
{{/-first}}
20-
{{/authMethods}}
22+
{{/hasAuthMethods}}
2123
import org.springframework.context.annotation.Configuration
24+
{{^useSpringBoot4}}
2225
{{#authMethods}}
2326
{{#isOAuth}}
2427
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor
@@ -38,9 +41,29 @@ import org.springframework.security.oauth2.client.token.grant.password.ResourceO
3841
{{/isPassword}}
3942
{{/isOAuth}}
4043
{{/authMethods}}
44+
{{/useSpringBoot4}}
45+
{{#useSpringBoot4}}
46+
{{#hasOAuthMethods}}
47+
import org.springframework.security.authentication.AnonymousAuthenticationToken
48+
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager
49+
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest
50+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager
51+
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService
52+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
53+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
54+
import org.springframework.security.oauth2.core.OAuth2AccessToken
55+
import org.springframework.security.core.authority.AuthorityUtils
56+
import org.springframework.http.HttpHeaders
57+
58+
import feign.RequestInterceptor
59+
import feign.RequestTemplate
60+
{{/hasOAuthMethods}}
61+
{{/useSpringBoot4}}
4162

4263
@Configuration
64+
{{^useSpringBoot4}}
4365
@EnableConfigurationProperties
66+
{{/useSpringBoot4}}
4467
class ClientConfiguration {
4568
4669
{{#authMethods}}
@@ -70,6 +93,7 @@ class ClientConfiguration {
7093

7194
{{/isApiKey}}
7295
{{#isOAuth}}
96+
{{^useSpringBoot4}}
7397
@Bean
7498
@ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id")
7599
fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(oAuth2ClientContext: OAuth2ClientContext): OAuth2FeignRequestInterceptor {
@@ -127,6 +151,61 @@ class ClientConfiguration {
127151
}
128152

129153
{{/isImplicit}}
154+
{{/useSpringBoot4}}
155+
{{#useSpringBoot4}}
156+
@Bean
157+
@ConditionalOnProperty(prefix = "spring.security.oauth2.client.registration.{{{name}}}{{#lambda.pascalcase}}{{{flow}}}{{/lambda.pascalcase}}", name = ["enabled"], havingValue = "true")
158+
fun {{{flow}}}OAuth2RequestInterceptor({{{flow}}}AuthorizedClientManager: OAuth2AuthorizedClientManager): OAuth2RequestInterceptor {
159+
return OAuth2RequestInterceptor(
160+
OAuth2AuthorizeRequest.withClientRegistrationId("{{{name}}}{{#lambda.pascalcase}}{{{flow}}}{{/lambda.pascalcase}}")
161+
.principal(AnonymousAuthenticationToken(CLIENT_PRINCIPAL_{{#lambda.uppercase}}{{{flow}}}{{/lambda.uppercase}}, CLIENT_PRINCIPAL_{{#lambda.uppercase}}{{{flow}}}{{/lambda.uppercase}}, AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")))
162+
.build(),
163+
{{{flow}}}AuthorizedClientManager
164+
)
165+
}
166+
167+
@Bean
168+
@ConditionalOnProperty(prefix = "spring.security.oauth2.client.registration.{{{name}}}{{#lambda.pascalcase}}{{{flow}}}{{/lambda.pascalcase}}", name = ["enabled"], havingValue = "true")
169+
fun {{{flow}}}AuthorizedClientManager(
170+
clientRegistrationRepository: ClientRegistrationRepository,
171+
authorizedClientService: OAuth2AuthorizedClientService
172+
): OAuth2AuthorizedClientManager {
173+
return AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService)
174+
}
175+
176+
{{/useSpringBoot4}}
130177
{{/isOAuth}}
131178
{{/authMethods}}
132-
}
179+
{{#useSpringBoot4}}
180+
{{#hasOAuthMethods}}
181+
class OAuth2RequestInterceptor(
182+
private val oAuth2AuthorizeRequest: OAuth2AuthorizeRequest,
183+
private val oAuth2AuthorizedClientManager: OAuth2AuthorizedClientManager
184+
) : RequestInterceptor {
185+
186+
override fun apply(template: RequestTemplate) {
187+
template.header(HttpHeaders.AUTHORIZATION, getBearerToken())
188+
}
189+
190+
fun getAccessToken(): OAuth2AccessToken {
191+
val authorizedClient = oAuth2AuthorizedClientManager.authorize(oAuth2AuthorizeRequest)
192+
?: throw OAuth2AuthenticationException("Client failed to authenticate")
193+
return authorizedClient.accessToken
194+
}
195+
196+
fun getBearerToken(): String {
197+
val accessToken = getAccessToken()
198+
return String.format(java.util.Locale.ROOT, "%s %s", accessToken.tokenType?.value, accessToken.tokenValue)
199+
}
200+
}
201+
202+
companion object {
203+
{{#authMethods}}
204+
{{#isOAuth}}
205+
private const val CLIENT_PRINCIPAL_{{#lambda.uppercase}}{{{flow}}}{{/lambda.uppercase}} = "oauth2FeignClient"
206+
{{/isOAuth}}
207+
{{/authMethods}}
208+
}
209+
{{/hasOAuthMethods}}
210+
{{/useSpringBoot4}}
211+
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4967,6 +4967,49 @@ public void shouldDeclareSpringdocVersionWhenSwaggerUIDisabled() throws IOExcept
49674967
pomContent.indexOf("</properties>"));
49684968
assertThat(propertiesBlock).contains("<springdoc-openapi.version>");
49694969
}
4970+
4971+
@Test
4972+
public void shouldNotUseLegacyOAuth2WithSpringBoot4CloudLibrary() throws IOException {
4973+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
4974+
output.deleteOnExit();
4975+
String outputPath = output.getAbsolutePath().replace('\\', '/');
4976+
4977+
final OpenAPI openAPI = TestUtils.parseFlattenSpec(
4978+
"src/test/resources/3_0/petstore-with-fake-endpoints-models-for-testing.yaml");
4979+
final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen();
4980+
codegen.setOpenAPI(openAPI);
4981+
codegen.setOutputDir(output.getAbsolutePath());
4982+
codegen.setLibrary("spring-cloud");
4983+
4984+
codegen.additionalProperties().put(KotlinSpringServerCodegen.USE_SPRING_BOOT4, "true");
4985+
codegen.additionalProperties().put(DOCUMENTATION_PROVIDER, DocumentationProvider.NONE.toCliOptValue());
4986+
codegen.additionalProperties().put(ANNOTATION_LIBRARY, AnnotationLibrary.NONE.toCliOptValue());
4987+
4988+
ClientOptInput input = new ClientOptInput();
4989+
input.openAPI(openAPI);
4990+
input.config(codegen);
4991+
4992+
DefaultGenerator generator = new DefaultGenerator();
4993+
generator.setGenerateMetadata(false);
4994+
generator.opts(input).generate();
4995+
4996+
Path clientConfigPath = Paths.get(outputPath + "/src/main/kotlin/org/openapitools/configuration/ClientConfiguration.kt");
4997+
// Legacy OAuth2 classes must NOT be present
4998+
assertFileNotContains(clientConfigPath, "DefaultOAuth2ClientContext");
4999+
assertFileNotContains(clientConfigPath, "OAuth2FeignRequestInterceptor");
5000+
assertFileNotContains(clientConfigPath, "ClientCredentialsResourceDetails");
5001+
assertFileNotContains(clientConfigPath, "AuthorizationCodeResourceDetails");
5002+
assertFileNotContains(clientConfigPath, "ImplicitResourceDetails");
5003+
assertFileNotContains(clientConfigPath, "ResourceOwnerPasswordResourceDetails");
5004+
5005+
// Modern OAuth2 client classes MUST be present
5006+
assertFileContains(clientConfigPath, "OAuth2AuthorizedClientManager");
5007+
assertFileContains(clientConfigPath, "AuthorizedClientServiceOAuth2AuthorizedClientManager");
5008+
assertFileContains(clientConfigPath, "OAuth2AuthorizeRequest");
5009+
assertFileContains(clientConfigPath, "OAuth2AuthorizedClientService");
5010+
assertFileContains(clientConfigPath, "ClientRegistrationRepository");
5011+
assertFileContains(clientConfigPath, "OAuth2RequestInterceptor");
5012+
}
49705013
}
49715014

49725015

0 commit comments

Comments
 (0)