From 79089b260f41390c9c02abc32f48b73cbd8a6a50 Mon Sep 17 00:00:00 2001 From: PhilKes Date: Sun, 21 Jan 2024 11:49:20 +0100 Subject: [PATCH 1/2] Add OAuth2 Client ConnectionDetails --- .../client/OAuth2ClientConnectionDetails.java | 246 ++++++++++++++++++ ... OAuth2ClientConnectionDetailsMapper.java} | 26 +- ...opertiesOAuth2ClientConnectionDetails.java | 61 +++++ ...ReactiveOAuth2ClientAutoConfiguration.java | 3 - .../ReactiveOAuth2ClientConfigurations.java | 22 +- ...ntRegistrationRepositoryConfiguration.java | 22 +- ...h2ClientConnectionDetailsMapperTests.java} | 31 ++- ...iveOAuth2ClientAutoConfigurationTests.java | 82 ++++++ ...istrationRepositoryConfigurationTests.java | 92 +++++++ 9 files changed, 549 insertions(+), 36 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetails.java rename spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/{OAuth2ClientPropertiesMapper.java => OAuth2ClientConnectionDetailsMapper.java} (86%) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/PropertiesOAuth2ClientConnectionDetails.java rename spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/{OAuth2ClientPropertiesMapperTests.java => OAuth2ClientConnectionDetailsMapperTests.java} (95%) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetails.java new file mode 100644 index 000000000000..accac3d1ec21 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetails.java @@ -0,0 +1,246 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.client; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish connections to a OAuth2 authentication server. + * + * @author Philipp Kessler + * @since 3.3.0 + */ +public interface OAuth2ClientConnectionDetails extends ConnectionDetails { + + default Map getRegistrations() { + return new HashMap<>(); + } + + default Map getProviders() { + return new HashMap<>(); + } + + interface Registration { + + /** + * Reference to the OAuth 2.0 provider to use. May reference an element from the + * 'provider' property or used one of the commonly used providers (google, github, + * facebook, okta). + * @return reference to the OAuth 2.0 provider to use. + */ + String getProvider(); + + /** + * Client ID for the registration. + * @return client ID for the registration. + */ + String getClientId(); + + /** + * Client secret of the registration. + * @return client secret of the registration. + */ + String getClientSecret(); + + /** + * Client authentication method. May be left blank when using a pre-defined + * provider. + * @return client authentication method. + */ + String getClientAuthenticationMethod(); + + /** + * Authorization grant type. May be left blank when using a pre-defined provider. + * @return authorization grant type. + */ + String getAuthorizationGrantType(); + + /** + * Redirect URI. May be left blank when using a pre-defined provider. + * @return redirect URI. May be left blank when using a pre-defined provider. + */ + String getRedirectUri(); + + /** + * Authorization scopes. When left blank the provider's default scopes, if any, + * will be used. + * @return authorization scopes. + */ + Set getScopes(); + + /** + * Client name. May be left blank when using a pre-defined provider. + * @return client name. May be left blank when using a pre-defined provider. + */ + String getClientName(); + + static Registration of(String provider, String clientId, String clientSecret, String clientAuthenticationMethod, + String authorizationGrantType, String redirectUri, Set scope, String clientName) { + return new Registration() { + @Override + public String getProvider() { + return provider; + } + + @Override + public String getClientId() { + return clientId; + } + + @Override + public String getClientSecret() { + return clientSecret; + } + + @Override + public String getClientAuthenticationMethod() { + return clientAuthenticationMethod; + } + + @Override + public String getAuthorizationGrantType() { + return authorizationGrantType; + } + + @Override + public String getRedirectUri() { + return redirectUri; + } + + @Override + public Set getScopes() { + return scope; + } + + @Override + public String getClientName() { + return clientName; + } + }; + } + + } + + interface Provider { + + /** + * Authorization URI for the provider. + * @return authorization URI for the provider. + */ + default String getAuthorizationUri() { + return null; + } + + /** + * Token URI for the provider. + * @return token URI for the provider. + */ + default String getTokenUri() { + return null; + } + + /** + * User info URI for the provider. + * @return user info URI for the provider. + */ + default String getUserInfoUri() { + return null; + } + + /** + * User info authentication method for the provider. + * @return user info authentication method for the provider. + */ + default String getUserInfoAuthenticationMethod() { + return null; + } + + /** + * Name of the attribute that will be used to extract the username from the call + * to 'userInfoUri'. + * @return name of the attribute that will be used to extract the username from + * the call * to 'userInfoUri' + */ + default String getUserNameAttribute() { + return null; + } + + /** + * JWK set URI for the provider. + * @return jwk set URI for the provider. + */ + default String getJwkSetUri() { + return null; + } + + /** + * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 + * Authorization Server Metadata endpoint defined by RFC 8414. + * @return uri that can either be an OpenID Connect discovery endpoint or an OAuth + * 2.0 * Authorization Server Metadata endpoint defined by RFC 8414. + */ + default String getIssuerUri() { + return null; + } + + static Provider of(String authorizationUri, String tokenUri, String userInfoUri, + String userInfoAuthenticationMethod, String userNameAttributes, String jwkSetUri, String issuerUri) { + return new Provider() { + @Override + public String getAuthorizationUri() { + return authorizationUri; + } + + @Override + public String getTokenUri() { + return tokenUri; + } + + @Override + public String getUserInfoUri() { + return userInfoUri; + } + + @Override + public String getUserInfoAuthenticationMethod() { + return userInfoAuthenticationMethod; + } + + @Override + public String getUserNameAttribute() { + return userNameAttributes; + } + + @Override + public String getJwkSetUri() { + return jwkSetUri; + } + + @Override + public String getIssuerUri() { + return issuerUri; + } + }; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesMapper.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetailsMapper.java similarity index 86% rename from spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesMapper.java rename to spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetailsMapper.java index 0ff1d412d426..18222a135682 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesMapper.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetailsMapper.java @@ -19,7 +19,8 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails.Provider; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails.Registration; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.core.convert.ConversionException; @@ -33,7 +34,8 @@ import org.springframework.util.StringUtils; /** - * Maps {@link OAuth2ClientProperties} to {@link ClientRegistration ClientRegistrations}. + * Maps {@link OAuth2ClientConnectionDetails} to {@link ClientRegistration + * ClientRegistrations}. * * @author Phillip Webb * @author Thiago Hirata @@ -42,16 +44,16 @@ * @author Andy Wilkinson * @since 3.1.0 */ -public final class OAuth2ClientPropertiesMapper { +public final class OAuth2ClientConnectionDetailsMapper { - private final OAuth2ClientProperties properties; + private final OAuth2ClientConnectionDetails connectionDetails; /** * Creates a new mapper for the given {@code properties}. - * @param properties the properties to map + * @param connectionDetails the properties to map */ - public OAuth2ClientPropertiesMapper(OAuth2ClientProperties properties) { - this.properties = properties; + public OAuth2ClientConnectionDetailsMapper(OAuth2ClientConnectionDetails connectionDetails) { + this.connectionDetails = connectionDetails; } /** @@ -60,14 +62,14 @@ public OAuth2ClientPropertiesMapper(OAuth2ClientProperties properties) { */ public Map asClientRegistrations() { Map clientRegistrations = new HashMap<>(); - this.properties.getRegistration() + this.connectionDetails.getRegistrations() .forEach((key, value) -> clientRegistrations.put(key, - getClientRegistration(key, value, this.properties.getProvider()))); + getClientRegistration(key, value, this.connectionDetails.getProviders()))); return clientRegistrations; } - private static ClientRegistration getClientRegistration(String registrationId, - OAuth2ClientProperties.Registration properties, Map providers) { + private static ClientRegistration getClientRegistration(String registrationId, Registration properties, + Map providers) { Builder builder = getBuilderFromIssuerIfPossible(registrationId, properties.getProvider(), providers); if (builder == null) { builder = getBuilder(registrationId, properties.getProvider(), providers); @@ -82,7 +84,7 @@ private static ClientRegistration getClientRegistration(String registrationId, .as(AuthorizationGrantType::new) .to(builder::authorizationGrantType); map.from(properties::getRedirectUri).to(builder::redirectUri); - map.from(properties::getScope).as(StringUtils::toStringArray).to(builder::scope); + map.from(properties::getScopes).as(StringUtils::toStringArray).to(builder::scope); map.from(properties::getClientName).to(builder::clientName); return builder.build(); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/PropertiesOAuth2ClientConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/PropertiesOAuth2ClientConnectionDetails.java new file mode 100644 index 000000000000..37e1c6a4f00b --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/PropertiesOAuth2ClientConnectionDetails.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.client; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +/** + * Adapts {@link OAuth2ClientProperties} to {@link OAuth2ClientConnectionDetails}. + * + * @author Philipp Kessler + * @since 3.3.0 + */ +public class PropertiesOAuth2ClientConnectionDetails implements OAuth2ClientConnectionDetails { + + private final OAuth2ClientProperties properties; + + public PropertiesOAuth2ClientConnectionDetails(OAuth2ClientProperties properties) { + this.properties = properties; + } + + @Override + public Map getRegistrations() { + return this.properties.getRegistration() + .entrySet() + .stream() + .collect(Collectors.toMap(Entry::getKey, (entry) -> { + OAuth2ClientProperties.Registration registration = entry.getValue(); + return Registration.of(registration.getProvider(), registration.getClientId(), + registration.getClientSecret(), registration.getClientAuthenticationMethod(), + registration.getAuthorizationGrantType(), registration.getRedirectUri(), + registration.getScope(), registration.getClientName()); + })); + } + + @Override + public Map getProviders() { + return this.properties.getProvider().entrySet().stream().collect(Collectors.toMap(Entry::getKey, (entry) -> { + OAuth2ClientProperties.Provider provider = entry.getValue(); + return Provider.of(provider.getAuthorizationUri(), provider.getTokenUri(), provider.getUserInfoUri(), + provider.getUserInfoAuthenticationMethod(), provider.getUserNameAttribute(), + provider.getJwkSetUri(), provider.getIssuerUri()); + })); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java index f5d645e0086f..a6a3594b5531 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfiguration.java @@ -23,9 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Import; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; @@ -39,7 +37,6 @@ * @since 2.1.0 */ @AutoConfiguration(before = ReactiveSecurityAutoConfiguration.class) -@EnableConfigurationProperties(OAuth2ClientProperties.class) @Conditional(ReactiveOAuth2ClientAutoConfiguration.NonServletApplicationCondition.class) @ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, ClientRegistration.class }) @Import({ ReactiveOAuth2ClientConfigurations.ReactiveClientRegistrationRepositoryConfiguration.class, diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java index 8eb3871b9377..46381aeea85c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientConfigurations.java @@ -23,8 +23,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetailsMapper; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper; +import org.springframework.boot.autoconfigure.security.oauth2.client.PropertiesOAuth2ClientConnectionDetails; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -48,14 +51,23 @@ class ReactiveOAuth2ClientConfigurations { @Configuration(proxyBeanMethods = false) - @Conditional(ClientsConfiguredCondition.class) - @ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class) + @EnableConfigurationProperties(OAuth2ClientProperties.class) static class ReactiveClientRegistrationRepositoryConfiguration { @Bean - InMemoryReactiveClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) { + @ConditionalOnMissingBean(OAuth2ClientConnectionDetails.class) + @Conditional(ClientsConfiguredCondition.class) + PropertiesOAuth2ClientConnectionDetails oAuth2ClientConnectionDetails(OAuth2ClientProperties properties) { + return new PropertiesOAuth2ClientConnectionDetails(properties); + } + + @Bean + @ConditionalOnBean(OAuth2ClientConnectionDetails.class) + @ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class) + InMemoryReactiveClientRegistrationRepository clientRegistrationRepository( + OAuth2ClientConnectionDetails connectionDetails) { List registrations = new ArrayList<>( - new OAuth2ClientPropertiesMapper(properties).asClientRegistrations().values()); + new OAuth2ClientConnectionDetailsMapper(connectionDetails).asClientRegistrations().values()); return new InMemoryReactiveClientRegistrationRepository(registrations); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfiguration.java index 09855253ad1a..5c73cb2e27a5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfiguration.java @@ -19,10 +19,13 @@ import java.util.ArrayList; import java.util.List; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetailsMapper; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; -import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper; +import org.springframework.boot.autoconfigure.security.oauth2.client.PropertiesOAuth2ClientConnectionDetails; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; @@ -32,21 +35,28 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; /** - * {@link Configuration @Configuration} used to map {@link OAuth2ClientProperties} to - * client registrations. + * {@link Configuration @Configuration} used to map {@link OAuth2ClientConnectionDetails} + * to client registrations. * * @author Madhura Bhave */ @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(OAuth2ClientProperties.class) -@Conditional(ClientsConfiguredCondition.class) class OAuth2ClientRegistrationRepositoryConfiguration { @Bean + @ConditionalOnMissingBean(OAuth2ClientConnectionDetails.class) + @Conditional(ClientsConfiguredCondition.class) + PropertiesOAuth2ClientConnectionDetails oAuth2ClientConnectionDetails(OAuth2ClientProperties properties) { + return new PropertiesOAuth2ClientConnectionDetails(properties); + } + + @Bean + @ConditionalOnBean(OAuth2ClientConnectionDetails.class) @ConditionalOnMissingBean(ClientRegistrationRepository.class) - InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) { + InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientConnectionDetails connectionDetails) { List registrations = new ArrayList<>( - new OAuth2ClientPropertiesMapper(properties).asClientRegistrations().values()); + new OAuth2ClientConnectionDetailsMapper(connectionDetails).asClientRegistrations().values()); return new InMemoryClientRegistrationRepository(registrations); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesMapperTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetailsMapperTests.java similarity index 95% rename from spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesMapperTests.java rename to spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetailsMapperTests.java index 3bcb4663a40c..c076c5fa140b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesMapperTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientConnectionDetailsMapperTests.java @@ -43,14 +43,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** - * Tests for {@link OAuth2ClientPropertiesMapper}. + * Tests for {@link OAuth2ClientConnectionDetailsMapper}. * * @author Phillip Webb * @author Madhura Bhave * @author Thiago Hirata * @author HaiTao Zhang + * @author Philipp Kessler */ -class OAuth2ClientPropertiesMapperTests { +class OAuth2ClientConnectionDetailsMapperTests { private MockWebServer server; @@ -70,7 +71,8 @@ void getClientRegistrationsWhenUsingDefinedProviderShouldAdapt() { registration.setClientName("clientName"); properties.getRegistration().put("registration", registration); properties.getProvider().put("provider", provider); - Map registrations = new OAuth2ClientPropertiesMapper(properties) + Map registrations = new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) .asClientRegistrations(); ClientRegistration adapted = registrations.get("registration"); ProviderDetails adaptedProvider = adapted.getProviderDetails(); @@ -102,7 +104,8 @@ void getClientRegistrationsWhenUsingCommonProviderShouldAdapt() { registration.setClientId("clientId"); registration.setClientSecret("clientSecret"); properties.getRegistration().put("registration", registration); - Map registrations = new OAuth2ClientPropertiesMapper(properties) + Map registrations = new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) .asClientRegistrations(); ClientRegistration adapted = registrations.get("registration"); ProviderDetails adaptedProvider = adapted.getProviderDetails(); @@ -130,7 +133,8 @@ void getClientRegistrationsWhenUsingCommonProviderWithOverrideShouldAdapt() { OAuth2ClientProperties.Registration registration = createRegistration("google"); registration.setClientName("clientName"); properties.getRegistration().put("registration", registration); - Map registrations = new OAuth2ClientPropertiesMapper(properties) + Map registrations = new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) .asClientRegistrations(); ClientRegistration adapted = registrations.get("registration"); ProviderDetails adaptedProvider = adapted.getProviderDetails(); @@ -161,7 +165,9 @@ void getClientRegistrationsWhenUnknownProviderShouldThrowException() { registration.setProvider("missing"); properties.getRegistration().put("registration", registration); assertThatIllegalStateException() - .isThrownBy(() -> new OAuth2ClientPropertiesMapper(properties).asClientRegistrations()) + .isThrownBy(() -> new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) + .asClientRegistrations()) .withMessageContaining("Unknown provider ID 'missing'"); } @@ -172,7 +178,8 @@ void getClientRegistrationsWhenProviderNotSpecifiedShouldUseRegistrationId() { registration.setClientId("clientId"); registration.setClientSecret("clientSecret"); properties.getRegistration().put("google", registration); - Map registrations = new OAuth2ClientPropertiesMapper(properties) + Map registrations = new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) .asClientRegistrations(); ClientRegistration adapted = registrations.get("google"); ProviderDetails adaptedProvider = adapted.getProviderDetails(); @@ -201,7 +208,9 @@ void getClientRegistrationsWhenProviderNotSpecifiedAndUnknownProviderShouldThrow OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration(); properties.getRegistration().put("missing", registration); assertThatIllegalStateException() - .isThrownBy(() -> new OAuth2ClientPropertiesMapper(properties).asClientRegistrations()) + .isThrownBy(() -> new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) + .asClientRegistrations()) .withMessageContaining("Provider ID must be specified for client registration 'missing'"); } @@ -250,7 +259,8 @@ void oidcProviderConfigurationWithCustomConfigurationOverridesProviderDefaults() OAuth2ClientProperties properties = new OAuth2ClientProperties(); properties.getProvider().put("okta-oidc", provider); properties.getRegistration().put("okta", registration); - Map registrations = new OAuth2ClientPropertiesMapper(properties) + Map registrations = new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) .asClientRegistrations(); ClientRegistration adapted = registrations.get("okta"); ProviderDetails providerDetails = adapted.getProviderDetails(); @@ -301,7 +311,8 @@ private void testIssuerConfiguration(OAuth2ClientProperties.Registration registr provider.setIssuerUri(issuer); properties.getProvider().put(providerId, provider); properties.getRegistration().put("okta", registration); - Map registrations = new OAuth2ClientPropertiesMapper(properties) + Map registrations = new OAuth2ClientConnectionDetailsMapper( + new PropertiesOAuth2ClientConnectionDetails(properties)) .asClientRegistrations(); ClientRegistration adapted = registrations.get("okta"); ProviderDetails providerDetails = adapted.getProviderDetails(); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java index 270ac7029ae0..19b71ffdd8a6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/reactive/ReactiveOAuth2ClientAutoConfigurationTests.java @@ -18,12 +18,17 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails; +import org.springframework.boot.autoconfigure.security.oauth2.client.PropertiesOAuth2ClientConnectionDetails; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; @@ -94,6 +99,22 @@ void clientRegistrationRepositoryBeanShouldBeCreatedWhenPropertiesPresent() { }); } + @Test + void clientRegistrationRepositoryBeanShouldBeCreatedWhenConnectionDetailsPresent() { + this.contextRunner.withUserConfiguration(ConnectionDetailsClientConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(OAuth2ClientConnectionDetails.class) + .doesNotHaveBean(PropertiesOAuth2ClientConnectionDetails.class); + ReactiveClientRegistrationRepository repo = context.getBean(ReactiveClientRegistrationRepository.class); + ClientRegistration registration = repo.findByRegistrationId("oauth2-client").block(Duration.ofSeconds(30)); + assertThat(registration).isNotNull(); + assertThat(registration.getClientName()).isEqualTo("client"); + assertThat(registration.getClientId()).isEqualTo("client-id"); + assertThat(registration.getClientSecret()).isEqualTo("client-secret"); + assertThat(registration.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(registration.getScopes()).contains("openid", "some-scope"); + }); + } + @Test void authorizedClientServiceAndRepositoryBeansAreConditionalOnClientRegistrationRepository() { this.contextRunner.run((context) -> { @@ -190,6 +211,67 @@ private boolean hasFilter(AssertableReactiveWebApplicationContext context, Class return filters.stream().anyMatch(filter::isInstance); } + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsClientConfiguration { + + @Bean + OAuth2ClientConnectionDetails oAuth2ClientConnectionDetails() { + return new OAuth2ClientConnectionDetails() { + @Override + public Map getRegistrations() { + Registration registration = new Registration() { + @Override + public String getProvider() { + return "github"; + } + + @Override + public String getClientId() { + return "client-id"; + } + + @Override + public String getClientSecret() { + return "client-secret"; + } + + @Override + public String getClientAuthenticationMethod() { + return null; + } + + @Override + public String getAuthorizationGrantType() { + return AuthorizationGrantType.AUTHORIZATION_CODE.getValue(); + } + + @Override + public String getRedirectUri() { + return null; + } + + @Override + public Set getScopes() { + return Set.of("openid", "some-scope"); + } + + @Override + public String getClientName() { + return "client"; + } + }; + return Map.of("oauth2-client", registration); + } + + @Override + public Map getProviders() { + return Collections.emptyMap(); + } + }; + } + + } + @Configuration(proxyBeanMethods = false) static class ReactiveClientRepositoryConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java index ba567dbc5842..6dadc5398351 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/servlet/OAuth2ClientRegistrationRepositoryConfigurationTests.java @@ -16,11 +16,22 @@ package org.springframework.boot.autoconfigure.security.oauth2.client.servlet; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; +import org.springframework.boot.autoconfigure.security.oauth2.client.PropertiesOAuth2ClientConnectionDetails; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; import static org.assertj.core.api.Assertions.assertThat; @@ -54,4 +65,85 @@ void clientRegistrationRepositoryBeanShouldBeCreatedWhenPropertiesPresent() { }); } + @Test + void clientRegistrationRepositoryBeanShouldBeCreatedWhenConnectionDetailsPresent() { + this.contextRunner + .withUserConfiguration(ConnectionDetailsClientConfiguration.class, + OAuth2ClientRegistrationRepositoryConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(OAuth2ClientConnectionDetails.class) + .doesNotHaveBean(PropertiesOAuth2ClientConnectionDetails.class); + ClientRegistrationRepository repo = context.getBean(ClientRegistrationRepository.class); + ClientRegistration registration = repo.findByRegistrationId("oauth2-client"); + assertThat(registration).isNotNull(); + assertThat(registration.getClientName()).isEqualTo("client"); + assertThat(registration.getClientId()).isEqualTo("client-id"); + assertThat(registration.getClientSecret()).isEqualTo("client-secret"); + assertThat(registration.getAuthorizationGrantType()) + .isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(registration.getScopes()).contains("openid", "some-scope"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetailsClientConfiguration { + + @Bean + OAuth2ClientConnectionDetails oAuth2ClientConnectionDetails() { + return new OAuth2ClientConnectionDetails() { + @Override + public Map getRegistrations() { + Registration registration = new Registration() { + @Override + public String getProvider() { + return "github"; + } + + @Override + public String getClientId() { + return "client-id"; + } + + @Override + public String getClientSecret() { + return "client-secret"; + } + + @Override + public String getClientAuthenticationMethod() { + return null; + } + + @Override + public String getAuthorizationGrantType() { + return AuthorizationGrantType.AUTHORIZATION_CODE.getValue(); + } + + @Override + public String getRedirectUri() { + return null; + } + + @Override + public Set getScopes() { + return Set.of("openid", "some-scope"); + } + + @Override + public String getClientName() { + return "client"; + } + }; + return Map.of("oauth2-client", registration); + } + + @Override + public Map getProviders() { + return Collections.emptyMap(); + } + }; + } + + } + } From 2a13122cb4762dc45e1200347eed7cd78b3a109d Mon Sep 17 00:00:00 2001 From: PhilKes Date: Sun, 21 Jan 2024 11:49:38 +0100 Subject: [PATCH 2/2] Add Keycloak Docker compose support --- .../DockerComposeConnectionSource.java | 1 + ...DockerComposeConnectionDetailsFactory.java | 166 ++++++++++++++++++ .../oauth2/client/package-info.java | 20 +++ .../main/resources/META-INF/spring.factories | 3 +- ...rComposeConnectionDetailsFactoryTests.java | 60 +++++++ ...ectionDetailsFactoryIntegrationTests.java} | 4 +- .../oauth2/client/keycloak-compose.yaml | 15 ++ .../testcontainers/DockerImageNames.java | 10 ++ 8 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/package-info.java create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactoryTests.java rename spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/{RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java => KeycloakDockerComposeConnectionDetailsFactoryIntegrationTests.java} (91%) create mode 100644 spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oauth2/client/keycloak-compose.yaml diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java index 1009069dcbce..b3925b0beffd 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/DockerComposeConnectionSource.java @@ -30,6 +30,7 @@ */ public final class DockerComposeConnectionSource { + // TODO: add list of other runningServices private final RunningService runningService; /** diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactory.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactory.java new file mode 100644 index 000000000000..e7891be5ac37 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactory.java @@ -0,0 +1,166 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.oauth2.client; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.boot.autoconfigure.data.redis.RedisConnectionDetails; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails; +import org.springframework.boot.docker.compose.core.RunningService; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory; +import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource; + +/** + * {@link DockerComposeConnectionDetailsFactory} to create + * {@link OAuth2ClientConnectionDetails} for a {@code keycloak} service. + * + * @author Philipp Kessler + */ +class KeycloakDockerComposeConnectionDetailsFactory + extends DockerComposeConnectionDetailsFactory { + + KeycloakDockerComposeConnectionDetailsFactory() { + super("keycloak/keycloak"); + } + + @Override + protected OAuth2ClientConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) { + return new KeycloakDockerComposeConnectionDetails(source.getRunningService()); + } + + /** + * {@link RedisConnectionDetails} backed by a {@code redis} {@link RunningService}. + */ + static class KeycloakDockerComposeConnectionDetails extends DockerComposeConnectionDetails + implements OAuth2ClientConnectionDetails { + + private static final String KC_HOSTNAME = "KC_HOSTNAME"; + + private static final String KC_HOSTNAME_DEFAULT = "0.0.0.0"; + + private static final String KC_REALM_DEFAULT = "master"; + + private static final String KC_HTTPS_CERTIFICATE_FILE = "KC_HTTPS_CERTIFICATE_FILE"; + + private static final Integer KC_PORT_HTTP_DEFAULT = 8080; + + private static final Integer KC_PORT_HTTPS_DEFAULT = 8443; + + private static final String CLIENT_SCOPES_DEFAULT = "openid"; + + private static final String REGISTRATION_ID_DEFAULT = "keycloak"; + + private static final String REALM_LABEL = "org.springframework.boot.security.oauth2.client.keycloak.realm"; + + private static final String CLIENT_ID_LABEL = "org.springframework.boot.security.oauth2.client.id"; + + private static final String CLIENT_SECRET_LABEL = "org.springframework.boot.security.oauth2.client.secret"; + + private static final String CLIENT_SCOPES_LABEL = "org.springframework.boot.security.oauth2.client.scopes"; + + public static final String PROVIDER_DEFAULT = "keycloak"; + + private final Map registrations; + + private final Map providers; + + KeycloakDockerComposeConnectionDetails(RunningService service) { + super(service); + this.registrations = new HashMap<>(); + this.providers = new HashMap<>(); + Map env = service.env(); + String host = env.getOrDefault(KC_HOSTNAME, KC_HOSTNAME_DEFAULT); + String httpsCertFile = env.getOrDefault(KC_HTTPS_CERTIFICATE_FILE, null); + boolean isHttpsEnabled = httpsCertFile != null && httpsCertFile.isEmpty() && httpsCertFile.isBlank(); + Integer port = isHttpsEnabled ? KC_PORT_HTTPS_DEFAULT : KC_PORT_HTTP_DEFAULT; + Integer actualPort = service.ports().get(port); + Map labels = service.labels(); + String realm = labels.getOrDefault(REALM_LABEL, KC_REALM_DEFAULT); + Provider provider = new Provider() { + @Override + public String getIssuerUri() { + return "%s://%s:%s/realms/%s".formatted(isHttpsEnabled ? "https" : "http", host, actualPort, realm); + } + }; + this.providers.put(PROVIDER_DEFAULT, provider); + String registrationId = labels.getOrDefault(CLIENT_ID_LABEL, REGISTRATION_ID_DEFAULT); + String clientSecret = labels.getOrDefault(CLIENT_SECRET_LABEL, null); + Set scopes = Arrays + .stream(labels.getOrDefault(CLIENT_SCOPES_LABEL, CLIENT_SCOPES_DEFAULT).split(",")) + .collect(Collectors.toSet()); + Registration registration = new Registration() { + + @Override + public String getProvider() { + return PROVIDER_DEFAULT; + } + + @Override + public String getClientId() { + return registrationId; + } + + @Override + public String getClientSecret() { + return clientSecret; + } + + @Override + public String getClientAuthenticationMethod() { + return "client_secret_basic"; + } + + @Override + public String getAuthorizationGrantType() { + return "authorization_code"; + } + + @Override + public String getRedirectUri() { + return null; + } + + @Override + public Set getScopes() { + return scopes; + } + + @Override + public String getClientName() { + return registrationId; + } + }; + this.registrations.put(registrationId, registration); + } + + @Override + public Map getRegistrations() { + return this.registrations; + } + + @Override + public Map getProviders() { + return this.providers; + } + + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/package-info.java b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/package-info.java new file mode 100644 index 000000000000..afdb1296f3b1 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for docker compose Keycloak service connections. + */ +package org.springframework.boot.docker.compose.service.connection.oauth2.client; diff --git a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories index d5b70a5f7cc3..59d38e2631ad 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories @@ -32,4 +32,5 @@ org.springframework.boot.docker.compose.service.connection.rabbit.RabbitDockerCo org.springframework.boot.docker.compose.service.connection.redis.RedisDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.sqlserver.SqlServerJdbcDockerComposeConnectionDetailsFactory,\ org.springframework.boot.docker.compose.service.connection.sqlserver.SqlServerR2dbcDockerComposeConnectionDetailsFactory,\ -org.springframework.boot.docker.compose.service.connection.zipkin.ZipkinDockerComposeConnectionDetailsFactory +org.springframework.boot.docker.compose.service.connection.zipkin.ZipkinDockerComposeConnectionDetailsFactory,\ +org.springframework.boot.docker.compose.service.connection.oauth2.client.KeycloakDockerComposeConnectionDetailsFactory diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactoryTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactoryTests.java new file mode 100644 index 000000000000..58d5fbc0bdad --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/oauth2/client/KeycloakDockerComposeConnectionDetailsFactoryTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docker.compose.service.connection.oauth2.client; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails.Provider; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConnectionDetails.Registration; +import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIntegrationTests; +import org.springframework.boot.testsupport.testcontainers.DockerImageNames; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link KeycloakDockerComposeConnectionDetailsFactory}. + * + * @author Philipp Kessler + */ +class KeycloakDockerComposeConnectionDetailsFactoryTests extends AbstractDockerComposeIntegrationTests { + + KeycloakDockerComposeConnectionDetailsFactoryTests() { + super("keycloak-compose.yaml", DockerImageNames.keycloak()); + } + + @Test + void runCreatesConnectionDetails() { + OAuth2ClientConnectionDetails connectionDetails = run(OAuth2ClientConnectionDetails.class); + Map registrations = connectionDetails.getRegistrations(); + Map providers = connectionDetails.getProviders(); + assertThat(registrations).isNotNull(); + assertThat(providers).isNotNull(); + assertThat(registrations).containsKey("keycloak-client"); + assertThat(providers).containsKey("keycloak"); + Registration registration = registrations.get("keycloak-client"); + assertThat(registration.getProvider()).isEqualTo("keycloak"); + assertThat(registration.getClientSecret()).isEqualTo("secret"); + assertThat(registration.getScopes()).containsExactly("openid", "some_scope"); + Provider provider = providers.get("keycloak"); + assertThat(provider.getIssuerUri()).startsWith("http://"); + assertThat(provider.getIssuerUri()).endsWith("/realms/KeycloakRealm"); + } + +} diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/KeycloakDockerComposeConnectionDetailsFactoryIntegrationTests.java similarity index 91% rename from spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java rename to spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/KeycloakDockerComposeConnectionDetailsFactoryIntegrationTests.java index 720f6a1d940f..ecc844c97953 100644 --- a/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/RedisDockerComposeConnectionDetailsFactoryIntegrationTests.java +++ b/spring-boot-project/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/redis/KeycloakDockerComposeConnectionDetailsFactoryIntegrationTests.java @@ -32,9 +32,9 @@ * @author Andy Wilkinson * @author Phillip Webb */ -class RedisDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { +class KeycloakDockerComposeConnectionDetailsFactoryIntegrationTests extends AbstractDockerComposeIntegrationTests { - RedisDockerComposeConnectionDetailsFactoryIntegrationTests() { + KeycloakDockerComposeConnectionDetailsFactoryIntegrationTests() { super("redis-compose.yaml", DockerImageNames.redis()); } diff --git a/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oauth2/client/keycloak-compose.yaml b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oauth2/client/keycloak-compose.yaml new file mode 100644 index 000000000000..f93638108fb3 --- /dev/null +++ b/spring-boot-project/spring-boot-docker-compose/src/test/resources/org/springframework/boot/docker/compose/service/connection/oauth2/client/keycloak-compose.yaml @@ -0,0 +1,15 @@ +services: + keycloak: + image: '{imageName}' + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + ports: + - "8080:8080" + command: + - start-dev + labels: + org.springframework.boot.security.oauth2.client.id: keycloak-client + org.springframework.boot.security.oauth2.client.secret: secret + org.springframework.boot.security.oauth2.client.scopes: openid,some_scope + org.springframework.boot.security.oauth2.client.keycloak.realm: KeycloakRealm diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java index 84cacff6b4f8..fb98284b6ff5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/testcontainers/DockerImageNames.java @@ -42,6 +42,8 @@ public final class DockerImageNames { private static final String KAFKA_VERSION = "7.4.0"; + private static final String KEYCLOAK_VERSION = "23.0"; + private static final String MARIADB_VERSION = "10.10"; private static final String MONGO_VERSION = "5.0.17"; @@ -139,6 +141,14 @@ public static DockerImageName kafka() { return DockerImageName.parse("confluentinc/cp-kafka").withTag(KAFKA_VERSION); } + /** + * Return a {@link DockerImageName} suitable for running Keycloak. + * @return a docker image name for running Keycloak + */ + public static DockerImageName keycloak() { + return DockerImageName.parse("keycloak/keycloak").withTag(KEYCLOAK_VERSION); + } + /** * Return a {@link DockerImageName} suitable for running OpenLDAP. * @return a docker image name for running OpenLDAP