Test spring boot controllers with JUnit5+Spring Security - spring-boot

I have a spring boot application and want to write integration tests for controllers. It is my SecurityConfig:
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final MyUserDetailsService userDetailsService;
private final SessionAuthenticationProvider authenticationProvider;
private final SessionAuthenticationFilter sessionAuthenticationFilter;
#Override
public void configure(WebSecurity web) {
//...
}
#Override
protected void configure(HttpSecurity http) throws Exception {
/...
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
auth.userDetailsService(userDetailsService);
}
}
It is my controller:
#RestController
public class MyController {
//...
#GetMapping("/test")
public List<TestDto> getAll(){
List<TestDto> tests= testService.findAll(authService.getLoggedUser().getId());
return mapper.toTestDtos(tests);
}
}
I Created a test(JUnit 5):
#WebMvcTest(TestController.class)
class TestControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean(name = "mockTestService")
private TestService testService;
#Autowired
private TestMapper mapper;
#MockBean(name = "mockAuthService")
private AuthService authService;
private Test test;
#BeforeEach
void setUp() {
User user = new Test();
user.setId("userId");
when(authService.getLoggedUser()).thenReturn(user);
test = new Facility();
test.setId("id");
test.setName("name");
when(testService.findAll("userId")).thenReturn(singletonList(test));
}
#Test
void shouldReturnAllIpaFacilitiesForCurrentTenant() throws Exception {
mockMvc.perform(get("/test").contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$..id").value(test.getId()))
.andExpect(jsonPath("$..timeOut").value(test.getName()));
}
}
When I start the test I get an exception: Consider defining a bean of type 'com.auth.MyUserDetailsService' in your configuration.
It happens because I have not UserDetailsService bean in the test. What should I do:
Add 3 beans are required for SecurityConfig, like:
#MockBean
MyUserDetailsService userDetailsService;
#MockBean
SessionAuthenticationProvider authenticationProvider;
#MockBean
SessionAuthenticationFilter sessionAuthenticationFilter;
Add test implementation of SecurityConfig
something else
Which approach is better?

For writing tests for your controller endpoints using #WebMvcTest I would use the great integration from MockMvc and Spring Security.
Make sure you have the following dependency:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Next, you can mock the authenticated user for your MockMvc requests and also set the username, roles, etc.
Either use an annotation to populate the authenticated user inside the SecurityContext for the test:
#Test
#WithMockUser("youruser")
public void shouldReturnAllIpaFacilitiesForCurrentTenant() throws Exception {
// ...
}
Or use one of the SecurityMockMvcRequestPostProcessors:
this.mockMvc
.perform(
post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"taskTitle\": \"Learn MockMvc\"}")
.with(csrf())
.with(SecurityMockMvcRequestPostProcessors.user("duke"))
)
.andExpect(status().isCreated());
You can find more information on this here.

Related

Spring Boot mockMvc does not use securitycontext

My testclass wit mockmvc seems not to load the springsecuritscontext:
#WebMvcTest(ReportingController.class)
class ReportingControllerTest {
private MockMvc mockMvc;
#Autowired
private WebApplicationContext context;
#BeforeEach
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).build();
}
#WithMockUser(username = "piet", roles = { "ACTUATOR" })
#Test
void test() throws Exception {
Query query = new Query();
ObjectMapper mapper = new ObjectMapper();;
ObjectWriter ow = mapper.writer().withDefaultPrettyPrinter();
mockMvc.perform(post("/statistic/report/test").with(csrf()).sessionAttr("query", query)).andDo(print()).andExpect(view().name("test")).andExpect(status().isOk());
}
}
The test failed with "Failed to load ApplicationContext" .... Error creating bean with name 'webSecurityConfig'. The class webSecurityConfig is my extension for WebSecurityConfigurerAdapter:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private LoginSuccessHandler loginSuccessHandler;
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/vendors/**", "/build/**", "/images/**", "/favicon.ico", "/qlocation/**", "/service/**", "/help.html", "/invalid-session.html",
"/robots.txt").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().defaultSuccessUrl("/").successHandler(loginSuccessHandler)
.permitAll();
}
#Autowired
private CustomAuthenticationProvider authProvider;
#Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
...
}
My assumption is that springSecurity() should load the context or mock it away.
But I'm not sure what is missing.

AuthenticationFailureBadCredentialsEvent never called

I use spring-boot 2.6.8 with spring security
When user don't enter correct information, i would like to do an operation.
So I created this class.
#Component
public class AuthenticationFailureEventListener implements ApplicationListener<AuthenticationFailureBadCredentialsEvent {
private LoginAttemptService loginAttemptService;
#Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
WebAuthenticationDetails auth = (WebAuthenticationDetails) e.getAuthentication().getDetails();
loginAttemptService.loginFailed(e.getAuthentication().getName(), auth.getRemoteAddress());
}
}
If a user enter a bad password, this event is never called
Edit
For the security, I have this
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private AuthenticationEventPublisher authenticationEventPublisher;
#Autowired
private PasswordEncoder passwordEncoder;
#Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationEventPublisher(authenticationEventPublisher).userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}
...
}
The events are not published out of the box. You need to also declare an AuthenticationEventPublisher with code like this:
#Bean
public AuthenticationEventPublisher authenticationEventPublisher(
ApplicationEventPublisher applicationEventPublisher
) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
Please also have a look at this part of the reference documentation: https://docs.spring.io/spring-security/reference/servlet/authentication/events.html

Spring security cannot access userService while login NullPointerException

When making post request to login this error shows up telling me that userService is null.
When I create an object of UserService instead of autowiring it it passes it but tell me that the repository called in userService is null. The repository is autowired and i cannot instanciate it because its an interface.
Here is the service class:
#Service
public class MyUserDetailsService implements UserDetailsService {
#Autowired
UserService userService;
#Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return new MyUserDetails(userService.getByUsernameOrEmail(s));
}
}
And this is the security configuration class:
Also I am creating an object of MyUserService because spring cannot autowire it telling me that no bean have such name.
#Configuration
#EnableWebSecurity
public class UserSercurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(new MyUserDetailsService());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().hasRole(UserType.ADMIN.toString())
.and().formLogin();
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
You cannot instantiate method or variables in your repository as it is an interface but you can autowire your repository class to use your method declared in userRepository interface, you have to autowire your repository class.You can do it this way as I have done in the below code.
Your service class should be like this:
#Autowired
private UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = userRepository.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Could not find user");
}
return new MyUserDetails(user);
}
And your repository should be like this:
#Repository
public interface UserRepository extends JpaRepository<Users, Long> {
#Query("SELECT u FROM Users u WHERE u.name = ?1")
public Users getUserByUsername(String username);
}
And also autowire UserDetailsService in your configuration class and pass the instance userDetailsService in your configure(AuthenticationManagerBuilder auth) method, UserDetailsService provides you instance of your MyUserDetailService class.
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
UserDetailsService userDetailsService;
#Bean
public PasswordEncoder getPasswordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.and()
.formLogin().permitAll()
.defaultSuccessUrl("/", true)
.and()
.logout().permitAll()
.logoutSuccessUrl("/");
}
Try to implement in this way, If this is solution of your question please let me know and if you still have doubt feel free to ask.

Mocking JWT token in #SpringBootTest with WebTestClient

I have the following controller class in a Spring Boot project:
#GetMapping
public ResponseEntity<UserResponse> getUser(#AuthenticationPrincipal CustomUserDetails userDetails) {
try {
final UserResponse userData = userService.getUser(userDetails.getId());
return ResponseEntity.ok(userData);
} catch (UserNotFoundException e) {
log.error("User with id {} not found", userDetails.getId());
return ResponseEntity.notFound().build();
}
}
This resource is only accessible if the client sends a JWT token with Authorization: Bearer <token>. The CustomUserDetails are provided by a CustomUserDetailsService after having parsed the JWT token via a JwtRequestFilter.
Now I'd like to write a #SpringBootTest which uses a real HTTP client calling this resource. My first idea was to use a MockMvc object but then I read about the WebTestClient provided by Spring.
However I don't yet understand how I would be able to mock the JWT token. This is my initial attempt:
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureWebTestClient
public class UserControllerIT {
#Autowired
private ApplicationContext context;
private WebTestClient webTestClient;
#MockBean
private UserRepo userRepo;
#BeforeEach
public void setup() {
webTestClient = WebTestClient
.bindToApplicationContext(context)
.apply(springSecurity())
.configureClient()
.build();
}
#Test
#WithMockUser
public void someTest() {
final User user = createUser("foo#bar.com", "my-password");
when(userRepo.findById(anyLong())).thenReturn(Optional.of(user));
webTestClient
.mutateWith(mockJwt())
.get()
.uri("/user")
.header(ACCEPT, APPLICATION_JSON_VALUE)
.exchange()
.expectStatus().is2xxSuccessful();
}
This test fails with the following exception:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'webHandler' available
However I'm not sure if my approach makes sense. Is WebTestClient the "correct" way? Or do I need to use a WebClient?
My security configuration:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtRequestFilter jwtRequestFilter;
public SecurityConfiguration(JwtRequestFilter jwtRequestFilter) {
this.jwtRequestFilter = jwtRequestFilter;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(final HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors().and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.mvcMatcher("/services/**").authorizeRequests()
.mvcMatchers(PUBLIC_RESOURCES).permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
WebTestClient is the recommended replacement for TestRestTemplate.
A deep-dive into the Spring Security source at https://github.com/spring-projects/spring-security, shows some examples of use of WebTestClient with JWT. E.g.: ServerOAuth2ResourceServerApplicationITests
Given that you have a service JwtTokenProvider that is responsible for generating JWT-tokens, a test may look like below. Or, if possible, you may use constant tokens like in ServerOAuth2ResourceServerApplicationITests.
package no.yourcompany.yourapp.yourpackage;
import no.yourcompany.yourapp.configuration.JwtTokenProvider;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.test.web.reactive.server.WebTestClient;
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutoConfigureWebTestClient
public class YourIntegrationTest {
#Autowired
WebTestClient webTestClient;
#Autowired
JwtTokenProvider jwtTokenProvider;
#Test
public void postTest_withValidToken_receiveOk() {
var tokenString = jwtTokenProvider.createToken(
new UsernamePasswordAuthenticationToken("test-user", "P4ssword"));
webTestClient
.post().uri("/test")
.headers(http -> http.setBearerAuth(tokenString))
.exchange()
.expectStatus().isOk();
}
}
For WebTestClient, add this to POM
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>

Authentication should not be null in unit tests with spring-session

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.

Resources