Unit test returns 401 Unauthorized on a permitted route in Spring WebFlux Security - spring-boot

I am trying to test a route that returns array of objects but the test fails because it returns Unauthorized instead of 200 OK
My test class
#RunWith(SpringRunner.class)
#WebFluxTest(value = CatController.class)
class ContentManagementTestApplicationTests {
#Autowired
ApplicationContext context;
#Autowired
private WebTestClient webTestClient;
#MockBean
CatRepository catRepository;
#BeforeEach
public void setup(){
webTestClient=WebTestClient.bindToApplicationContext(context)
.apply(SecurityMockServerConfigurers.springSecurity())
.configureClient()
.build();
}
#Test
void contextLoads() {
}
#Test
public void getApprovedCats(){
webTestClient.get()
.uri("/cat")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk();
}
}
And ApplicationSecurityConfig class, has a SecurityWebFilterChain Bean
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, AuthConverter jwtAuthConverter, AuthManager jwtAuthManager){
AuthenticationWebFilter jwtFilter = new AuthenticationWebFilter(jwtAuthManager);
jwtFilter.setServerAuthenticationConverter(jwtAuthConverter);
return http .csrf().disable()
.authorizeExchange()
.pathMatchers(HttpMethod.GET,"/cat").permitAll()
.anyExchange()
.authenticated()
.and()
.addFilterAt(jwtFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.formLogin().disable()
.httpBasic().disable()
.build();
}
The Error on JUint test shows following
java.lang.AssertionError: Status expected:<200 OK> but was:<401 UNAUTHORIZED>
Expected :200 OK
Actual :401 UNAUTHORIZED
Before test fails and shows Unauthorized, in console it prints
"Using generated security password: 8e5dd468-3fd1-42b6-864a-c4c2ed2227b7"
And I believe that should not be printed since I disabled it in securityFilterChain

From the Javadoc of #WebFluxTest:
Annotation that can be used for a Spring WebFlux test that focuses only on Spring WebFlux components.
Using this annotation will disable full auto-configuration and instead apply only configuration relevant to WebFlux tests
If you are looking to load your full application configuration and use WebTestClient, you should consider #SpringBootTest combined with #AutoConfigureWebTestClient rather than this annotation.
In other words, when using #WebFluxTest in your test, your SecurityWebFilterChain is not picked up.
Try annotating your class with #SpringBootTest instead.

Related

How can I unit test AuthControllerIntegrationTest and mock JwtDecoder in Spock correctly?

I have a spring security Book store api which is working as expected.
My Security Config class look like this.
#Configuration
public class SecurityConfiguration {
#Bean
JwtDecoder jwtDecoder () {
return JwtDecoders.fromIssuerLocation("https://jwt-issuer-location")
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.antMatchers(HttpMethod.GET, '/api/auth/**')
.antMatchers(HttpMethod.GET, '/api/books').authenticated()
.antMatchers(HttpMethod.GET, '/api/users').authenticated()
.anyRequest().authenticated()
.and().httpBasic()
)
.oauth2ResourceServer().jwt()
return http.build();
}
#Bean
InMemoryUserDetailsManagerService() {
UserDetails user = User.builder().username("bob").password("password123").roles("api").build()
return new InMemoryUserDetailsManager(user)
}
}
For controller integrations like returning the list of users and books, I have used #MockBean for JwtDecoder like following
#MockBean
JwtDecoder jwtDecoder
then the test works.
However, for my integration test for authentication, it does not work in the same way. The integration looks like this
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#AutocConfigureMockMvc
class AuthController extends Specification {
MockMvc mockMvc
#Autowired
WebApplicationContext context
def setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).apply(springSecurity()).build()
}
def "401 Unauthorized Exception"() {
given:
String url = "/api/books"
when:
ResultActions res = this.mockMvc.perform(get(url))
then:
response.andExpect(status().is4xxClientError())
}
}
For some reason, this auth controller integration test tries to make a http request for verifying the token sent by the user. the url is passed to jwtDecoders.fromIssuerLocation function.
I believe that is not an ideal way of make a unit test. It is not supposed to make any request to 3rd party
And since this is just the unit test, I passed some random url (I do not want to pass actual url for security purpose and push it. For the production app, I am pulling the actual url from environment variable).
Because of that, when mockMvc.perform runs, it throws I/O error on GET request for .....
I do not understand why other integration works by MockBean annotation on JwtDecoder. But it does not resolve AuthControllerIntegrationTest error.
Can someone tell I how I should mock the JwtDecoder instead?
Thank you in advance :)
I have tried adding MockBean on JwtDecoder but it does not work for AuthControllerIntegrationTest.
I would like to know how to mock the JwtDecoder correctly or maybe how to mock the JwtDecoders.fromIssuerLocation. But I do not know how to mock and return what.

How to test http status code 401 (unauthenticated) with MockMVC and Spring Boot OAuth2 Resource Server?

I am currently developing a Spring Boot 3 application which provides a REST API. To consume this API, users have to be authenticated via an OAuth2 workflow of our identity provider keycloak. Therefore, I have used org.springframework.boot:spring-boot-starter-oauth2-resource-server. When I run the application, authentification and authorization works as expected.
Unfortunately, I am unable to write a WebMvcTest for the use case when the user does not provide a JWT for authentification. In this case I expect a HTTP response with status code 401 (unauthenticated) but I get status code 403 (forbidden). Is this event possible because MockMvc mocks parts of the response processing?
I have successfully written test cases for the following to use cases.
The user provides a JWT with the expected claim => I expect status code 200 ✔
The user provides a JWT without the expected claim => I expect status code 403 ✔
I have tried to follow everything from the Spring Security documentation: https://docs.spring.io/spring-security/reference/servlet/test/index.html
Here is my code.
#WebMvcTest(CustomerController.class)
#ImportAutoConfiguration(classes = {RequestInformationExtractor.class})
#ContextConfiguration(classes = SecurityConfiguration.class)
#Import({TestConfiguration.class, CustomerController.class})
public class PartnerControllerTest {
#Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
#BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
// runs successfully
#Test
void shouldReturnListOfCustomers() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}")
.with(jwt()
.authorities(
new SimpleGrantedAuthority("basic")
)))
.andExpect(status().isOk());
}
// fails: expect 401 but got 403
#Test
void shouldReturn401WithoutJwt() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}"))
.andExpect(status().isUnauthorized());
}
// runs successfully
#Test
void shouldReturn403() throws Exception {
mockMvc.perform(
post("/search")
.contentType(MediaType.APPLICATION_JSON)
.content("{" +
"\"searchKeyword\": \"Mustermann\"" +
"}")
.with(jwt()))
.andExpect(status().isForbidden());
}
}
#org.springframework.boot.test.context.TestConfiguration
public class TestConfiguration {
#Bean
public JwtDecoder jwtDecoder() {
SecretKey secretKey = new SecretKeySpec("dasdasdasdfgsg9423942342394239492349fsd9fsd9fsdfjkldasd".getBytes(), JWSAlgorithm.HS256.getName());
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey).build();
return jwtDecoder;
}
}
#Configuration
#EnableWebSecurity
public class SecurityConfiguration {
#Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/actuator/**").permitAll()
.anyRequest().hasAuthority("Basic")
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
grantedAuthoritiesConverter.setAuthoritiesClaimName("groups");
grantedAuthoritiesConverter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
You probably have a 403 because an exception is thrown before access control is evaluated (CORS or CSRF or something).
For instance, in your security configuration, you disable sessions (session-creation policy to stateless) but not CSRF protection.
Either disable CSRF in your conf (you can because CSRF attacks use sessions) or use MockMvc csrf() post-processor in your tests.
I have many demos of resource-servers with security configuration and tests (unit and integration) in my samples and tutorials. Most have references to my test annotations and boot starters (which enable to define almost all security conf from properties without Java conf), but this one is using nothing from my extensions. You should find useful tips for your security conf and tests there.

Http401 in tests but working in normal usage

I'm using keycloak, but for tests I turned it off
keycloak.enabled = false
In my config I used almost everything to pass test as anonymous
public class KeyCloakConfig extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http
.anonymous().and() .csrf().disable()
.authorizeRequests()
.antMatchers("/ingredients").permitAll();
}
}
In test I've got 401 even when with #WithAnonymousUser
#WebMvcTest(value = IngredientController.class)
#TestPropertySource("classpath:application-development.properties")
class IngredientControllerTest {
#MockBean
IngredientService ingredientService;
#Autowired
MockMvc mockMvc;
#AfterEach
void tearDown() {
reset(ingredientService);
}
#Test
#WithAnonymousUser
void getAllIngredients() throws Exception {
given(ingredientService.findAll()).willReturn(Arrays.asList(new Ingredient(),new Ingredient()));
mockMvc.perform(get("/ingredients"))
.andExpect(status().isOk());
then(ingredientService).should().findAll();
assertThat(ingredientService.findAll()).hasSize(2);
}
}
What is funny when I use annotation #WithMockUser in test everything is ok, also turn on my web service and I go to this url everything is also ok
Also I've got basic authentication beacuse config class is disabled, I'm sure it's basic because in log I can see Using generated security password: 52cce531-b308-4d90-b76f-8984a88879fd. How to add to context slice configuration class ?
#SpringJUnitConfig(classes = TestSecurityConfig.class) doesn't work
This other stackoverflow question may help you out:
Spring Boot - How to disable Keycloak?.
The solution here is to add
spring.autoconfigure.exclude=org.keycloak.adapters.springboot.KeycloakSpringBootConfiguration
to your application-development.properties file

How test correctly the LoginForm through #WithAnonymousUser

About Spring Security
For the class that extends WebSecurityConfigurerAdapter I have the following for the configure method:
.and()
.formLogin()
.loginPage("/perfom/login")
.loginProcessingUrl("/perform_/login")
.usernameParameter("username")//default
.passwordParameter("password")//default
.defaultSuccessUrl("/welcome")
.failureUrl("/perfom/login?error") //default is /login?error
.permitAll()
It works fine in runtime. Until here no problems.
When Spring MVC Test plays I have the following:
#Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(springSecurity())
.build();
}
#Test
#WithAnonymousUser
public void saveOneHtmlGetForNonAuthenticatedPrincipalTest() throws Exception {
resultActions = mockMvc.perform(get(url).with(csrf().asHeader()).accept(MediaType.TEXT_HTML_VALUE)
.header("Accept-Language", ClientUserLocale.ENGLISH.getLocale().toString())).andDo(print());
resultActions.andExpect(status().isFound())
.andExpect(redirectedUrl("/perfom/login"));
}
The test fails with the following error message:
java.lang.AssertionError:
Redirected URL expected:</perfom/login>
but was:<http://localhost/perfom/login>
Not sure if it is the expected behavior or something missing, not sure why
http://localhost/appears.
I had the same problem and was able to fix it using redirectedUrlPattern.
In your case the solution should be:
.andExpect(redirectedUrlPattern("**/perfom/login"))

getting http 404 when testing Spring Boot with Spring Security

TLDR: spring boot test does not find url endpoint defined using spring security
Long story:
My SpringBoot application uses Spring Security.
In its Security context it defines:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/api/login").permitAll()
.and()
.formLogin()
.permitAll()
.loginProcessingUrl("/api/login")
.antMatchers(POST, "/api/**")
.hasAuthority(ADMIN)
}
}
My test code is initialized as a SpringBoot Test:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = MySpringBootApplication.class)
public class ContractVerifierBase {
#Autowired
private WebApplicationContext context;
#Before
public void setUp() throws Exception {
RestAssuredMockMvc.webAppContextSetup(context);
}
}
My test sends s POST request to /api/login and although I expect a 401 to be rturned, a 404 is returned.
ResponseOptions response = given().spec(request.post("/api/login");
Why is it not finding the /api/login?
I think you need to pass to rest assured in the base class basic authentication data. E.g.
#Before
public void setup() {
RestAssuredMockMvc.authentication =
RestAssuredMockMvc.basic("sk_test_BQokikJOvBiI2HlWgH4olfQ2", "");
}

Resources