|
|
|
|
@ -1,5 +1,5 @@
@@ -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;
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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(); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@Test |
|
|
|
|
void defaultUserNotCreatedIfResourceServerWithOpaqueIsUsed() { |
|
|
|
|
this.contextRunner.withUserConfiguration(TestConfigWithIntrospectionClient.class).run((context) -> { |
|
|
|
|
assertThat(context).hasSingleBean(OpaqueTokenIntrospector.class); |
|
|
|
|
assertThat(context).doesNotHaveBean(UserDetailsService.class); |
|
|
|
|
}); |
|
|
|
|
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 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 {
@@ -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))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@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))); |
|
|
|
|
@ParameterizedTest |
|
|
|
|
@EnumSource |
|
|
|
|
void whenClassOfAlternativeIsPresentUserDetailsServiceBacksOff(AlternativeFormOfAuthentication alternative) { |
|
|
|
|
this.contextRunner.with(alternative.present()) |
|
|
|
|
.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 {
@@ -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 {
@@ -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))); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|