Create spring-boot-security-oauth2-client module

This commit is contained in:
Andy Wilkinson
2025-03-31 10:39:09 +01:00
committed by Phillip Webb
parent f53b9786e5
commit d5e84c627d
38 changed files with 100 additions and 102 deletions

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* Condition that matches if any {@code spring.security.oauth2.client.registration}
* properties are defined.
*
* @author Madhura Bhave
*/
class ClientsConfiguredCondition extends SpringBootCondition {
private static final Bindable<Map<String, OAuth2ClientProperties.Registration>> STRING_REGISTRATION_MAP = Bindable
.mapOf(String.class, OAuth2ClientProperties.Registration.class);
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Clients Configured Condition");
Map<String, OAuth2ClientProperties.Registration> registrations = getRegistrations(context.getEnvironment());
if (!registrations.isEmpty()) {
return ConditionOutcome.match(message.foundExactly("registered clients " + registrations.values()
.stream()
.map(OAuth2ClientProperties.Registration::getClientId)
.collect(Collectors.joining(", "))));
}
return ConditionOutcome.noMatch(message.notAvailable("registered clients"));
}
private Map<String, OAuth2ClientProperties.Registration> getRegistrations(Environment environment) {
return Binder.get(environment)
.bind("spring.security.oauth2.client.registration", STRING_REGISTRATION_MAP)
.orElse(Collections.emptyMap());
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* Condition that matches if any {@code spring.security.oauth2.client.registration}
* properties are defined.
*
* @author Andy Wilkinson
* @since 4.0.0
*/
@SuppressWarnings("removal")
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(ClientsConfiguredCondition.class)
public @interface ConditionalOnOAuth2ClientRegistrationProperties {
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration.NonReactiveWebApplicationCondition;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for OAuth client support.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 3.5.0
*/
@AutoConfiguration
@Conditional(NonReactiveWebApplicationCondition.class)
@ConditionalOnClass(ClientRegistration.class)
@Import({ OAuth2ClientConfigurations.ClientRegistrationRepositoryConfiguration.class,
OAuth2ClientConfigurations.OAuth2AuthorizedClientServiceConfiguration.class })
public class OAuth2ClientAutoConfiguration {
static class NonReactiveWebApplicationCondition extends NoneNestedConditions {
NonReactiveWebApplicationCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
static class ReactiveWebApplicationCondition {
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
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.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
/**
* Configurations related to auto-configuration of OAuth2 client support.
*
* @author Madhura Bhave
* @author Andy Wilkinson
*/
class OAuth2ClientConfigurations {
@Configuration(proxyBeanMethods = false)
@ConditionalOnOAuth2ClientRegistrationProperties
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
static class ClientRegistrationRepositoryConfiguration {
@Bean
InMemoryClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
new OAuth2ClientPropertiesMapper(properties).asClientRegistrations().values());
return new InMemoryClientRegistrationRepository(registrations);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ClientRegistrationRepository.class)
static class OAuth2AuthorizedClientServiceConfiguration {
@Bean
@ConditionalOnMissingBean
OAuth2AuthorizedClientService authorizedClientService(
ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
}
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
/**
* OAuth 2.0 client properties.
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Artsiom Yudovin
* @author MyeongHyeon Lee
* @author Moritz Halbritter
* @since 4.0.0
*/
@ConfigurationProperties("spring.security.oauth2.client")
public class OAuth2ClientProperties implements InitializingBean {
/**
* OAuth provider details.
*/
private final Map<String, Provider> provider = new HashMap<>();
/**
* OAuth client registrations.
*/
private final Map<String, Registration> registration = new HashMap<>();
public Map<String, Provider> getProvider() {
return this.provider;
}
public Map<String, Registration> getRegistration() {
return this.registration;
}
@Override
public void afterPropertiesSet() {
validate();
}
public void validate() {
getRegistration().forEach(this::validateRegistration);
}
private void validateRegistration(String id, Registration registration) {
if (!StringUtils.hasText(registration.getClientId())) {
throw new IllegalStateException("Client id of registration '%s' must not be empty.".formatted(id));
}
}
/**
* A single client registration.
*/
public static class 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).
*/
private String provider;
/**
* Client ID for the registration.
*/
private String clientId;
/**
* Client secret of the registration.
*/
private String clientSecret;
/**
* Client authentication method. May be left blank when using a pre-defined
* provider.
*/
private String clientAuthenticationMethod;
/**
* Authorization grant type. May be left blank when using a pre-defined provider.
*/
private String authorizationGrantType;
/**
* Redirect URI. May be left blank when using a pre-defined provider.
*/
private String redirectUri;
/**
* Authorization scopes. When left blank the provider's default scopes, if any,
* will be used.
*/
private Set<String> scope;
/**
* Client name. May be left blank when using a pre-defined provider.
*/
private String clientName;
public String getProvider() {
return this.provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
public String getClientId() {
return this.clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return this.clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
public String getClientAuthenticationMethod() {
return this.clientAuthenticationMethod;
}
public void setClientAuthenticationMethod(String clientAuthenticationMethod) {
this.clientAuthenticationMethod = clientAuthenticationMethod;
}
public String getAuthorizationGrantType() {
return this.authorizationGrantType;
}
public void setAuthorizationGrantType(String authorizationGrantType) {
this.authorizationGrantType = authorizationGrantType;
}
public String getRedirectUri() {
return this.redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
public Set<String> getScope() {
return this.scope;
}
public void setScope(Set<String> scope) {
this.scope = scope;
}
public String getClientName() {
return this.clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
}
public static class Provider {
/**
* Authorization URI for the provider.
*/
private String authorizationUri;
/**
* Token URI for the provider.
*/
private String tokenUri;
/**
* User info URI for the provider.
*/
private String userInfoUri;
/**
* User info authentication method for the provider.
*/
private String userInfoAuthenticationMethod;
/**
* Name of the attribute that will be used to extract the username from the call
* to 'userInfoUri'.
*/
private String userNameAttribute;
/**
* JWK set URI for the provider.
*/
private String jwkSetUri;
/**
* URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0
* Authorization Server Metadata endpoint defined by RFC 8414.
*/
private String issuerUri;
public String getAuthorizationUri() {
return this.authorizationUri;
}
public void setAuthorizationUri(String authorizationUri) {
this.authorizationUri = authorizationUri;
}
public String getTokenUri() {
return this.tokenUri;
}
public void setTokenUri(String tokenUri) {
this.tokenUri = tokenUri;
}
public String getUserInfoUri() {
return this.userInfoUri;
}
public void setUserInfoUri(String userInfoUri) {
this.userInfoUri = userInfoUri;
}
public String getUserInfoAuthenticationMethod() {
return this.userInfoAuthenticationMethod;
}
public void setUserInfoAuthenticationMethod(String userInfoAuthenticationMethod) {
this.userInfoAuthenticationMethod = userInfoAuthenticationMethod;
}
public String getUserNameAttribute() {
return this.userNameAttribute;
}
public void setUserNameAttribute(String userNameAttribute) {
this.userNameAttribute = userNameAttribute;
}
public String getJwkSetUri() {
return this.jwkSetUri;
}
public void setJwkSetUri(String jwkSetUri) {
this.jwkSetUri = jwkSetUri;
}
public String getIssuerUri() {
return this.issuerUri;
}
public void setIssuerUri(String issuerUri) {
this.issuerUri = issuerUri;
}
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties.Provider;
import org.springframework.core.convert.ConversionException;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.Builder;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.core.AuthenticationMethod;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.util.StringUtils;
/**
* Maps {@link OAuth2ClientProperties} to {@link ClientRegistration ClientRegistrations}.
*
* @author Phillip Webb
* @author Thiago Hirata
* @author Madhura Bhave
* @author MyeongHyeon Lee
* @author Andy Wilkinson
* @since 4.0.0
*/
public final class OAuth2ClientPropertiesMapper {
private final OAuth2ClientProperties properties;
/**
* Creates a new mapper for the given {@code properties}.
* @param properties the properties to map
*/
public OAuth2ClientPropertiesMapper(OAuth2ClientProperties properties) {
this.properties = properties;
}
/**
* Maps the properties to {@link ClientRegistration ClientRegistrations}.
* @return the mapped {@code ClientRegistrations}
*/
public Map<String, ClientRegistration> asClientRegistrations() {
Map<String, ClientRegistration> clientRegistrations = new HashMap<>();
this.properties.getRegistration()
.forEach((key, value) -> clientRegistrations.put(key,
getClientRegistration(key, value, this.properties.getProvider())));
return clientRegistrations;
}
private static ClientRegistration getClientRegistration(String registrationId,
OAuth2ClientProperties.Registration properties, Map<String, Provider> providers) {
Builder builder = getBuilderFromIssuerIfPossible(registrationId, properties.getProvider(), providers);
if (builder == null) {
builder = getBuilder(registrationId, properties.getProvider(), providers);
}
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(properties::getClientId).to(builder::clientId);
map.from(properties::getClientSecret).to(builder::clientSecret);
map.from(properties::getClientAuthenticationMethod)
.as(ClientAuthenticationMethod::new)
.to(builder::clientAuthenticationMethod);
map.from(properties::getAuthorizationGrantType)
.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::getClientName).to(builder::clientName);
return builder.build();
}
private static Builder getBuilderFromIssuerIfPossible(String registrationId, String configuredProviderId,
Map<String, Provider> providers) {
String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId;
if (providers.containsKey(providerId)) {
Provider provider = providers.get(providerId);
String issuer = provider.getIssuerUri();
if (issuer != null) {
Builder builder = ClientRegistrations.fromIssuerLocation(issuer).registrationId(registrationId);
return getBuilder(builder, provider);
}
}
return null;
}
private static Builder getBuilder(String registrationId, String configuredProviderId,
Map<String, Provider> providers) {
String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId;
CommonOAuth2Provider provider = getCommonProvider(providerId);
if (provider == null && !providers.containsKey(providerId)) {
throw new IllegalStateException(getErrorMessage(configuredProviderId, registrationId));
}
Builder builder = (provider != null) ? provider.getBuilder(registrationId)
: ClientRegistration.withRegistrationId(registrationId);
if (providers.containsKey(providerId)) {
return getBuilder(builder, providers.get(providerId));
}
return builder;
}
private static String getErrorMessage(String configuredProviderId, String registrationId) {
return ((configuredProviderId != null) ? "Unknown provider ID '" + configuredProviderId + "'"
: "Provider ID must be specified for client registration '" + registrationId + "'");
}
private static Builder getBuilder(Builder builder, Provider provider) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(provider::getAuthorizationUri).to(builder::authorizationUri);
map.from(provider::getTokenUri).to(builder::tokenUri);
map.from(provider::getUserInfoUri).to(builder::userInfoUri);
map.from(provider::getUserInfoAuthenticationMethod)
.as(AuthenticationMethod::new)
.to(builder::userInfoAuthenticationMethod);
map.from(provider::getJwkSetUri).to(builder::jwkSetUri);
map.from(provider::getUserNameAttribute).to(builder::userNameAttributeName);
return builder;
}
private static CommonOAuth2Provider getCommonProvider(String providerId) {
try {
return ApplicationConversionService.getSharedInstance().convert(providerId, CommonOAuth2Provider.class);
}
catch (ConversionException ex) {
return null;
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Support for Spring Security's OAuth 2 client.
*/
package org.springframework.boot.security.oauth2.client.autoconfigure;

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure.reactive;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.NoneNestedConditions;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Spring Security's Reactive
* OAuth2 client.
*
* @author Madhura Bhave
* @since 4.0.0
*/
@AutoConfiguration
@Conditional(ReactiveOAuth2ClientAutoConfiguration.NonServletApplicationCondition.class)
@ConditionalOnClass({ Flux.class, ClientRegistration.class })
@Import({ ReactiveOAuth2ClientConfigurations.ReactiveClientRegistrationRepositoryConfiguration.class,
ReactiveOAuth2ClientConfigurations.ReactiveOAuth2AuthorizedClientServiceConfiguration.class })
public class ReactiveOAuth2ClientAutoConfiguration {
static class NonServletApplicationCondition extends NoneNestedConditions {
NonServletApplicationCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
static class ServletApplicationCondition {
}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure.reactive;
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.context.properties.EnableConfigurationProperties;
import org.springframework.boot.security.oauth2.client.autoconfigure.ConditionalOnOAuth2ClientRegistrationProperties;
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties;
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientPropertiesMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
/**
* Reactive OAuth2 Client configurations.
*
* @author Madhura Bhave
*/
class ReactiveOAuth2ClientConfigurations {
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
@ConditionalOnOAuth2ClientRegistrationProperties
@ConditionalOnMissingBean(ReactiveClientRegistrationRepository.class)
static class ReactiveClientRegistrationRepositoryConfiguration {
@Bean
InMemoryReactiveClientRegistrationRepository reactiveClientRegistrationRepository(
OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
new OAuth2ClientPropertiesMapper(properties).asClientRegistrations().values());
return new InMemoryReactiveClientRegistrationRepository(registrations);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBean(ReactiveClientRegistrationRepository.class)
static class ReactiveOAuth2AuthorizedClientServiceConfiguration {
@Bean
@ConditionalOnMissingBean
ReactiveOAuth2AuthorizedClientService reactiveAuthorizedClientService(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure.reactive;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.security.autoconfigure.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.web.server.SecurityWebFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Auto-configuration for reactive web security that uses an OAuth 2 client.
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Andy Wilkinson
* @since 4.0.0
*/
@AutoConfiguration(before = ReactiveSecurityAutoConfiguration.class,
after = ReactiveOAuth2ClientAutoConfiguration.class)
@ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, ServerOAuth2AuthorizedClientRepository.class })
@ConditionalOnBean(ReactiveOAuth2AuthorizedClientService.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class ReactiveOAuth2ClientWebSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ServerOAuth2AuthorizedClientRepository authorizedClientRepository(
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
}
@Bean
@ConditionalOnMissingBean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange((exchange) -> exchange.anyExchange().authenticated());
http.oauth2Login(withDefaults());
http.oauth2Client(withDefaults());
return http.build();
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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 Spring Security's Reactive OAuth 2 client.
*/
package org.springframework.boot.security.oauth2.client.autoconfigure.reactive;

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure.servlet;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.security.autoconfigure.ConditionalOnDefaultWebSecurity;
import org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration;
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
/**
* Auto-configuration for web security that uses an OAuth 2 client.
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Andy Wilkinson
* @since 3.5.0
*/
@AutoConfiguration(before = SecurityAutoConfiguration.class, after = OAuth2ClientAutoConfiguration.class)
@ConditionalOnClass({ EnableWebSecurity.class, OAuth2AuthorizedClientRepository.class })
@ConditionalOnBean(OAuth2AuthorizedClientService.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class OAuth2ClientWebSecurityAutoConfiguration {
@Bean
@ConditionalOnMissingBean
OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class OAuth2SecurityFilterChainConfiguration {
@Bean
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.oauth2Login(withDefaults());
http.oauth2Client(withDefaults());
return http.build();
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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 Spring Security's OAuth 2 client.
*/
package org.springframework.boot.security.oauth2.client.autoconfigure.servlet;

View File

@@ -0,0 +1,4 @@
org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientAutoConfiguration
org.springframework.boot.security.oauth2.client.autoconfigure.reactive.ReactiveOAuth2ClientAutoConfiguration
org.springframework.boot.security.oauth2.client.autoconfigure.reactive.ReactiveOAuth2ClientWebSecurityAutoConfiguration
org.springframework.boot.security.oauth2.client.autoconfigure.servlet.OAuth2ClientWebSecurityAutoConfiguration

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OAuth2ClientAutoConfiguration}.
*
* @author Madhura Bhave
* @author Andy Wilkinson
*/
class OAuth2ClientAutoConfigurationTests {
private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration";
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OAuth2ClientAutoConfiguration.class));
@Test
void beansShouldNotBeCreatedWhenPropertiesAbsent() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ClientRegistrationRepository.class)
.doesNotHaveBean(OAuth2AuthorizedClientService.class));
}
@Test
void beansAreCreatedWhenPropertiesPresent() {
this.contextRunner
.withPropertyValues(REGISTRATION_PREFIX + ".foo.client-id=abcd",
REGISTRATION_PREFIX + ".foo.client-secret=secret", REGISTRATION_PREFIX + ".foo.provider=github")
.run((context) -> {
assertThat(context).hasSingleBean(ClientRegistrationRepository.class);
assertThat(context).hasSingleBean(OAuth2AuthorizedClientService.class);
ClientRegistrationRepository repository = context.getBean(ClientRegistrationRepository.class);
ClientRegistration registration = repository.findByRegistrationId("foo");
assertThat(registration).isNotNull();
assertThat(registration.getClientSecret()).isEqualTo("secret");
});
}
@Test
void clientServiceBeanIsConditionalOnMissingBean() {
this.contextRunner
.withBean("testAuthorizedClientService", OAuth2AuthorizedClientService.class,
() -> mock(OAuth2AuthorizedClientService.class))
.run((context) -> {
assertThat(context).hasSingleBean(OAuth2AuthorizedClientService.class);
assertThat(context).hasBean("testAuthorizedClientService");
});
}
@Test
void clientServiceBeanIsCreatedWithUserDefinedClientRegistrationRepository() {
this.contextRunner
.withBean(ClientRegistrationRepository.class,
() -> new InMemoryClientRegistrationRepository(getClientRegistration("test", "test")))
.run((context) -> assertThat(context).hasSingleBean(OAuth2AuthorizedClientService.class));
}
private ClientRegistration getClientRegistration(String id, String userInfoUri) {
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(id);
builder.clientName("foo")
.clientId("foo")
.clientAuthenticationMethod(
org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("read")
.clientSecret("secret")
.redirectUri("https://redirect-uri.com")
.authorizationUri("https://authorization-uri.com")
.tokenUri("https://token-uri.com")
.userInfoUri(userInfoUri)
.userNameAttributeName("login");
return builder.build();
}
}

View File

@@ -0,0 +1,357 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties.Provider;
import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties.Registration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link OAuth2ClientPropertiesMapper}.
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Thiago Hirata
* @author HaiTao Zhang
*/
class OAuth2ClientPropertiesMapperTests {
private MockWebServer server;
@AfterEach
void cleanup() throws Exception {
if (this.server != null) {
this.server.shutdown();
}
}
@Test
void getClientRegistrationsWhenUsingDefinedProviderShouldAdapt() {
OAuth2ClientProperties properties = new OAuth2ClientProperties();
Provider provider = createProvider();
provider.setUserInfoAuthenticationMethod("form");
OAuth2ClientProperties.Registration registration = createRegistration("provider");
registration.setClientName("clientName");
properties.getRegistration().put("registration", registration);
properties.getProvider().put("provider", provider);
Map<String, ClientRegistration> registrations = new OAuth2ClientPropertiesMapper(properties)
.asClientRegistrations();
ClientRegistration adapted = registrations.get("registration");
ProviderDetails adaptedProvider = adapted.getProviderDetails();
assertThat(adaptedProvider.getAuthorizationUri()).isEqualTo("https://example.com/auth");
assertThat(adaptedProvider.getTokenUri()).isEqualTo("https://example.com/token");
UserInfoEndpoint userInfoEndpoint = adaptedProvider.getUserInfoEndpoint();
assertThat(userInfoEndpoint.getUri()).isEqualTo("https://example.com/info");
assertThat(userInfoEndpoint.getAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.AuthenticationMethod.FORM);
assertThat(userInfoEndpoint.getUserNameAttributeName()).isEqualTo("sub");
assertThat(adaptedProvider.getJwkSetUri()).isEqualTo("https://example.com/jwk");
assertThat(adapted.getRegistrationId()).isEqualTo("registration");
assertThat(adapted.getClientId()).isEqualTo("clientId");
assertThat(adapted.getClientSecret()).isEqualTo("clientSecret");
assertThat(adapted.getClientAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST);
assertThat(adapted.getAuthorizationGrantType())
.isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(adapted.getRedirectUri()).isEqualTo("https://example.com/redirect");
assertThat(adapted.getScopes()).containsExactly("user");
assertThat(adapted.getClientName()).isEqualTo("clientName");
}
@Test
void getClientRegistrationsWhenUsingCommonProviderShouldAdapt() {
OAuth2ClientProperties properties = new OAuth2ClientProperties();
OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration();
registration.setProvider("google");
registration.setClientId("clientId");
registration.setClientSecret("clientSecret");
properties.getRegistration().put("registration", registration);
Map<String, ClientRegistration> registrations = new OAuth2ClientPropertiesMapper(properties)
.asClientRegistrations();
ClientRegistration adapted = registrations.get("registration");
ProviderDetails adaptedProvider = adapted.getProviderDetails();
assertThat(adaptedProvider.getAuthorizationUri()).isEqualTo("https://accounts.google.com/o/oauth2/v2/auth");
assertThat(adaptedProvider.getTokenUri()).isEqualTo("https://www.googleapis.com/oauth2/v4/token");
UserInfoEndpoint userInfoEndpoint = adaptedProvider.getUserInfoEndpoint();
assertThat(userInfoEndpoint.getUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/userinfo");
assertThat(userInfoEndpoint.getUserNameAttributeName()).isEqualTo(IdTokenClaimNames.SUB);
assertThat(adaptedProvider.getJwkSetUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/certs");
assertThat(adapted.getRegistrationId()).isEqualTo("registration");
assertThat(adapted.getClientId()).isEqualTo("clientId");
assertThat(adapted.getClientSecret()).isEqualTo("clientSecret");
assertThat(adapted.getClientAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
assertThat(adapted.getAuthorizationGrantType())
.isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(adapted.getRedirectUri()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}");
assertThat(adapted.getScopes()).containsExactly("openid", "profile", "email");
assertThat(adapted.getClientName()).isEqualTo("Google");
}
@Test
void getClientRegistrationsWhenUsingCommonProviderWithOverrideShouldAdapt() {
OAuth2ClientProperties properties = new OAuth2ClientProperties();
OAuth2ClientProperties.Registration registration = createRegistration("google");
registration.setClientName("clientName");
properties.getRegistration().put("registration", registration);
Map<String, ClientRegistration> registrations = new OAuth2ClientPropertiesMapper(properties)
.asClientRegistrations();
ClientRegistration adapted = registrations.get("registration");
ProviderDetails adaptedProvider = adapted.getProviderDetails();
assertThat(adaptedProvider.getAuthorizationUri()).isEqualTo("https://accounts.google.com/o/oauth2/v2/auth");
assertThat(adaptedProvider.getTokenUri()).isEqualTo("https://www.googleapis.com/oauth2/v4/token");
UserInfoEndpoint userInfoEndpoint = adaptedProvider.getUserInfoEndpoint();
assertThat(userInfoEndpoint.getUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/userinfo");
assertThat(userInfoEndpoint.getUserNameAttributeName()).isEqualTo(IdTokenClaimNames.SUB);
assertThat(userInfoEndpoint.getAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.AuthenticationMethod.HEADER);
assertThat(adaptedProvider.getJwkSetUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/certs");
assertThat(adapted.getRegistrationId()).isEqualTo("registration");
assertThat(adapted.getClientId()).isEqualTo("clientId");
assertThat(adapted.getClientSecret()).isEqualTo("clientSecret");
assertThat(adapted.getClientAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_POST);
assertThat(adapted.getAuthorizationGrantType())
.isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(adapted.getRedirectUri()).isEqualTo("https://example.com/redirect");
assertThat(adapted.getScopes()).containsExactly("user");
assertThat(adapted.getClientName()).isEqualTo("clientName");
}
@Test
void getClientRegistrationsWhenUnknownProviderShouldThrowException() {
OAuth2ClientProperties properties = new OAuth2ClientProperties();
OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration();
registration.setProvider("missing");
properties.getRegistration().put("registration", registration);
assertThatIllegalStateException()
.isThrownBy(() -> new OAuth2ClientPropertiesMapper(properties).asClientRegistrations())
.withMessageContaining("Unknown provider ID 'missing'");
}
@Test
void getClientRegistrationsWhenProviderNotSpecifiedShouldUseRegistrationId() {
OAuth2ClientProperties properties = new OAuth2ClientProperties();
OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration();
registration.setClientId("clientId");
registration.setClientSecret("clientSecret");
properties.getRegistration().put("google", registration);
Map<String, ClientRegistration> registrations = new OAuth2ClientPropertiesMapper(properties)
.asClientRegistrations();
ClientRegistration adapted = registrations.get("google");
ProviderDetails adaptedProvider = adapted.getProviderDetails();
assertThat(adaptedProvider.getAuthorizationUri()).isEqualTo("https://accounts.google.com/o/oauth2/v2/auth");
assertThat(adaptedProvider.getTokenUri()).isEqualTo("https://www.googleapis.com/oauth2/v4/token");
UserInfoEndpoint userInfoEndpoint = adaptedProvider.getUserInfoEndpoint();
assertThat(userInfoEndpoint.getUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/userinfo");
assertThat(userInfoEndpoint.getAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.AuthenticationMethod.HEADER);
assertThat(adaptedProvider.getJwkSetUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/certs");
assertThat(adapted.getRegistrationId()).isEqualTo("google");
assertThat(adapted.getClientId()).isEqualTo("clientId");
assertThat(adapted.getClientSecret()).isEqualTo("clientSecret");
assertThat(adapted.getClientAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
assertThat(adapted.getAuthorizationGrantType())
.isEqualTo(org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(adapted.getRedirectUri()).isEqualTo("{baseUrl}/{action}/oauth2/code/{registrationId}");
assertThat(adapted.getScopes()).containsExactly("openid", "profile", "email");
assertThat(adapted.getClientName()).isEqualTo("Google");
}
@Test
void getClientRegistrationsWhenProviderNotSpecifiedAndUnknownProviderShouldThrowException() {
OAuth2ClientProperties properties = new OAuth2ClientProperties();
OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration();
properties.getRegistration().put("missing", registration);
assertThatIllegalStateException()
.isThrownBy(() -> new OAuth2ClientPropertiesMapper(properties).asClientRegistrations())
.withMessageContaining("Provider ID must be specified for client registration 'missing'");
}
@Test
void oidcProviderConfigurationWhenProviderNotSpecifiedOnRegistration() throws Exception {
Registration login = new OAuth2ClientProperties.Registration();
login.setClientId("clientId");
login.setClientSecret("clientSecret");
testIssuerConfiguration(login, "okta", 0, 1);
}
@Test
void oidcProviderConfigurationWhenProviderSpecifiedOnRegistration() throws Exception {
OAuth2ClientProperties.Registration login = new Registration();
login.setProvider("okta-oidc");
login.setClientId("clientId");
login.setClientSecret("clientSecret");
testIssuerConfiguration(login, "okta-oidc", 0, 1);
}
@Test
void issuerUriConfigurationTriesOidcRfc8414UriSecond() throws Exception {
OAuth2ClientProperties.Registration login = new Registration();
login.setClientId("clientId");
login.setClientSecret("clientSecret");
testIssuerConfiguration(login, "okta", 1, 2);
}
@Test
void issuerUriConfigurationTriesOAuthMetadataUriThird() throws Exception {
OAuth2ClientProperties.Registration login = new Registration();
login.setClientId("clientId");
login.setClientSecret("clientSecret");
testIssuerConfiguration(login, "okta", 2, 3);
}
@Test
void oidcProviderConfigurationWithCustomConfigurationOverridesProviderDefaults() throws Exception {
this.server = new MockWebServer();
this.server.start();
String issuer = this.server.url("").toString();
setupMockResponse(issuer);
OAuth2ClientProperties.Registration registration = createRegistration("okta-oidc");
Provider provider = createProvider();
provider.setIssuerUri(issuer);
OAuth2ClientProperties properties = new OAuth2ClientProperties();
properties.getProvider().put("okta-oidc", provider);
properties.getRegistration().put("okta", registration);
Map<String, ClientRegistration> registrations = new OAuth2ClientPropertiesMapper(properties)
.asClientRegistrations();
ClientRegistration adapted = registrations.get("okta");
ProviderDetails providerDetails = adapted.getProviderDetails();
assertThat(adapted.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_POST);
assertThat(adapted.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(adapted.getRegistrationId()).isEqualTo("okta");
assertThat(adapted.getClientName()).isEqualTo(issuer);
assertThat(adapted.getScopes()).containsOnly("user");
assertThat(adapted.getRedirectUri()).isEqualTo("https://example.com/redirect");
assertThat(providerDetails.getAuthorizationUri()).isEqualTo("https://example.com/auth");
assertThat(providerDetails.getTokenUri()).isEqualTo("https://example.com/token");
assertThat(providerDetails.getJwkSetUri()).isEqualTo("https://example.com/jwk");
UserInfoEndpoint userInfoEndpoint = providerDetails.getUserInfoEndpoint();
assertThat(userInfoEndpoint.getUri()).isEqualTo("https://example.com/info");
assertThat(userInfoEndpoint.getUserNameAttributeName()).isEqualTo("sub");
}
private Provider createProvider() {
Provider provider = new Provider();
provider.setAuthorizationUri("https://example.com/auth");
provider.setTokenUri("https://example.com/token");
provider.setUserInfoUri("https://example.com/info");
provider.setUserNameAttribute("sub");
provider.setJwkSetUri("https://example.com/jwk");
return provider;
}
private OAuth2ClientProperties.Registration createRegistration(String provider) {
OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration();
registration.setProvider(provider);
registration.setClientId("clientId");
registration.setClientSecret("clientSecret");
registration.setClientAuthenticationMethod("client_secret_post");
registration.setRedirectUri("https://example.com/redirect");
registration.setScope(Collections.singleton("user"));
registration.setAuthorizationGrantType("authorization_code");
return registration;
}
private void testIssuerConfiguration(OAuth2ClientProperties.Registration registration, String providerId,
int errorResponseCount, int numberOfRequests) throws Exception {
this.server = new MockWebServer();
this.server.start();
String issuer = this.server.url("").toString();
setupMockResponsesWithErrors(issuer, errorResponseCount);
OAuth2ClientProperties properties = new OAuth2ClientProperties();
Provider provider = new Provider();
provider.setIssuerUri(issuer);
properties.getProvider().put(providerId, provider);
properties.getRegistration().put("okta", registration);
Map<String, ClientRegistration> registrations = new OAuth2ClientPropertiesMapper(properties)
.asClientRegistrations();
ClientRegistration adapted = registrations.get("okta");
ProviderDetails providerDetails = adapted.getProviderDetails();
assertThat(adapted.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
assertThat(adapted.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
assertThat(adapted.getRegistrationId()).isEqualTo("okta");
assertThat(adapted.getClientName()).isEqualTo(issuer);
assertThat(adapted.getScopes()).isNull();
assertThat(providerDetails.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth");
assertThat(providerDetails.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token");
assertThat(providerDetails.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs");
UserInfoEndpoint userInfoEndpoint = providerDetails.getUserInfoEndpoint();
assertThat(userInfoEndpoint.getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo");
assertThat(userInfoEndpoint.getAuthenticationMethod())
.isEqualTo(org.springframework.security.oauth2.core.AuthenticationMethod.HEADER);
assertThat(this.server.getRequestCount()).isEqualTo(numberOfRequests);
}
private void setupMockResponse(String issuer) throws JsonProcessingException {
MockResponse mockResponse = new MockResponse().setResponseCode(HttpStatus.OK.value())
.setBody(new ObjectMapper().writeValueAsString(getResponse(issuer)))
.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
this.server.enqueue(mockResponse);
}
private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException {
for (int i = 0; i < errorResponseCount; i++) {
MockResponse emptyResponse = new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value());
this.server.enqueue(emptyResponse);
}
setupMockResponse(issuer);
}
private Map<String, Object> getResponse(String issuer) {
Map<String, Object> response = new HashMap<>();
response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth");
response.put("claims_supported", Collections.emptyList());
response.put("code_challenge_methods_supported", Collections.emptyList());
response.put("id_token_signing_alg_values_supported", Collections.emptyList());
response.put("issuer", issuer);
response.put("jwks_uri", "https://example.com/oauth2/v3/certs");
response.put("response_types_supported", Collections.emptyList());
response.put("revocation_endpoint", "https://example.com/o/oauth2/revoke");
response.put("scopes_supported", Collections.singletonList("openid"));
response.put("subject_types_supported", Collections.singletonList("public"));
response.put("grant_types_supported", Collections.singletonList("authorization_code"));
response.put("token_endpoint", "https://example.com/oauth2/v4/token");
response.put("token_endpoint_auth_methods_supported", Collections.singletonList("client_secret_basic"));
response.put("userinfo_endpoint", "https://example.com/oauth2/v3/userinfo");
return response;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link OAuth2ClientProperties}.
*
* @author Madhura Bhave
* @author Artsiom Yudovin
*/
class OAuth2ClientPropertiesTests {
private final OAuth2ClientProperties properties = new OAuth2ClientProperties();
@Test
void clientIdAbsentThrowsException() {
OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration();
registration.setClientSecret("secret");
registration.setProvider("google");
this.properties.getRegistration().put("foo", registration);
assertThatIllegalStateException().isThrownBy(this.properties::validate)
.withMessageContaining("Client id of registration 'foo' must not be empty.");
}
@Test
void clientSecretAbsentShouldNotThrowException() {
OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration();
registration.setClientId("foo");
registration.setProvider("google");
this.properties.getRegistration().put("foo", registration);
this.properties.validate();
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure.reactive;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.security.autoconfigure.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ReactiveOAuth2ClientAutoConfiguration}.
*
* @author Madhura Bhave
*/
class ReactiveOAuth2ClientAutoConfigurationTests {
private static final String REGISTRATION_PREFIX = "spring.security.oauth2.client.registration";
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class));
@Test
void autoConfigurationShouldBackOffForServletEnvironments() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOAuth2ClientAutoConfiguration.class));
}
@Test
void beansShouldNotBeCreatedWhenPropertiesAbsent() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveClientRegistrationRepository.class)
.doesNotHaveBean(ReactiveOAuth2AuthorizedClientService.class));
}
@Test
void beansAreCreatedWhenPropertiesPresent() {
this.contextRunner
.withPropertyValues(REGISTRATION_PREFIX + ".foo.client-id=abcd",
REGISTRATION_PREFIX + ".foo.client-secret=secret", REGISTRATION_PREFIX + ".foo.provider=github")
.run((context) -> {
assertThat(context).hasSingleBean(ReactiveClientRegistrationRepository.class);
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientService.class);
ReactiveClientRegistrationRepository repository = context
.getBean(ReactiveClientRegistrationRepository.class);
ClientRegistration registration = repository.findByRegistrationId("foo").block(Duration.ofSeconds(30));
assertThat(registration).isNotNull();
assertThat(registration.getClientSecret()).isEqualTo("secret");
});
}
@Test
void clientServiceBeanIsConditionalOnMissingBean() {
this.contextRunner
.withBean("testAuthorizedClientService", ReactiveOAuth2AuthorizedClientService.class,
() -> mock(ReactiveOAuth2AuthorizedClientService.class))
.run((context) -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientService.class);
assertThat(context).hasBean("testAuthorizedClientService");
});
}
@Test
void clientServiceBeanIsCreatedWithUserDefinedClientRegistrationRepository() {
this.contextRunner
.withBean(InMemoryReactiveClientRegistrationRepository.class,
() -> new InMemoryReactiveClientRegistrationRepository(getClientRegistration("test", "test")))
.run((context) -> assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientService.class));
}
@Test
void autoConfigurationConditionalOnClassFlux() {
assertWhenClassNotPresent(Flux.class);
}
@Test
void autoConfigurationConditionalOnClassClientRegistration() {
assertWhenClassNotPresent(ClientRegistration.class);
}
private void assertWhenClassNotPresent(Class<?> classToFilter) {
FilteredClassLoader classLoader = new FilteredClassLoader(classToFilter);
this.contextRunner.withClassLoader(classLoader)
.withPropertyValues(REGISTRATION_PREFIX + ".foo.client-id=abcd",
REGISTRATION_PREFIX + ".foo.client-secret=secret", REGISTRATION_PREFIX + ".foo.provider=github")
.run((context) -> assertThat(context).doesNotHaveBean(ReactiveOAuth2ClientAutoConfiguration.class));
}
private ClientRegistration getClientRegistration(String id, String userInfoUri) {
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(id);
builder.clientName("foo")
.clientId("foo")
.clientAuthenticationMethod(
org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("read")
.clientSecret("secret")
.redirectUri("https://redirect-uri.com")
.authorizationUri("https://authorization-uri.com")
.tokenUri("https://token-uri.com")
.userInfoUri(userInfoUri)
.userNameAttributeName("login");
return builder.build();
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure.reactive;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.security.autoconfigure.reactive.ReactiveSecurityAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.server.AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.OAuth2AuthorizationCodeGrantWebFilter;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.server.authentication.OAuth2LoginAuthenticationWebFilter;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.WebFilter;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ReactiveOAuth2ClientWebSecurityAutoConfiguration}.
*
* @author Madhura Bhave
* @author Andy Wilkinson
*/
class ReactiveOAuth2ClientWebSecurityAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientWebSecurityAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class));
@Test
void autoConfigurationShouldBackOffForServletEnvironments() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientWebSecurityAutoConfiguration.class))
.run((context) -> assertThat(context)
.doesNotHaveBean(ReactiveOAuth2ClientWebSecurityAutoConfiguration.class));
}
@Test
void autoConfigurationIsConditionalOnAuthorizedClientService() {
this.contextRunner.run((context) -> assertThat(context)
.doesNotHaveBean(ReactiveOAuth2ClientWebSecurityAutoConfiguration.class));
}
@Test
void configurationRegistersAuthorizedClientRepositoryBean() {
this.contextRunner.withUserConfiguration(ReactiveOAuth2AuthorizedClientServiceConfiguration.class)
.run((context) -> assertThat(context)
.hasSingleBean(AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository.class));
}
@Test
void authorizedClientRepositoryBeanIsConditionalOnMissingBean() {
this.contextRunner.withUserConfiguration(ReactiveOAuth2AuthorizedClientRepositoryConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(ServerOAuth2AuthorizedClientRepository.class);
assertThat(context).hasBean("testAuthorizedClientRepository");
});
}
@Test
void configurationRegistersSecurityWebFilterChainBean() { // gh-17949
this.contextRunner
.withUserConfiguration(ReactiveOAuth2AuthorizedClientServiceConfiguration.class,
ServerHttpSecurityConfiguration.class)
.run((context) -> {
assertThat(hasFilter(context, OAuth2LoginAuthenticationWebFilter.class)).isTrue();
assertThat(hasFilter(context, OAuth2AuthorizationCodeGrantWebFilter.class)).isTrue();
});
}
@Test
void securityWebFilterChainBeanConditionalOnWebApplication() {
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ReactiveOAuth2ClientWebSecurityAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class))
.withUserConfiguration(ReactiveOAuth2AuthorizedClientRepositoryConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(SecurityWebFilterChain.class));
}
@SuppressWarnings("unchecked")
private boolean hasFilter(AssertableReactiveWebApplicationContext context, Class<? extends WebFilter> filter) {
SecurityWebFilterChain filterChain = (SecurityWebFilterChain) context
.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN);
List<WebFilter> filters = (List<WebFilter>) ReflectionTestUtils.getField(filterChain, "filters");
return filters.stream().anyMatch(filter::isInstance);
}
@Configuration(proxyBeanMethods = false)
@Import(ReactiveClientRepositoryConfiguration.class)
static class ReactiveOAuth2AuthorizedClientServiceConfiguration {
@Bean
InMemoryReactiveOAuth2AuthorizedClientService testAuthorizedClientService(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository);
}
}
@Configuration(proxyBeanMethods = false)
@Import(ReactiveOAuth2AuthorizedClientServiceConfiguration.class)
static class ReactiveOAuth2AuthorizedClientRepositoryConfiguration {
@Bean
ServerOAuth2AuthorizedClientRepository testAuthorizedClientRepository(
ReactiveOAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService);
}
}
@Configuration(proxyBeanMethods = false)
static class ReactiveClientRepositoryConfiguration {
@Bean
ReactiveClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
registrations.add(getClientRegistration("first", "https://user-info-uri.com"));
registrations.add(getClientRegistration("second", "https://other-user-info"));
return new InMemoryReactiveClientRegistrationRepository(registrations);
}
private ClientRegistration getClientRegistration(String id, String userInfoUri) {
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(id);
builder.clientName("foo")
.clientId("foo")
.clientAuthenticationMethod(
org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("read")
.clientSecret("secret")
.redirectUri("https://redirect-uri.com")
.authorizationUri("https://authorization-uri.com")
.tokenUri("https://token-uri.com")
.userInfoUri(userInfoUri)
.userNameAttributeName("login");
return builder.build();
}
}
@Configuration(proxyBeanMethods = false)
static class ServerHttpSecurityConfiguration {
@Bean
ServerHttpSecurity http() {
TestServerHttpSecurity httpSecurity = new TestServerHttpSecurity();
return httpSecurity;
}
static class TestServerHttpSecurity extends ServerHttpSecurity implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
super.setApplicationContext(applicationContext);
}
}
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright 2012-2025 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.security.oauth2.client.autoconfigure.servlet;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.Filter;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.CompositeFilter;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OAuth2ClientWebSecurityAutoConfiguration}.
*
* @author Madhura Bhave
* @author Andy Wilkinson
*/
class OAuth2ClientWebSecurityAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OAuth2ClientWebSecurityAutoConfiguration.class));
@Test
void autoConfigurationIsConditionalOnAuthorizedClientService() {
this.contextRunner
.run((context) -> assertThat(context).doesNotHaveBean(OAuth2ClientWebSecurityAutoConfiguration.class));
}
@Test
void configurationRegistersAuthorizedClientRepositoryBean() {
this.contextRunner.withUserConfiguration(OAuth2AuthorizedClientServiceConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(OAuth2AuthorizedClientRepository.class));
}
@Test
void authorizedClientRepositoryBeanIsConditionalOnMissingBean() {
this.contextRunner.withUserConfiguration(OAuth2AuthorizedClientRepositoryConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(OAuth2AuthorizedClientRepository.class);
assertThat(context).hasBean("testAuthorizedClientRepository");
});
}
@Test
void securityConfigurerConfiguresOAuth2Login() {
this.contextRunner.withUserConfiguration(OAuth2AuthorizedClientServiceConfiguration.class).run((context) -> {
ClientRegistrationRepository expected = context.getBean(ClientRegistrationRepository.class);
ClientRegistrationRepository actual = (ClientRegistrationRepository) ReflectionTestUtils.getField(
getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class).get(0),
"clientRegistrationRepository");
assertThat(isEqual(expected.findByRegistrationId("first"), actual.findByRegistrationId("first"))).isTrue();
assertThat(isEqual(expected.findByRegistrationId("second"), actual.findByRegistrationId("second")))
.isTrue();
});
}
@Test
void securityConfigurerConfiguresAuthorizationCode() {
this.contextRunner.withUserConfiguration(OAuth2AuthorizedClientServiceConfiguration.class).run((context) -> {
ClientRegistrationRepository expected = context.getBean(ClientRegistrationRepository.class);
ClientRegistrationRepository actual = (ClientRegistrationRepository) ReflectionTestUtils.getField(
getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class).get(0),
"clientRegistrationRepository");
assertThat(isEqual(expected.findByRegistrationId("first"), actual.findByRegistrationId("first"))).isTrue();
assertThat(isEqual(expected.findByRegistrationId("second"), actual.findByRegistrationId("second")))
.isTrue();
});
}
@Test
void securityConfigurerBacksOffWhenClientRegistrationBeanAbsent() {
this.contextRunner.withUserConfiguration(TestConfig.class).run((context) -> {
assertThat(getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty();
assertThat(getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty();
});
}
@Test
void securityFilterChainConfigBacksOffWhenOtherSecurityFilterChainBeanPresent() {
this.contextRunner.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class))
.withUserConfiguration(TestSecurityFilterChainConfiguration.class)
.run((context) -> {
assertThat(getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty();
assertThat(getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty();
assertThat(context).getBean(OAuth2AuthorizedClientService.class).isNotNull();
});
}
@Test
void securityFilterChainConfigConditionalOnSecurityFilterChainClass() {
this.contextRunner.withUserConfiguration(ClientRegistrationRepositoryConfiguration.class)
.withClassLoader(new FilteredClassLoader(SecurityFilterChain.class))
.run((context) -> {
assertThat(getSecurityFilters(context, OAuth2LoginAuthenticationFilter.class)).isEmpty();
assertThat(getSecurityFilters(context, OAuth2AuthorizationCodeGrantFilter.class)).isEmpty();
});
}
private List<Filter> getSecurityFilters(AssertableWebApplicationContext context, Class<? extends Filter> filter) {
return getSecurityFilterChain(context).getFilters().stream().filter(filter::isInstance).toList();
}
private SecurityFilterChain getSecurityFilterChain(AssertableWebApplicationContext context) {
Filter springSecurityFilterChain = context.getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN, Filter.class);
FilterChainProxy filterChainProxy = getFilterChainProxy(springSecurityFilterChain);
SecurityFilterChain securityFilterChain = filterChainProxy.getFilterChains().get(0);
return securityFilterChain;
}
private FilterChainProxy getFilterChainProxy(Filter filter) {
if (filter instanceof FilterChainProxy filterChainProxy) {
return filterChainProxy;
}
if (filter instanceof CompositeFilter) {
List<?> filters = (List<?>) ReflectionTestUtils.getField(filter, "filters");
return (FilterChainProxy) filters.stream()
.filter(FilterChainProxy.class::isInstance)
.findFirst()
.orElseThrow();
}
throw new IllegalStateException("No FilterChainProxy found");
}
private boolean isEqual(ClientRegistration reg1, ClientRegistration reg2) {
boolean result = ObjectUtils.nullSafeEquals(reg1.getClientId(), reg2.getClientId());
result = result && ObjectUtils.nullSafeEquals(reg1.getClientName(), reg2.getClientName());
result = result && ObjectUtils.nullSafeEquals(reg1.getClientSecret(), reg2.getClientSecret());
result = result && ObjectUtils.nullSafeEquals(reg1.getScopes(), reg2.getScopes());
result = result && ObjectUtils.nullSafeEquals(reg1.getRedirectUri(), reg2.getRedirectUri());
result = result && ObjectUtils.nullSafeEquals(reg1.getRegistrationId(), reg2.getRegistrationId());
result = result
&& ObjectUtils.nullSafeEquals(reg1.getAuthorizationGrantType(), reg2.getAuthorizationGrantType());
result = result && ObjectUtils.nullSafeEquals(reg1.getProviderDetails().getAuthorizationUri(),
reg2.getProviderDetails().getAuthorizationUri());
result = result && ObjectUtils.nullSafeEquals(reg1.getProviderDetails().getUserInfoEndpoint(),
reg2.getProviderDetails().getUserInfoEndpoint());
result = result && ObjectUtils.nullSafeEquals(reg1.getProviderDetails().getTokenUri(),
reg2.getProviderDetails().getTokenUri());
return result;
}
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
static class TestConfig {
@Bean
TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(0);
}
}
@Configuration(proxyBeanMethods = false)
@Import(ClientRegistrationRepositoryConfiguration.class)
static class OAuth2AuthorizedClientServiceConfiguration {
@Bean
InMemoryOAuth2AuthorizedClientService authorizedClientService(
ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
}
@Configuration(proxyBeanMethods = false)
@Import(OAuth2AuthorizedClientServiceConfiguration.class)
static class OAuth2AuthorizedClientRepositoryConfiguration {
@Bean
OAuth2AuthorizedClientRepository testAuthorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
}
@Configuration(proxyBeanMethods = false)
@Import(TestConfig.class)
static class ClientRegistrationRepositoryConfiguration {
@Bean
ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
registrations.add(getClientRegistration("first", "https://user-info-uri.com"));
registrations.add(getClientRegistration("second", "https://other-user-info"));
return new InMemoryClientRegistrationRepository(registrations);
}
private ClientRegistration getClientRegistration(String id, String userInfoUri) {
ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(id);
builder.clientName("foo")
.clientId("foo")
.clientAuthenticationMethod(
org.springframework.security.oauth2.core.ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.scope("read")
.clientSecret("secret")
.redirectUri("https://redirect-uri.com")
.authorizationUri("https://authorization-uri.com")
.tokenUri("https://token-uri.com")
.userInfoUri(userInfoUri)
.userNameAttributeName("login");
return builder.build();
}
}
@Configuration(proxyBeanMethods = false)
@Import(OAuth2AuthorizedClientServiceConfiguration.class)
static class TestSecurityFilterChainConfiguration {
@Bean
SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception {
return http.securityMatcher("/**")
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.build();
}
}
}