-1

My SpringBoot application makes use of an authentication and authorization filter to accept a request to the '/api/v1/login' URL, check the credentials of the request, and add the bearer JWT token to the response header if successful.

When I run both my API and seperate CORS React project, I am able to signup, login and access site features as one user before literally just clicking signup and login again (no logout necessary) and it works fine.

However, in my integration tests I am calling one method to sign up multiple users and login as one which is successful, but when I try to send another login call within the joinLeague() test, the server responds with a 401 error:

ERROR MESSAGE: unauthorized.

AuthorizationFilter:

public class AuthorizationFilter extends BasicAuthenticationFilter {

public AuthorizationFilter(AuthenticationManager authenticationManager) { super(authenticationManager);}

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)

        throws IOException, ServletException {

        String header = request.getHeader("Authorization");
        
        if(header == null || !header.startsWith("Bearer")) {

        filterChain.doFilter(request,response);

        return;
        }

        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request,response);

        }


private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        if(token != null) {

        String user = Jwts.parser().setSigningKey("SecretKeyToGenJWTs".getBytes())
        .parseClaimsJws(token.replace("Bearer",""))
        .getBody()
        .getSubject();

        if(user != null) {

        return new UsernamePasswordAuthenticationToken(user, null, new ArrayList<>());

        }
        return null;
        }
        return null;
        }
}

AuthenticationFilter:

  public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private String apiVersion = "v1";

    private AuthenticationManager authenticationManager;

    public AuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        setFilterProcessesUrl("/api/v1/login");
    }
    @Override

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            com.example.gambit2.domain.User creds = new ObjectMapper().readValue(request.getInputStream(), com.example.gambit2.domain.User.class);
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(creds.getUsername(), creds.getPassword(),new ArrayList<>()));
        }

    catch(IOException e) {
            throw new RuntimeException("Could not read request" + e);
        }

    }
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain, Authentication authentication)
    {
        String token = Jwts.builder()
                .setSubject(((User) authentication.getPrincipal()).getUsername())
                .setExpiration(new Date(System.currentTimeMillis() + 864_000_000))
                .signWith(SignatureAlgorithm.HS512, "SecretKeyToGenJWTs".getBytes())
                .compact();
        response.addHeader("Authorization","Bearer " + token);
    }
}

Test:

@Transactional
    @Test
    public void joinLeague() throws Exception {

        String token = signupAndLogin();

        System.out.println(token);

        createLeague(token);

        //Correctly grabs the right user.
        User user = userRepository.findByUsername(username2);


        System.out.println();
        System.out.println(user.toString());
        assertThat(user.getId() == 2);

        MvcResult mvcResult2 = mvc.perform(post("/api/v1/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(user)))
                .andReturn();
        String token2 = mvcResult2.getResponse().getHeader("Authorization");
        System.out.println("Bearer Token: USER 2 TEST" + token2);
        assertThat(token2);

        System.out.println();
        System.out.println(token2);
        System.out.println();

        String content = "{\"username\":\"pp\", \"leagueId\":\"1\"}";

        MvcResult mvcResult = mvc.perform(post("http://localhost:8080/api/v1/leagues/join-league")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content).header("Authorization", token2))
                .andExpect(status().isOk())
                .andReturn();

        String jsonResult = mvcResult.getResponse().getContentAsString();

        System.out.println(jsonResult);
}

signupAndLogin() {
 User user = new User(1L, username1, "ExamplePass72-", "London");

    mvc.perform(post("http://localhost:8080/api/v1/users/signup")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(asJsonString(user)))
            .andExpect(status().isOk());

    Optional<User> savedUser = userService.getUser(1L);
    System.out.println(savedUser.get().getUserStats().get(0).toString());
    

    //2nd User
    User user2 = new User(2L, username2, "ExamplePass72-", "London");



    mvc.perform(post("http://localhost:8080/api/v1/users/signup")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(asJsonString(user2)))
            .andExpect(status().isOk());

    Optional<User> savedUser2 = userService.getUser(2L);
    System.out.println(savedUser2.toString());

    //3rd User
    User user3 = new User(3L, username3, "ExamplePass72-", "London");



    mvc.perform(post("http://localhost:8080/api/v1/users/signup")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(asJsonString(user3)))
            .andExpect(status().isOk());

    Optional<User> savedUser3 = userService.getUser(3L);
    System.out.println(savedUser3.toString());

    //LOGIN

    MvcResult mvcResult = mvc.perform(post("/api/v1/login")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(asJsonString(user)))
            .andExpect(status().isOk())
            .andReturn();
    String token = mvcResult.getResponse().getHeader("Authorization");

    //PRINTS THE CORRECT BEARER TOKEN, EVERYTHING WORKS FINE.
    System.out.println("Bearer Token: " + token);

    return token;

}

Also if I put the 2nd login directly after the 1st login in the above method, it logs in fine and returns a different bearer token.

Any help would be greatly appreciated, thanks.

Edit: WebSecConfig:

@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {


        private UserDetailsService userDetailsService;

        @Bean
        public BCryptPasswordEncoder passwordEncoder() {
                return new BCryptPasswordEncoder();
        }

        private static final String[] AUTH_WHITELIST = {
                "/v2/api-docs",
                "/swagger-resources",
                "/swagger-resources/**",
                "/configuration/ui",
                "/configuration/security",
                "/swagger-ui.html",
                "/webjars/**",
                "/api/v1/**"
        };

        public WebSecurityConfiguration(UserDetailsService userDetailsService) {
                this.userDetailsService = userDetailsService;

        }

        protected void configure(HttpSecurity httpSecurity) throws Exception {

                httpSecurity.cors().and().csrf().disable().authorizeRequests()

                        .antMatchers(AUTH_WHITELIST).permitAll()
                        .antMatchers(HttpMethod.GET, "/users/signup").permitAll()

                        .antMatchers(HttpMethod.POST, "/users/signup").permitAll()
                        .antMatchers(HttpMethod.GET, "/login").permitAll()

                        .antMatchers(HttpMethod.POST, "api/v1/login").permitAll()
                        .anyRequest().authenticated()

                        .and().addFilter(new AuthenticationFilter(authenticationManager()))

                        .addFilter(new AuthorizationFilter(authenticationManager()))

                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

                httpSecurity.csrf().disable();
        }

        public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
                authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }

        @Bean
        public CorsConfigurationSource corsConfigurationSource() {

                final CorsConfiguration configuration = new CorsConfiguration();
                List<String> allowedOrigins = new ArrayList<>();
                allowedOrigins.add("http://localhost:8080");
                allowedOrigins.add("http://localhost:3000");
                configuration.setAllowedOrigins(allowedOrigins);
                configuration.setAllowedMethods(List.of("HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
                // setAllowCredentials(true) is important, otherwise:
                // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
                configuration.setAllowCredentials(true);
                // setAllowedHeaders is important! Without it, OPTIONS preflight request
                // will fail with 403 Invalid CORS request
                configuration.setAllowedHeaders(List.of("Cache-Control", "Content-Type", "Authorization"));
                // allow header "Location" to be read by clients to enable them to read the location of an uploaded group logo
                configuration.addExposedHeader("Authorization");
                final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
                source.registerCorsConfiguration("/**", configuration);
                return source;
        }



        @Override
        public void configure(WebSecurity web) throws Exception {
                web.ignoring().regexMatchers("^/users/signup$");
        }
}

When I test by putting this in the signUpAndLogin()

MvcResult mvcResult = mvc.perform(post("/api/v1/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(user)))
                .andExpect(status().isOk())
                .andReturn();
        String token = mvcResult.getResponse().getHeader("Authorization");
        System.out.println("Bearer Token: " + token);

        MvcResult mvcResult2 = mvc.perform(post("/api/v1/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(asJsonString(user2)))
                .andExpect(status().isOk())
                .andReturn();
        String token2 = mvcResult2.getResponse().getHeader("Authorization");
        System.out.println("Bearer Token: " + token2);
        
        
        assertThat(!token2.equals(token));

As previously, but now with immediately logging in as user 2, it works fine and token 2 != token 1.

Thanks

Error:

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: Multiple representations of the same entity [com.example.gambit2.domain.User#1] are being merged. Managed: [com.example.gambit2.domain.User@1ec8afc4]; Detached: [com.example.gambit2.domain.User@4addfb95]; nested exception is java.lang.IllegalStateException: Multiple representations of the same entity [com.example.gambit2.domain.User#1] are being merged. Managed: [com.example.gambit2.domain.User@1ec8afc4]; Detached: [com.example.gambit2.domain.User@4addfb95]
devo9191
  • 219
  • 3
  • 13
  • I am not sure about extending `BasicAuthenticationFilter` and using JWT. Maybe extend `OncePerRequestFilter`. Have you tried debugging to check if you code in filter is executing correctly and setting the authentication object? What is your security configuration? – Boris Jan 17 '22 at 14:39
  • @Boris I added the config to the question, the thing is that it works completely fine for requests sent by my react frontend, therefore It must be something to do with the test maybe? Also when I put the 2 logins next to each other in the signUpAndLogin() they both work fine, I added to the question also. Thanks. – devo9191 Jan 17 '22 at 14:52

1 Answers1

1

I think that this is the problem. In test you are doing this:

MvcResult mvcResult = mvc.perform(post("http://localhost:8080/api/v1/leagues/join-league")
        .contentType(MediaType.APPLICATION_JSON)
        .content(content).header("Authorization", token2))
    .andExpect(status().isOk())
    .andReturn();

and in filter you are doing this:

        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer")) {

            filterChain.doFilter(request, response);

            return;
        }

The Authorisation in your request does not contain Bearer to start with. And because of that you are entering this condition. So you will not set authorisation in your request using:

UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

filterChain.doFilter(request, response);

before going down the chain.

Try this:

MvcResult mvcResult = mvc.perform(post("http://localhost:8080/api/v1/leagues/join-league")
        .contentType(MediaType.APPLICATION_JSON)
        .content(content).header("Authorization", "Bearer " + token2))
    .andExpect(status().isOk())
    .andReturn();

Also, be aware of this in your filter, because of space after Bearer

.parseClaimsJws(token.replace("Bearer",""))
Boris
  • 726
  • 1
  • 10
  • 22
  • Thanks so much for your response and help, I think that is certainly correct or at least part of the issue, since changing it to what you have said I now have a different error, that I have added to the question, thanks. – devo9191 Jan 17 '22 at 18:38
  • Hey I managed to fix it, it says "io.jsonwebtoken.MalformedJwtException: JWT strings must contain exactly 2 period characters. Found: 0", because the call to login controller is returning null for some reason, thanks. – devo9191 Jan 17 '22 at 19:25
  • Could it be because the request to login is already carrying a token from the last login? – devo9191 Jan 17 '22 at 19:28
  • Hi. Error that you specified in question is related to JPA. I am not sure how your entities look like. Check this out, maybe it will be more insightful: https://stackoverflow.com/questions/26591521/java-lang-illegalstateexception-multiple-representations-of-the-same-entity-wit – Boris Jan 17 '22 at 19:48
  • I was able to fix that don't worry, but now the call to login is returning null, despite it working on frontend project/react. Thanks. – devo9191 Jan 17 '22 at 19:52
  • And for error from comments, maybe it is because on login you do not have JWT, so filter will try to get authorization, so maybe there is another error in request. Try filter like this: https://pastebin.com/9kq3daYp – Boris Jan 17 '22 at 19:55
  • But the first user can be logged in fine, it is only the second user that cannot login. thanks, – devo9191 Jan 17 '22 at 19:59
  • Is it possible fotr you to share this code, on github for example, so I could play with it, to check it out? – Boris Jan 17 '22 at 20:00