According to Spring Boot Docs, a nested #TestConfiguration should be detected by tests automatically.
But in my test codes it is problematic when I ran the whole test class, it was not detected even I added it explicitly by #Import. The test code structure is like the following:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
#RunWith(SpringRunner.class)
//#Import(IntegrationTests.TestSecurityConfig.class)
public class IntegrationTests {
// test methods
// test configuration
#TestConfiguration
static class TestSecurityConfig {}
}
When I ran single test cases(test methods) individually, all tests are passed as expected, but when I ran the test class directly, some tests are failed, the #TestConfiguration was not applied to test.
The complete codes of this IntegrationTests is here.
UPDATE:
A workaround added in my codes to make the tests passed.
#TestComponent
#Slf4j
static class TestUserDetailsService implements UserDetailsService {
private final PasswordEncoder passwordEncoder;
TestUserDetailsService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = User.withUsername("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("password"))
.roles("ADMIN")
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
log.debug("dummy user:" + user);
log.debug("dummy admin:" + admin);
if ("user".equals(username)) {
return user;
} else {
return admin;
}
}
}
#TestConfiguration
#Slf4j
#Import(TestUserDetailsService.class)
#Order(-1)
static class TestSecurityConfig extends WebSecurityConfigurerAdapter {
#Inject
PasswordEncoder passwordEncoder;
#Inject
UserDetailsService userDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/posts/**").permitAll()
.antMatchers(HttpMethod.DELETE, "/posts/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
}
There are still some things confused me.
In the test class, why the #TestConfiguration can not detect #TestComponent located in the same test, I have to add #Import to fix it.
As described in the security section of Spring Boot Docs, I was thinking defining a UserDetailsService bean is enough, it will serve the users in security, but it did not work in tests. I have to configure a WebSecurityConfigurerAdapter and expose AuthenticationManager for test, why? And more confused me is as described before , running the tests one by one is ok if there is no WebSecurityConfigurerAdapter defined for test.
The #TestConfiguration annotated WebSecurityConfigurerAdapter does not get a higer order, I have to add #Order on it. I was thinking a #TestConfiguration bean should get Primary automatically and replace the bean in my application config, right?
Simply adding #Order(Ordered.HIGHEST_PRECEDENCE) solved it in my case:
#Order(Ordered.HIGHEST_PRECEDENCE)
#TestConfiguration
I'm not quite sure why that isn't the default. My #ConditionalOnBean was evaluated before that #TestConfiguration was actually initializing those beans.
Related
I have a spring boot-2.5.1 application with spring security 5.
WebSecurityConfig looks as below:
#Slf4j
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomProperties customProperties;
#Override
protected void configure(HttpSecurity http) throws Exception {
if (customProperties.isEnabled()) {
log.info("authentication enabled.");
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri(customProperties.getAuthorizationResponseUri())
.and()
.userInfoEndpoint(userInfo -> userInfo.userService(new CustomOAuth2UserService()::loadUser));
} else {
log.info("authentication disabled.");
http.authorizeRequests()
.anyRequest()
.permitAll();
}
}
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring()
.antMatchers("/actuator/info", "/actuator/health/*");
}
}
}
Controller looks like as below:
#Controller
#RequiredArgsConstructor
public class UserController {
private final UserService userService;
#PostMapping("/users")
public #ResponseBody Mono<Map<String, String>> saveUsers(#RequestBody List<UserDto> userDtos) {
return userService.saveUser(userDtos);
}
}
JUnit corresponding to the above controller:
#AutoConfigureMockMvc
#WebMvcTest(UserController.class)
class UserControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private UserService userService;
#SneakyThrows
#Test
#WithMockTestUser
void shouldSaveUsers() {
var userDtos = "...";
mockMvc.perform(post("/users")
.contentType(APPLICATION_JSON)
.content(userDtos))
.andDo(print())
.andExpect(status().isOk());
}
}
Above JUnit without CSRF is giving status as OK instead of forbidden request.
If I debug, I could see, if csrf() is not included then no token will be generated & assigned to request. However, still, the request is passing. Ideally, it should throw forbidden request access.
Even with csrf(), I can see token is generated & assigned to parameter.
In both cases, I do not see anywhere POST request is being validated whether it contains CSRF token or not with mockmvc.
Does mockmvc need any extra configuration to validate whether POST request contains CSRF or not?
Rather than auto-configuring MockMvc, you need to build MockMvc using SecurityMockMvcConfigurers.springSecurity().
This adds the Spring Bean named "springSecurityFilterChain" as a Filter and ensures that the TestSecurityContextHolder is leveraged for each request.
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
#Before
public void setup() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(springSecurity())
.build();
}
I use the following SecurityConfiguration class for securing endpoints in a Spring Boot application. This class depends on the ApiConfiguration class which provides the username and password for the in-memory authentication.
When starting a #WebMvcTest for a controller, then Spring also tries to initialize the security configuration but fails to load the application context.
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'ApiConfiguration' available: expected at least 1 bean which qualifies as autowire candidate.
I tried adding a MockBean to the test class:
#MockBean
private ApiConfiguration apiConfiguration;
This resolves the above issue, but then the username and password are null.
Is there any Spring support for providing this configuration bean for testing?
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
#Configuration
#Order(1)
public static class ApiSecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String API_USER_ROLE = "API_USER";
private final ApiConfiguration apiConfig;
public ApiSecurityConfiguration(ApiConfiguration apiConfig) {
this.apiConfig = apiConfig;
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(apiConfig.getUsername())
.password(passwordEncoder().encode(apiConfig.getPassword()))
.roles(API_USER_ROLE);
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.formLogin().disable()
.mvcMatcher("/api/**")
.authorizeRequests()
.anyRequest().hasRole(API_USER_ROLE)
.and()
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
I ended up creating a CustomApiConfiguration annotated with #TestConfiguration. This seems to do the trick. At least, Spring is able to read the properties when setting up the SecurityConfiguration class.
#TestConfiguration
public class CustomApiConfiguration {
#Bean
public ApiConfiguration apiConfiguration() {
final ApiConfiguration config = new ApiConfiguration();
config.setUsername("api-username");
config.setPassword("api-password");
return config;
}
}
I'm developing a springboot application with spring security.
I'm trying to make my custom authentication filter reading some properties from the application.properties file without success.
I've read this other question which is similar but within a different context (not related to spring security filters). The reason for the failure makes sense to me but I've tried the way suggested with the DelegatingFilterProxy but without success (to be fair, I didn't really get the meaning of the part added to the Application class). The other solution does not fit my case as I don't have any onStartup method to override.
Here is the code I'm using:
public class JWTAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
#Value("${app.jwtSecret}")
public String SECRET2;
Almost the same code, in a controller class, works fine:
#RestController
#RequestMapping("/api")
#CrossOrigin
#EnableAutoConfiguration
public class UsersController {
#Value("${app.jwtSecret}")
public String SECRET2;
But I can't make it work in the filter. I'm using springboot 2.0.3.
Any suggestion? Is the DelegatingFilterProxy the right approach in this situation? In that case, any example/article I could follow?
Thanks,
Michele.
UPDATE:
to fully answer to the first comment, the filter is called by the following class:
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
private LdapAuthenticationProvider ldapAuthenticationProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers(HttpMethod.POST, "/api/secureLogin").permitAll()
.antMatchers(HttpMethod.GET, "/api").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.addFilterBefore(new JWTAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(ldapAuthenticationProvider);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
No need to use #Value in filter class:
public class JWTAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
private String secret;
//... setter for secret
But inject the secret in the config class:
#EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Value("${app.jwtSecret}")
public String secret;
//...
#Override
protected void configure(HttpSecurity http) throws Exception {
JWTAuthorizationFilter jwtFilter = new JWTAuthorizationFilter(authenticationManager());
//set secret
//...
}
I have a spring boot (version 1.5.9.RELEASE) application which uses spring-session to store sessions on Redis. It also uses spring-security to authenticate users. When running the application, after a successful login, the security context contains the Authentication object. But when running unit tests I get this error message Authentication should not be null. Code to reproduce is the following:
#SpringBootApplication
public class DemoRedisDataSessionApplication {
#Configuration
#EnableWebSecurity
#EnableRedisHttpSession(redisNamespace = "demo-redis-spring-session")
public static class AppConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("0000").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
#RestController
public static class AppController {
#GetMapping("/secured")
public String secured() {
return "secured";
}
}
public static void main(String[] args) {
SpringApplication.run(DemoRedisDataSessionApplication.class, args);
}
}
Here is application.properties
spring.session.store-type=redis
Here is the failing test
#RunWith(SpringRunner.class)
#SpringBootTest
#AutoConfigureMockMvc
public class DemoRedisDataSessionApplicationTests {
#Autowired
private MockMvc mockMvc;
#Test
public void testUserShouldBeAuthenticated() throws Exception {
mockMvc.perform(formLogin().user("user").password("0000"))
.andExpect(status().is3xxRedirection())
.andExpect(authenticated());
}
}
Error message for the failed test:
java.lang.AssertionError: Authentication should not be null
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:35)
at org.springframework.test.util.AssertionErrors.assertTrue(AssertionErrors.java:65)
at org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers$AuthenticatedMatcher.match(SecurityMockMvcResultMatchers.java:98)
In particular it seems that the session is null in the class HttpSessionSecurityContextRepository line 110, but I don't understand why.
I expect the user to be authenticated and the SecurityContext populated after a successful login. Do you have any idea on how to solve this?
Updated:
Firstly, you need to instruct your authentication provider (in your case, it is the default DaoAuthenticationProvider) to return what kind of Authentication object. For instance, you can add httpBasic() into your configure(HttpSecurity http) method in your customized WebSecurityConfigurerAdapter. Essentially, httpBasic() will convert your username and password to a UsernamePasswordAuthenticationToken object such that your DaoAuthenticationProvider can use it to do authentication.
In addition, you need to permitAll for you login url.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests()
.antMatchers("/login/**").permitAll()
.anyRequest().fullyAuthenticated()
.and().httpBasic();
}
With regards to the unit test, the issue was due to the fact that you didn't wire-in spring security into your mockMvc object. As you are actually spring-boot, I would give you a sample solution with spring-boot-test:
#RunWith(SpringRunner.class)
#SpringBootTest
#WebAppConfiguration
public class DemoRedisDataSessionApplicationTests {
#Autowired
WebApplicationContext wac;
private MockMvc mockMvc;
#Before
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.apply(springSecurity())
.build();
}
#Test
public void testUserShouldBeAuthenticated() throws Exception {
mockMvc.perform(formLogin().user("user").password("0000"))
.andExpect(status().is3xxRedirection())
.andExpect(authenticated());
}
}
Key Note: springSecurity() in the code above is from import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity.
When launching with mvn spring-boot:run or even with gradle returns that issue.
***************************
APPLICATION FAILED TO START
***************************
Description:
Field userDetailsService in webroot.websrv.auth.config.WebSecurityConfiguration required a bean of type 'org.springframework.security.core.userdetails.UserDetailsService' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.security.core.userdetails.UserDetailsService' in your configuration.
BUILD SUCCESSFUL
Total time: 19.013 secs
Here are the main classes, all the requirements looks ok to me, I am using the org.springframework.boot release 1.5.7.RELEASE
package webroot.websrv.auth.config;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers(
HttpMethod.GET,
"/",
"/**/*.html",
"/**/*.{png,jpg,jpeg,svg.ico}",
"/**/*.css",
"/**/*.js"
).permitAll()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated();
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
httpSecurity.headers().cacheControl();
}
}
and:
package webroot.websrv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class WebcliApplication {
public static void main(String[] args) {
SpringApplication.run(WebcliApplication.class, args);
}
}
Using Maven or Gradle it returns the same issue. All annotations and packages names seems to be as required.
Add a bean for UserDetailsService
#Autowired
private UserDetailsService userDetailsService;
#Bean
public UserDetailsService userDetailsService() {
return super.userDetailsService();
}
I also come accross this error. In my case, I have a class JwtUserDetailsService and I have forget implement UserDetailsService. After adding implements UserDetailsService the error was disappered.
Note that: if you also have own UserDetailsService and you use Munna's answer, than you got error StackoverflowError it mean you also repited my mistake.
In Service class make annotation
#Service
to
#Service("userDetailsService")