6

I am having some problems when testing an oauth2 resource server using @WebMvcTest and the POST HTTP method.

I always receive a 403 status code when I don't send the csrf token, even though the token is not required when I am using a bearer token.

Here is the POST method that I want to test.

@PostMapping("/message")
public String createMessage(@RequestBody String message) {
    return String.format("Message was created. Content: %s", message);
}

Here is my security config:

http.authorizeRequests(authorizeRequests -> authorizeRequests       
   .antMatchers("/message/**")
   .hasAuthority("SCOPE_message:read")
   .anyRequest().authenticated()
).oauth2ResourceServer(oauth2ResourceServer ->               
    oauth2ResourceServer
    .jwt(withDefaults())
);

I am following the tests provided in the samples of spring-security.

The following test was supposed to pass but it fails because the csrf token is not sent in the request.

mockMvc.perform(post("/message").content("Hello message")
    .with(jwt(jwt -> jwt.claim("scope", "message:read")))
    .andExpect(status().isOk())
    .andExpect(content().string(is("Message was created. Content: Hello message")));

When I add the csrf token to the request, the test passes:

mockMvc.perform(post("/message").content("Hello message")
    .with(jwt(jwt -> jwt.claim("scope", "message:read")))
    .with(csrf()))
    .andExpect(status().isOk())
    .andExpect(content().string(is("Message was created. Content: Hello message")));

When I run the application, there is no need to send a csrf token in the POST request.

I have forked the Spring Security GitHub repository and the project with this failing test is available at this link.

Is there a way for me to configure my tests so I don't need to send the csrf token in the POST request?

henriquels
  • 518
  • 4
  • 20
  • You said "When I run the application, there is no need to send a csrf token in the POST request.", how did you try this? A CSRF token is required for all POST requests unless explicitly disabled using `.csrf(csrf -> csrf.disable())` in your security configuration. The sample project tests are all for GET requests, so they don't require a CSRF token. – Eleftheria Stein-Kousathana Jul 19 '19 at 09:35
  • Actually, the CSRF token is not required in runtime. When I run the following command: `curl -v -H "Authorization: Bearer $TOKEN" -d "my message" -X POST localhost:8080/message` I receive a 200 status. As far as I understand, the CSRF filter only checks the token if there is no Bearer token. – henriquels Jul 19 '19 at 13:56
  • My mistake, you are correct that the CSRF token is not needed if there is a Bearer token. The issue with the test that only includes the `jwt` post processor is that while this creates the security context containing the JWT, it does not create a Bearer token in the request (which is what the CSRF filter is looking for). – Eleftheria Stein-Kousathana Jul 19 '19 at 16:17

2 Answers2

8

In order for the CSRF filter to detect that you are using a JWT token, you will need to include the JWT token in your request as an Authorization header, or as a request parameter.
The tests that you have mentioned have a mock JwtDecoder, which means you can use any string as your token and mock the decoded value.
Your test would then become:

Jwt jwt = Jwt.withTokenValue("token")
        .header("alg", "none")
        .claim("scope", "message:read")
        .build();
when(jwtDecoder.decode(anyString())).thenReturn(jwt);
mockMvc.perform(post("/message")
        .content("Hello message")
        .header("Authorization", "Bearer " + jwt.getTokenValue()))
        .andExpect(status().isOk())
        .andExpect(content().string(is("Message was created. Content: Hello message")));

If you are not mocking the JwtDecoder then you would need to retrieve a valid bearer token and pass that in the Authorization header.

  • it worked, thanks! But wouldn't it be possible to use the JwtRequestPostProcessor in the POST request? The code for testing the GET method is cleaner. – henriquels Jul 19 '19 at 17:16
  • @henriquels The `JwtRequestPostProcessor` doesn't add the header that is needed by the CSRF filter. You could write a custom post processor, similar to [this one](https://github.com/spring-projects/spring-security/blob/b93528138e2b2f7ad34d10a040e6b7ac50fc587a/samples/boot/oauth2resourceserver-static/src/integration-test/java/sample/OAuth2ResourceServerApplicationITests.java). – Eleftheria Stein-Kousathana Jul 21 '19 at 14:13
0

First with Bearer access-token, you might be able to disable sessions (STATELESS session-management), and, as so, CSRF protection (CSRF attack vector is session).

Second there is a .csrf() MockMvc request post-processor which would simulate the CSRF token if you want to keep cessions (and CSRF protection).

Last, what jwt() request post-processor does is configuring directly the security context with a JwtAuthenticationToken instance, not creating a valid JWT token set as Bearer access-token. With MockMvc, Authorization header is not decoded nor converted to an Authentication instance. => What you should set before MockMvc request execution for your test to pass are authorities, not scope claim:

.with(jwt(jwt -> jwt.authorities(List.of(new SimpleGrantedAuthority("SCOPE_message:read")))))

Note that you could also simply decorate your test function with:

@WithMockJwtAuth("SCOPE_message:read")

From this lib I maintain

ch4mp
  • 6,622
  • 6
  • 29
  • 49