I looking for a way how to test Spring Boot REST API with following setup:
#RestController
class SomeRestController {
#Autowired
private SomeService someService;
#GetMapping("/getSome")
#PreAuthorize("#canGetSome.isValid()")
public SomeObject getSomeObject() {
return someService.getSomeObject();
}
}
_
#Component
public class CanGetSome{
#Autowired
private final LoggedUser loggedUser;
public boolean isValid() {
return loggedUser.getPermissions().isCanGetSome();
}
}
_
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
...
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionFixation().newSession()
.and()
.authorizeRequests())
.anyRequest().authenticated();
}
//custom LoggedUser object which is initzialized on authentication sucessfull
#Bean
#Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public LoggedUser loggedUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (LoggedUser) authentication.getPrincipal();
}
...
}
My test case:
#SpringBootTest(
classes = SpringBootApplication,
webEnvironment = RANDOM_PORT)
#ContextConfiguration
class RestSecurityTest extends Specification {
#Autowired
private TestRestTemplate testRestTemplate
#LocalServerPort
private int port
#WithCustomMockUser
def "test testRestTemplare"(){
expect:
def respone = testRestTemplate.getForObject('http://localhost:'+ port+'/getSome', String)
}
_
public class WithCustomMockUserSecurityContextFactory implements WithSecurityContextFactory<WithCustomMockUser> {
#Override
public SecurityContext createSecurityContext(WithCustomMockUser annotation) {
//init securityContext like #WithMockUser but with LoggedUser as principal which return true on loggedUser.getPermissions().isCanGetSome();
}
}
Unfortunately I getting following response in test:
{
"timestamp": 1524759173851,
"status": 403,
"error": "Forbidden",
"message": "Access Denied",
"path": "/getSome"
}
I also debug different spring filters after request and there SecurityContext authentication is null and later is switched to AnonymousAuthenticationToken
I don't know why SecurityContext is a null after request and isn't SecurityContext which is initialized with #WithCustomMockUser annotation. Any ideas how to fix it ?
Spring MVC should be the way to go.
Test class:-
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { TestContext.class, TestWebMvcConfigurerAdapter.class, SecurityConfiguration .class })
#WebAppConfiguration
public class SomeControllerTest {
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private FilterChainProxy springSecurityFilterChain;
#Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.addFilters(this.springSecurityFilterChain)
.build();
}
#Test
public void shouldPreAuthorise() throws Exception {
this.mockMvc.perform(get("/getSome")
.with(user("user.name"))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
Test web configurer:-
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
#Configuration
#EnableWebMvc
#ComponentScan(basePackages = {
"your.package.here"
})
public class TestWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {
}
Some mock objects:-
#Configuration
public class TestContext {
#Bean
public SomeService someService() {
return mock(SomeService.class);
}
}
Related
This is my implementation of WebSecurityConfigurerAdapter:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private JWTUserDetailsService jwtUserDetailsService;
#Autowired
private JWTRequestFilter jwtRequestFilter;
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint() {
return new JWTAuthenticationEntryPoint();
}
#Bean
public JWTUserDetailsService jwtUserDetailsService() {
return new JWTUserDetailsService();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
Implementation omitted
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
implementation omitted
}
}
This is my implementation of User Detail Service:
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import tidbit.models.Reader;
import tidbit.models.ReaderRepository;
#Component
public class JWTUserDetailsService implements UserDetailsService {
#Autowired private ReaderRepository<Reader> repository;
public JWTUserDetailsService() {
super();
}
#Override
public UserDetails loadUserByUsername(String username) {
Implementation omitted
}
}
This is my implementation of the Request Filter
import io.jsonwebtoken.ExpiredJwtException;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
#Component
public class JWTRequestFilter extends OncePerRequestFilter {
private final JWTUserDetailsService jwtUserDetailsService;
private final JWTTokenUtils jwtTokenUtils;
#Autowired
public JWTRequestFilter(
JWTUserDetailsService jwtUserDetailsService, JWTTokenUtils jwtTokenUtils) {
this.jwtUserDetailsService = jwtUserDetailsService;
this.jwtTokenUtils = jwtTokenUtils;
}
#Override
protected void doFilterInternal(
Implementation omitted
}
}
In the WebSecurityConfig class I get an error for all the fields saying that Spring cannot autowire those fields:
Could not autowire. No beans of 'JWTRequestFilter' found.
I was able to solve this problem for the first two fields by creating those two methods annotated with #Bean.
However, I don't know how solve it for RequestFilter since it contains a constructor with parameters that technically should be already autowired.
In general, I think that I shouldn't have this error in the first place. I have looked at some tutorials online and they were able to autowire those fields without any problems and without creating any #bean method.
EDIT:
Thank you for your help:
Now I get the following error
Parameter 3 of constructor in tidbit.controllers.AuthenticationController required a single bean, but 2 were found:
- JWTUserDetailsService: defined in file [/Users/matteoomenetti/Documents/TidBit/backend/build/classes/java/main/tidbit/config/JWTUserDetailsService.class]
- jwtUserDetailsService: defined by method 'jwtUserDetailsService' in class path resource [tidbit/config/WebSecurityConfig.class]
It looks like I'm creating two beans of jwtUserDetailService. One is already defined in jwtUserDetailService and the other one is defined in WebSecurityConfig with the #bean annotation. However, if I already have a bean in jwtUserDetailService why isn't WebSecurityConfig finding it?
I have noticed that if I try to autowire jwtUserDetailService in any other class (without the bean method that I created) it works perfectly, meaning that the bean is found. For some reason my WebSecurityConfig class doesn't find beans...
SOLUTION
The error that I get is an error only of IntelliJ. If I try to compile through the terminal I don't get any error.
Thank you everybody
I suggest you stop using new keyword to instantiate beans. modify the following methods:
#Bean
public JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint() {
return jwtAuthenticationEntryPoint;
}
#Bean
public JWTUserDetailsService jwtUserDetailsService() {
return jwtUserDetailsService;
}
Also, modify the following class as follows:
#Component
public class JWTRequestFilter extends OncePerRequestFilter {
#Autowired
private final JWTUserDetailsService jwtUserDetailsService;
#Autowired
private final JWTTokenUtils jwtTokenUtils;
public JWTRequestFilter() {
}
#Override
protected void doFilterInternal(
Implementation omitted
}
}
You should not create new #Bean except the for the AuthenticationManager and PasswordEncoder.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JWTAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private JWTUserDetailsService jwtUserDetailsService;
#Autowired
private JWTRequestFilter jwtRequestFilter;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
//authorizeRequests() order is important
http
.csrf().disable()
.authorizeRequests().antMatchers("/**").authenticated()
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint);
}
#Override
#Bean(name = BeanIds.AUTHENTICATION_MANAGER)
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public DaoAuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(jwtUserDetailsService);
provider.setPasswordEncoder(encoder());
return provider;
}
#Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder(12);
}
}
Note: you can use the DaoAuthenticationProvider omit it by adding the jwtUserDetailsService in:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
}
Is it somehow possible to declare a RestController in a test context, preferably as a inner class of a spring boot test? I do need it for a specific test setup. I already did try following simple example as a POC:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.AutoConfigureWebClient;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
#ExtendWith(SpringExtension.class)
#WebMvcTest(ExampleTest.TestController.class)
#AutoConfigureMockMvc
#AutoConfigureWebClient
public class ExampleTest {
#Autowired
private MockMvc mockMvc;
#Test
public void exampleTest() throws Exception {
ResultActions resultActions = this.mockMvc
.perform(get("/test"));
resultActions
.andDo(print());
}
#RestController
public static class TestController {
#GetMapping("/test")
public String test() {
return "hello";
}
}
}
Testing the endpoint via MockMvc delivers 404 though. Am i missing something?
The problem is, TestController is not loaded to the application context. It could be solved by adding
#ContextConfiguration(classes= ExampleTest.TestController.class)
The test will look like:
#ExtendWith(SpringExtension.class)
#WebMvcTest(ExampleTest.TestController.class)
#ContextConfiguration(classes= ExampleTest.TestController.class)
#AutoConfigureMockMvc
#AutoConfigureWebClient
public class ExampleTest {
#Autowired
private MockMvc mockMvc;
#Test
public void exampleTest() throws Exception {
ResultActions resultActions = this.mockMvc
.perform(get("/test"));
resultActions
.andDo(print());
}
#RestController
public static class TestController {
#GetMapping("/test")
public String test() {
return "hello";
}
}
}
And the output:
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"5"]
Content type = text/plain;charset=UTF-8
Body = hello
Forwarded URL = null
Redirected URL = null
Cookies = []
I created a Spring Boot app and have trouble with some endpoints that can be triggered manually or via #Scheduled annotation.
The issue I get is the one below:
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException:
An Authentication object was not found in the SecurityContext
Is there a way to trigger SecurityContext if the process called via #Scheduled?
I am new to Spring Security and it is very hard for me to understand the reference guide. I found some similar questions but still cannot understand how to apply answers to my case.
Example of my MyController:
#Secured("ROLE_ADMIN")
#RestController
#RequestMapping(value = "/api")
public class MyController {
#Scheduled(cron = "* * * * * *")
#GetMapping(path="/data")
public void getData() {
// Do some operations
}
}
The #Scheduled method should not be #Secured. Scheduled methods are already in trusted code.
Refactor your code, e.g.
#Secured("ROLE_ADMIN")
#RestController
#RequestMapping(value = "/api")
public class MyController {
#Autowired
private MyService myService;
#PostMapping(path="/run")
public void runJobs() {
myService.runJobs();
}
}
#Service
public class MyService {
#Scheduled(cron = "* * * * * *")
public void runJobs() {
// Do some operations
}
}
Basically you have to configure your scheduler with the necessary authentication. Something in the lines of the following:
import com.google.common.collect.ImmutableList;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.concurrent.DelegatingSecurityContextScheduledExecutorService;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import java.util.List;
import java.util.concurrent.Executor;
import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
import static org.springframework.security.core.context.SecurityContextHolder.getContext;
#Configuration
public class SchedulerConfiguration implements SchedulingConfigurer {
private static final String KEY = "spring";
private static final String PRINCIPAL = "spring";
private static final List<SimpleGrantedAuthority> AUTHORITIES = ImmutableList.of(new SimpleGrantedAuthority("ADMIN"));
#Override
public void configureTasks(final ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
#Bean
public Executor taskExecutor() {
final AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(KEY, PRINCIPAL, AUTHORITIES);
final SecurityContext securityContext = getContext();
securityContext.setAuthentication(token);
return new DelegatingSecurityContextScheduledExecutorService(newSingleThreadScheduledExecutor(), securityContext);
}
}
Then you can annotate your controller with #Secured("hasAuthority('ADMIN')").
Can you help me? I need to test this controller method. But don't know what to do with httpservletresponse object.
#Controller
public class HomeController {
#PostMapping("/signout")
public String signOut(HttpServletResponse response){
response.addCookie(new Cookie("auth-token", null));
return "redirect:http://localhost:3000";
}
}
Thank you)
Spring MVC Test provides an effective way to test controllers by performing requests and generating responses through the actual DispatcherServlet.
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
#RunWith(SpringRunner.class)
#WebMvcTest(controllers=HomeController.class)
public class HomeControllerTest {
#Autowired
private MockMvc mockMvc;
#Test
public void testSignOut() throws Exception {
mockMvc.perform(post("/signout"))
.andDo(print())
.andExpect(new ResultMatcher() {
#Override
public void match(MvcResult result) throws Exception {
Assert.assertEquals("http://localhost:3000",result.getResponse().getRedirectedUrl());
}
});
}
}
In case of Spring MVC without spring boot, use standalone MockMvc support
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration // or #ContextConfiguration
public class HomeControllerTest{
#Autowired
private HomeController homeController;
private MockMvc mockMvc;
#Before
public void setup() {
// Setup Spring test in standalone mode
this.mockMvc =
MockMvcBuilders.standaloneSetup(homeController).build();
}
Below I have a customUserDetailsService property, and a tokenAuthenticationService property. I need to pass customUserDetailsService into tokenAuthenticationService but tokenAuthenticationService is a #Bean file and customUserDetailsService is a #Service which means that tokenAuthenticationService gets called first with parameter for UserDetailsService as null for the UserDetailsService parameter. I need to either delay the initiation of tokenAuthenticationService as a Bean or Turn tokenAuthenticationService into a service as well and some how pass those parameters as a constructor. How do I go about doing this ?
package app.config;
import app.repo.User.CustomUserDetailsService;
import app.security.*;
import app.security.filters.StatelessAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.sql.DataSource;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
#Order(2)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static PasswordEncoder encoder;
#Autowired
private TokenAuthenticationService tokenAuthenticationService;
#Autowired
private UserDetailsService customUserDetailsService;
#Autowired
private RESTAuthenticationEntryPoint authenticationEntryPoint;
#Autowired
private RESTAuthenticationFailureHandler authenticationFailureHandler;
#Autowired
private RESTAuthenticationSuccessHandler authenticationSuccessHandler;
public WebSecurityConfig() {
super(true);
}
#Autowired
public void configureAuth(AuthenticationManagerBuilder auth,DataSource dataSource) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").authenticated();
http.csrf().disable();
http.httpBasic();
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.formLogin().defaultSuccessUrl("/").successHandler(authenticationSuccessHandler);
http.formLogin().failureHandler(authenticationFailureHandler);
http.addFilterBefore(new StatelessAuthenticationFilter(tokenAuthenticationService),
UsernamePasswordAuthenticationFilter.class);
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
#Bean
public TokenAuthenticationService tokenAuthenticationService() {
tokenAuthenticationService = new TokenAuthenticationService("tooManySecrets", customUserDetailsService);
return tokenAuthenticationService;
}
}
You can define the userDetailsService as direct dependency of the TokenAuthenticationService like this:
#Bean
public TokenAuthenticationService tokenAuthenticationService(UserDetailsService userDetailsService) {
tokenAuthenticationService = new TokenAuthenticationService("tooManySecrets", userDetailsService);
return tokenAuthenticationService;
}
That way, Spring will make sure, that the UserDetailsService is instantiated and injected when the TokenAuthenticationService is created.
You can try annotating tokenAuthenticationService() with #Lazy. Though even if that worked its a bit unpredictable, and future modifications to this or related beans may leave you wondering on why it stopped working.
Best to declare TokenAuthenticationService as #Service & inject UserDetailsService in it.
As a side note its better to not mix #Configuration with application code to avoid these kind of issues.
Update - I don't think #Lazy is going to work here. Since you are relying on #Bean being invoked in the middle of #Autowired beans being processed.
In order for your code to work the #Autowired customUserDetailsService should be set first, then #Bean method called and then #Autowired tokenAuthenticationService should be set.