2

I have a small Spring Boot 2.1.6 webapp with JWT authententication. Call flow is as follows:

  1. User enters username and password and sends a POST request to /authenticate
  2. A filter is watching this URL (setFilterProcessesUrl), when a request comes, it hashes the password and checks it against the hash stored in DB
  3. If matches, and user is not locked, it creates a JWT with username and granted roles, and returns it in response
  4. User must include this JWT in all further requests

Also, CSRF is disabled in the WebSecurityConfigurerAdapter.

The solution itself is working fine, but I have to create unit tests as well. I ended up with the following test case:

@RunWith(SpringRunner.class)
@WebMvcTest
@ContextConfiguration(classes = { ConfigReaderMock.class })
public class ControllerSecurityTest {

    private static final String VALID_USERNAME = "username";
    private static final String VALID_PASSWORD = "password";

    @Autowired
    private MockMvc mockMvc;

    private String createAuthenticationBody(String username, String passwordHash) {
        return "username=" + URLEncoder.encode(username, StandardCharsets.UTF_8) + "&password="
                + URLEncoder.encode(passwordHash, StandardCharsets.UTF_8);
    }

    @Test
    public void testValidLogin() throws Exception {
        MvcResult result = mockMvc
                .perform(MockMvcRequestBuilders.post("/authenticate")
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                        .content(createAuthenticationBody(VALID_USERNAME, VALID_PASSWORD)).accept(MediaType.ALL))
                .andExpect(status().isOk()).andReturn();

        String authHeader = result.getResponse().getHeader(SecurityConstants.TOKEN_HEADER);

        mockMvc.perform(MockMvcRequestBuilders.get("/main?" + SecurityConstants.TOKEN_QUERY_PARAM + "="
                + URLEncoder.encode(authHeader, StandardCharsets.UTF_8))).andExpect(status().isOk());
    }
}

What I expect, is that the server accepts the username and password provided, and returns the JWT, which I can use in the subsequent request to access the next page (the same is implemented in the front end). Instead I get HTTP 403 from the authentication filter:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /authenticate
       Parameters = {username=[username], password=[password]}
          Headers = [Content-Type:"application/x-www-form-urlencoded", Accept:"*/*"]
             Body = <no character encoding set>
    Session Attrs = {org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN=org.springframework.security.web.csrf.DefaultCsrfToken@4ac0fdc7}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 403
    Error message = Forbidden
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body =
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

I noticed it is sending a CSRF token for some reason in the Session Attributes. Further checking the logs, I can see the belo messages:

2019-07-29 08:09:17,438 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration'
2019-07-29 08:09:17,443 DEBUG o.s.s.c.a.a.c.AuthenticationConfiguration$EnableGlobalAuthenticationAutowiredConfigurer [main] Eagerly initializing {org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration=org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration$$EnhancerBySpringCGLIB$$236da03c@4e68aede}
2019-07-29 08:09:17,444 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'inMemoryUserDetailsManager'
2019-07-29 08:09:17,445 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration'
2019-07-29 08:09:17,454 DEBUG o.s.b.f.s.DefaultSingletonBeanRegistry [main] Creating shared instance of singleton bean 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,457 DEBUG o.s.b.f.s.ConstructorResolver [main] Autowiring by type from bean name 'inMemoryUserDetailsManager' via factory method to bean named 'spring.security-org.springframework.boot.autoconfigure.security.SecurityProperties'
2019-07-29 08:09:17,462 INFO o.s.b.a.s.s.UserDetailsServiceAutoConfiguration [main] 

Using generated security password: 963b2bac-d953-4793-a8cd-b3f81586823e

...

2019-07-29 08:09:17,783 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No HttpSession currently exists
2019-07-29 08:09:17,784 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository [main] No SecurityContext was available from the HttpSession: null. A new one will be created.
2019-07-29 08:09:17,794 DEBUG o.s.s.w.c.CsrfFilter [main] Invalid CSRF token found for http://localhost/authenticate
2019-07-29 08:09:17,795 DEBUG o.s.s.w.h.w.HstsHeaderWriter [main] Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@1c15a6aa
2019-07-29 08:09:17,796 DEBUG o.s.s.w.c.HttpSessionSecurityContextRepository$SaveToSessionResponseWrapper [main] SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2019-07-29 08:09:17,799 DEBUG o.s.s.w.c.SecurityContextPersistenceFilter [main] SecurityContextHolder now cleared, as request processing completed

So it seems like Spring Security is creating it's own security configuration, instead of using the class I created, extending WebSecurityConfigurerAdapter. Question is, why? And how can I force it to use my security config, as I'm dependent on it with the database login?

Update: added WebSecurityConfigurerAdapter

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AICAuthenticationService authenticationService;

    @Autowired
    private AICUserDetailsService aicUserDetailsService;

    @Autowired
    private AICLogoutSuccessHandler aicLogoutSuccessHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .cors()
                .and()
                .authorizeRequests()
                        .antMatchers("/resources/**", "/login", "/").permitAll()
                        .anyRequest().authenticated()
                .and()
                        .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                        .addFilter(new JwtAuthorizationFilter(authenticationManager()))
                .sessionManagement()
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .logout()
                        .logoutUrl("/logout")
                        .logoutSuccessHandler(aicLogoutSuccessHandler)
                        .invalidateHttpSession(true)
                        .deleteCookies("JSESSIONID", "error");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(aicUserDetailsService);
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return authenticationService;
    }

    @Bean
    public AuthenticationManager custromAuthenticationManager() throws Exception {
        return authenticationManager();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(aicUserDetailsService);
    }
6
  • Can you include your WebSecurityConfigurerAdapter? I could imagine that /authenticate is also protected by some rule you configured. Commented Jul 29, 2019 at 7:34
  • @MarcusHeld Added it. Although as I mentioned, the same works correctly when I run it as "mvn spring-boot:run", error only happens in case of "mvn test" command. Commented Jul 29, 2019 at 8:16
  • why have you built your own JWT filter when there is already one in spring security? Commented Jul 29, 2019 at 9:14
  • @ThomasAndolf Can you please provide an example for that? I looked through a number of online tutorials, and everywhere a custom filter was created. Basically I have two filters: an "AuthenticationFilter" which takes username and password from the request on "/authenticate" and attempts authentication from database data, if succeeded it creates the JWT, and returns it. The other is "AuthorizationFilter" which verifies if the JWT is present in the request (either in header or in query param). Commented Jul 29, 2019 at 10:42
  • my guess is that you have checked out the outdated tutorials at baeldung.com that are for spring security 4. My suggestion is to read the official documentation for spring security 5. docs.spring.io/spring-security/site/docs/current/reference/… Commented Jul 29, 2019 at 12:03

1 Answer 1

1

I was able to get it done with TestRestTemplate, like this:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ControllerSecurityTest {

    private static final String VALID_USERNAME = "username";
    private static final String VALID_PASSWORD = "password";

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testValidLogin() throws Exception {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setAccept(Arrays.asList(MediaType.ALL));

        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add("username", VALID_USERNAME);
        map.add("password", VALID_PASSWORD);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

        ResponseEntity<String> tokenResponse = restTemplate
                .postForEntity("http://localhost:" + port + "/authenticate", request, String.class);

        assertEquals(200, tokenResponse.getStatusCodeValue());

        String authHeader = tokenResponse.getHeaders().getFirst(SecurityConstants.TOKEN_HEADER);

        assertNotNull(authHeader);

        ResponseEntity<String> mainResponse = restTemplate.getForEntity("http://localhost:" + port + "/main?"
                + SecurityConstants.TOKEN_QUERY_PARAM + "=" + URLEncoder.encode(authHeader, StandardCharsets.UTF_8),
                String.class);

        assertEquals(200, mainResponse.getStatusCodeValue());
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.