Merge branch '3.4.x'

Closes gh-44943
This commit is contained in:
Andy Wilkinson 2025-03-28 16:39:17 +00:00
commit 67cb738337
1 changed files with 122 additions and 89 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* 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.
@ -18,19 +18,30 @@ package org.springframework.boot.autoconfigure.security.servlet;
import java.util.Collections;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.MissingAlternativeOrUserPropertiesConfigured;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.AbstractApplicationContextRunner;
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.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -70,7 +81,7 @@ class UserDetailsServiceAutoConfigurationTests {
@Test
void shouldSupplyUserDetailsServiceInServletApp() {
this.contextRunner.with(AuthenticationExclude.servletApp())
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.run((context) -> assertThat(context).hasSingleBean(UserDetailsService.class));
}
@ -78,7 +89,7 @@ class UserDetailsServiceAutoConfigurationTests {
void shouldNotSupplyUserDetailsServiceInReactiveApp() {
new ReactiveWebApplicationContextRunner().withUserConfiguration(TestSecurityConfiguration.class)
.withConfiguration(AutoConfigurations.of(UserDetailsServiceAutoConfiguration.class))
.with(AuthenticationExclude.reactiveApp())
.with(AlternativeFormOfAuthentication.nonPresent())
.run((context) -> assertThat(context).doesNotHaveBean(UserDetailsService.class));
}
@ -86,13 +97,14 @@ class UserDetailsServiceAutoConfigurationTests {
void shouldNotSupplyUserDetailsServiceInNonWebApp() {
new ApplicationContextRunner().withUserConfiguration(TestSecurityConfiguration.class)
.withConfiguration(AutoConfigurations.of(UserDetailsServiceAutoConfiguration.class))
.with(AuthenticationExclude.noWebApp())
.with(AlternativeFormOfAuthentication.nonPresent())
.run((context) -> assertThat(context).doesNotHaveBean(UserDetailsService.class));
}
@Test
void testDefaultUsernamePassword(CapturedOutput output) {
this.contextRunner.with(AuthenticationExclude.servletApp()).run((context) -> {
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent()).run((context) -> {
assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue();
UserDetailsService manager = context.getBean(UserDetailsService.class);
assertThat(output).contains("Using generated security password:");
assertThat(manager.loadUserByUsername("user")).isNotNull();
@ -101,60 +113,68 @@ class UserDetailsServiceAutoConfigurationTests {
@Test
void defaultUserNotCreatedIfAuthenticationManagerBeanPresent(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TestAuthenticationManagerConfiguration.class).run((context) -> {
AuthenticationManager manager = context.getBean(AuthenticationManager.class);
assertThat(manager)
.isEqualTo(context.getBean(TestAuthenticationManagerConfiguration.class).authenticationManager);
assertThat(output).doesNotContain("Using generated security password: ");
TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar");
assertThat(manager.authenticate(token)).isNotNull();
});
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.withUserConfiguration(TestAuthenticationManagerConfiguration.class)
.run((context) -> {
assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue();
AuthenticationManager manager = context.getBean(AuthenticationManager.class);
assertThat(manager)
.isEqualTo(context.getBean(TestAuthenticationManagerConfiguration.class).authenticationManager);
assertThat(output).doesNotContain("Using generated security password: ");
TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar");
assertThat(manager.authenticate(token)).isNotNull();
});
}
@Test
void defaultUserNotCreatedIfAuthenticationManagerResolverBeanPresent(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TestAuthenticationManagerResolverConfiguration.class)
.run((context) -> assertThat(output).doesNotContain("Using generated security password: "));
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.withUserConfiguration(TestAuthenticationManagerResolverConfiguration.class)
.run((context) -> {
assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue();
assertThat(output).doesNotContain("Using generated security password: ");
});
}
@Test
void defaultUserNotCreatedIfUserDetailsServiceBeanPresent(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TestUserDetailsServiceConfiguration.class).run((context) -> {
UserDetailsService userDetailsService = context.getBean(UserDetailsService.class);
assertThat(output).doesNotContain("Using generated security password: ");
assertThat(userDetailsService.loadUserByUsername("foo")).isNotNull();
});
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.withUserConfiguration(TestUserDetailsServiceConfiguration.class)
.run((context) -> {
assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue();
UserDetailsService userDetailsService = context.getBean(UserDetailsService.class);
assertThat(output).doesNotContain("Using generated security password: ");
assertThat(userDetailsService.loadUserByUsername("foo")).isNotNull();
});
}
@Test
void defaultUserNotCreatedIfAuthenticationProviderBeanPresent(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TestAuthenticationProviderConfiguration.class).run((context) -> {
AuthenticationProvider provider = context.getBean(AuthenticationProvider.class);
assertThat(output).doesNotContain("Using generated security password: ");
TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar");
assertThat(provider.authenticate(token)).isNotNull();
});
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.withUserConfiguration(TestAuthenticationProviderConfiguration.class)
.run((context) -> {
assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue();
AuthenticationProvider provider = context.getBean(AuthenticationProvider.class);
assertThat(output).doesNotContain("Using generated security password: ");
TestingAuthenticationToken token = new TestingAuthenticationToken("foo", "bar");
assertThat(provider.authenticate(token)).isNotNull();
});
}
@Test
void defaultUserNotCreatedIfResourceServerWithOpaqueIsUsed() {
this.contextRunner.withUserConfiguration(TestConfigWithIntrospectionClient.class).run((context) -> {
assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class);
assertThat(context).doesNotHaveBean(UserDetailsService.class);
});
}
@Test
void defaultUserNotCreatedIfResourceServerWithJWTIsUsed() {
this.contextRunner.withUserConfiguration(TestConfigWithJwtDecoder.class).run((context) -> {
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(context).doesNotHaveBean(UserDetailsService.class);
});
void defaultUserNotCreatedIfJwtDecoderBeanPresent() {
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.withUserConfiguration(TestConfigWithJwtDecoder.class)
.run((context) -> {
assertThat(outcomeOfMissingAlternativeCondition(context).isMatch()).isTrue();
assertThat(context).hasSingleBean(JwtDecoder.class);
assertThat(context).doesNotHaveBean(UserDetailsService.class);
});
}
@Test
void userDetailsServiceWhenPasswordEncoderAbsentAndDefaultPassword() {
this.contextRunner.with(AuthenticationExclude.servletApp())
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.withUserConfiguration(TestSecurityConfiguration.class)
.run(((context) -> {
InMemoryUserDetailsManager userDetailsService = context.getBean(InMemoryUserDetailsManager.class);
@ -179,49 +199,33 @@ class UserDetailsServiceAutoConfigurationTests {
testPasswordEncoding(TestConfigWithPasswordEncoder.class, "secret", "secret");
}
@Test
void userDetailsServiceWhenClientRegistrationRepositoryPresent() {
this.contextRunner
.withClassLoader(
new FilteredClassLoader(OpaqueTokenIntrospector.class, RelyingPartyRegistrationRepository.class))
.run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class)));
@ParameterizedTest
@EnumSource
void whenClassOfAlternativeIsPresentUserDetailsServiceBacksOff(AlternativeFormOfAuthentication alternative) {
this.contextRunner.with(alternative.present())
.run((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class));
}
@Test
void userDetailsServiceWhenOpaqueTokenIntrospectorPresent() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class,
RelyingPartyRegistrationRepository.class))
.run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class)));
}
@Test
void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresent() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class))
.run(((context) -> assertThat(context).doesNotHaveBean(InMemoryUserDetailsManager.class)));
}
@Test
void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndUsernameConfigured() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class))
@ParameterizedTest
@EnumSource
void whenAlternativeIsPresentAndUsernameIsConfiguredThenUserDetailsServiceIsAutoConfigured(
AlternativeFormOfAuthentication alternative) {
this.contextRunner.with(alternative.present())
.withPropertyValues("spring.security.user.name=alice")
.run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class)));
}
@Test
void userDetailsServiceWhenRelyingPartyRegistrationRepositoryPresentAndPasswordConfigured() {
this.contextRunner
.withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class))
@ParameterizedTest
@EnumSource
void whenAlternativeIsPresentAndPasswordIsConfiguredThenUserDetailsServiceIsAutoConfigured(
AlternativeFormOfAuthentication alternative) {
this.contextRunner.with(alternative.present())
.withPropertyValues("spring.security.user.password=secret")
.run(((context) -> assertThat(context).hasSingleBean(InMemoryUserDetailsManager.class)));
}
private void testPasswordEncoding(Class<?> configClass, String providedPassword, String expectedPassword) {
this.contextRunner.with(AuthenticationExclude.servletApp())
.withClassLoader(new FilteredClassLoader(ClientRegistrationRepository.class, OpaqueTokenIntrospector.class,
RelyingPartyRegistrationRepository.class))
this.contextRunner.with(AlternativeFormOfAuthentication.nonPresent())
.withUserConfiguration(configClass)
.withPropertyValues("spring.security.user.password=" + providedPassword)
.run(((context) -> {
@ -231,24 +235,16 @@ class UserDetailsServiceAutoConfigurationTests {
}));
}
private static final class AuthenticationExclude {
private static final FilteredClassLoader filteredClassLoader = new FilteredClassLoader(
ClientRegistrationRepository.class, OpaqueTokenIntrospector.class,
RelyingPartyRegistrationRepository.class);
static Function<WebApplicationContextRunner, WebApplicationContextRunner> servletApp() {
return (contextRunner) -> contextRunner.withClassLoader(filteredClassLoader);
private ConditionOutcome outcomeOfMissingAlternativeCondition(ConfigurableApplicationContext context) {
ConditionAndOutcomes conditionAndOutcomes = ConditionEvaluationReport.get(context.getBeanFactory())
.getConditionAndOutcomesBySource()
.get(UserDetailsServiceAutoConfiguration.class.getName());
for (ConditionAndOutcome conditionAndOutcome : conditionAndOutcomes) {
if (conditionAndOutcome.getCondition() instanceof MissingAlternativeOrUserPropertiesConfigured) {
return conditionAndOutcome.getOutcome();
}
}
static Function<ReactiveWebApplicationContextRunner, ReactiveWebApplicationContextRunner> reactiveApp() {
return (contextRunner) -> contextRunner.withClassLoader(filteredClassLoader);
}
static Function<ApplicationContextRunner, ApplicationContextRunner> noWebApp() {
return (contextRunner) -> contextRunner.withClassLoader(filteredClassLoader);
}
return null;
}
@Configuration(proxyBeanMethods = false)
@ -346,4 +342,41 @@ class UserDetailsServiceAutoConfigurationTests {
}
private enum AlternativeFormOfAuthentication {
CLIENT_REGISTRATION_REPOSITORY(ClientRegistrationRepository.class),
OPAQUE_TOKEN_INTROSPECTOR(OpaqueTokenIntrospector.class),
RELYING_PARTY_REGISTRATION_REPOSITORY(RelyingPartyRegistrationRepository.class);
private final Class<?> type;
AlternativeFormOfAuthentication(Class<?> type) {
this.type = type;
}
private Class<?> getType() {
return this.type;
}
@SuppressWarnings("unchecked")
private <T extends AbstractApplicationContextRunner<?, ?, ?>> Function<T, T> present() {
return (contextRunner) -> (T) contextRunner
.withClassLoader(new FilteredClassLoader(Stream.of(AlternativeFormOfAuthentication.values())
.filter(Predicate.not(this::equals))
.map(AlternativeFormOfAuthentication::getType)
.toArray(Class[]::new)));
}
@SuppressWarnings("unchecked")
private static <T extends AbstractApplicationContextRunner<?, ?, ?>> Function<T, T> nonPresent() {
return (contextRunner) -> (T) contextRunner
.withClassLoader(new FilteredClassLoader(Stream.of(AlternativeFormOfAuthentication.values())
.map(AlternativeFormOfAuthentication::getType)
.toArray(Class[]::new)));
}
}
}