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]