Mocking JWT token in #SpringBootTest with WebTestClient - spring-boot

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>

Related

configure spring security and test api postman

Iam working in springboot application and iam trying to save the data in database, code is executing properly and not getting any error during execution but when iam trying to post the url in postman iam getting status: 401 unauthorized
any quick suggestion
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Autowired
UsersService userService;
#Autowired
PasswordEncoder passwordEncoder;
#Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.csrf().disable() //TODO implementer csrf
.cors().and().authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
.antMatchers(HttpMethod.POST,"/ add-users").permitAll()
.and().authorizeRequests().anyRequest().authenticated();
}
#Service
public class UsersService implements UserDetailsService{
#Autowired
UsersRepository repo;
// #Autowired
// private PasswordEncoder passwordEncoder;
public Users save(Users u) {
String encodpass=new BCryptPasswordEncoder().encode(u.getPassword());
String confpass=new BCryptPasswordEncoder().encode(u.getConfirmepass());
u.setConfirmepass(confpass);
u.setPassword(encodpass);
u.setLock(false);
u.setEnable(true);
return repo.save(u);
}
#RestController
public class UsersController {
#Autowired
private UsersService service;
#PostMapping("/add-users")
public Users add(#RequestBody Users u) {
return service.save(u);
}`
First of all
remove this space and retest :
.antMatchers(HttpMethod.POST,"/ add-users").permitAll()
to
.antMatchers(HttpMethod.POST,"/add-users").permitAll()

mockmvc post request without csrf or invalid csrf is not failing

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();
}

Swagger UI version 3 returning a 404 page but api-docs is working

I am trying to setup swagger-UI to test my springboot REST API rather than using postman however after going through a few tutorials and some questions i can not seem to get past the 404 error when trying to access the html page via my browser.
My dependencies :
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
SpringConfig:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
#Configuration
#EnableSwagger2
public class SpringFoxConfig {
#Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
}
and my controller
#Controller
#CrossOrigin
#RequestMapping(path = "/api")
public class AppController {
#ResponseBody
#PostMapping(path ="/api")
public String home() {
return "Hello World";
}
#GetMapping(path = { "/api/Teacher/{id}"})
public TeacherPayload getTeacher(#PathVariable(required=false,name="id") String id) throws
Exception{
if (id != null) {
return teacher.getTeacher(id);
} else {
return teacher.getTeachers();
}
....
I changed my port number fro the default 8080 to 3005 but i do not think that should be the problem as i tried reverting back to 8080 to no avail.
Edit: My security config is as follows, note i permitted all paths to bypass the security whilst testing
#Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
#Autowired
private UserDetailsServiceImpl jwtUserDetailsService;
#Autowired
private JwtRequestFilter jwtRequestFilter;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/authenticate");
web.ignoring().antMatchers("/recoverPortal");
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// We don't need CSRF for this example
httpSecurity.cors();
httpSecurity.csrf().disable()
// dont authenticate this particular request
.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "**").permitAll()
.antMatchers("/**").permitAll()
.antMatchers("/newUser").authenticated()
.antMatchers("/newUser").authenticated()
.antMatchers("/admin/**").hasAnyAuthority("USER_ADMIN")
.antMatchers("/resetPassword").authenticated()// all other requests need to be authenticated
.anyRequest().authenticated().and().
// make sure we use stateless session; session won't be used to
// store user's state.
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
What endpoint are you hitting? It should be /swagger-ui/ at the end, not /swagger-ui.html. Also, I think you can omit the springfox-swagger-ui dependency. All I need in my setup is the springfox-boot-starter one.

Test spring boot controllers with JUnit5+Spring Security

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.

How to get Spring Security to respond to Pre-Flight CORS request with OAUTH2

I am not a Java programmer by profession. I am working on building an app for our lab. It started out as a Spring MVC app with JSP files. It has now migrated to a Spring REST API that uses OAUTH2 as a standalone authentication and authorization server. I am using a MySQL database to store my data and users. I can successfully get an access_token issued if I use Postman to access the server. However if I use the Angular 2 client I setup I cannot get past the pre-flight OPTIONS request sent by the client to the server. I have tried several different ways to configure a CORS filter, but I cannot get that filter to get implemented and I always get a 401 code returned for the pre-flight request. I have tried implementing the suggested solution in the Spring blog https://spring.io/blog/2015/06/08/cors-support-in-spring-framework but that hasn't worked yet.
In my pom.xml file I am using these versions of Spring
Spring Framework - 4.3.2.RELEASE
Spring Security - 4.1.2.RELEASE
Spring Security OAUTH2 - 2.0.10.RELEASE
I also included Spring Boot 1.4.0 when trying to use FilterRegistrationBean
Here is my code as it is right now.
WebConfig Class:
#EnableWebMvc
#Configuration
#ComponentScan(basePackages={"eng.lab"})
#Import({ApplicationContext.class})
#PropertySource("classpath:application.properties")
public class WebConfig extends WebMvcConfigurerAdapter{
/* #Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}*/
}
SecurityConfig Class:
#Configuration
#EnableWebSecurity
#PropertySource("classpath:application.properties")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
DataSource dataSource;
#Autowired
public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery(
"select username,password, enabled from user where username=?")
.authoritiesByUsernameQuery(
"select u.username, r.role from User u, Role r where r.id=u.roleId and u.userName=?");
}
#Autowired
private ClientDetailsService clientDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.anonymous().disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll()
//.requestMatchers(CorsUtils::isCorsRequest).permitAll()
.antMatchers("/oauth/token").permitAll();
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
#Bean
#Autowired
public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){
TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
handler.setTokenStore(tokenStore);
handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
handler.setClientDetailsService(clientDetailsService);
return handler;
}
#Bean
#Autowired
public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore);
return store;
}
}
MethodSecurityConfig Class:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
#SuppressWarnings("unused")
#Autowired
private SecurityConfig securityConfig;
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
WebAppInitializer Class:
public class WebAppInitializer implements WebApplicationInitializer{
#Override
public void onStartup(ServletContext container) throws ServletException {
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(WebConfig.class);
container.addListener(new ContextLoaderListener(rootContext));
DelegatingFilterProxy filter = new DelegatingFilterProxy("springSecurityFilterChain");
DispatcherServlet dispatcherServlet = new DispatcherServlet(rootContext);
ServletRegistration.Dynamic registration = container.addServlet("dispatcherServlet", dispatcherServlet);
container.addFilter("springSecurityFilterChain", filter).addMappingForUrlPatterns(null, false, "/*");
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
SimpleCorsFilter Class:
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter implements Filter {
public SimpleCorsFilter() {
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
#Override
public void init(FilterConfig filterConfig) {
}
#Override
public void destroy() {
}
}
If I include a second WebSecurityConfig class I do get a 403 code instead.
MyWebSecurity Class:
#Configuration
#EnableWebSecurity
#Order(-1)
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll();
}
}
Any suggestions as to how to get past the pre-flight issue beyond waiting for the DATAREST-573 bug to be fixed?
UPDATE: I tried Benjamin's suggestion, but not sure I implemented the solution properly. I have edited my WebConfig class as folows but the pre-flight still fails. The filter still isn't getting applied.
package eng.lab.config;
import javax.servlet.Filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
#EnableWebMvc
#Configuration
#ComponentScan(basePackages={"eng.lab"})
#Import({ApplicationContext.class})
#PropertySource("classpath:application.properties")
public class WebConfig extends WebMvcConfigurerAdapter{
//more custome rule beans
#Bean
public FilterRegistrationBean simpleCORSFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new SimpleCorsFilter());
registration.addUrlPatterns("/*");
//registration.addInitParameter("paramName", "paramValue");
registration.setName("simpleCorsFilter");
registration.setOrder(0);
return registration;
}
#Bean(name = "simpleCorsFilter")
public Filter simpleCorsFilter() {
return new SimpleCorsFilter();
}
/* #Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}*/
}
Your CORS filter is not registered. As this is a standard javax.servlet.Filter instance, you need to register it against a FilterRegistrationBean or declare it in your web.xml file.
You can refer to this SO question for more details: How to add a filter class in Spring Boot?

Resources