I'm running into a weird situation.
Here's what I know:
In order to get it to log in, I need to send the credentials as a x-www-form-urlencoded POST request. User provides correct credentials, it passes and provides the user with an access token and a refresh token, if not, it fails and does not provide any of those tokens - which is what one would expect.
Now, once logged in and I try to access a resource - in this case it's the list of users I have - it fails with a 403 forbidden error. I'm currently using PostMan to test the API. The initial POST request I made after login is with a Bearer Token authorisation where in I use the access token. It fails. After debugging my code on Spring Boot:
@Slf4j
public class AuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if(request.getServletPath().equals("api/login") || request.getServletPath().equals("/api/token/refresh")) {
filterChain.doFilter(request, response);
} else {
String authorizationHeader = request.getHeader(AUTHORIZATION);
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
try {
String token = authorizationHeader.substring("Bearer ".length());
// TODO: Refactor this to a utility class.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = verifier.verify(token);
String username = decodedJWT.getSubject();
String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
stream(roles).forEach(role -> {
authorities.add(new SimpleGrantedAuthority(role));
});
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (Exception exception) {
log.error("Error logging in: " exception.getMessage());
Map<String, String> error = new HashMap<>();
error.put("error_message", exception.getMessage());
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), error);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
I found that token returns null. OK... let's try sending it as x-www-form-urlencoded instead. token picks up the token sent... Great! stepped in all the way through the code after filterChain.doFilter(request, response); and get to requiresAuthentication() in Spring Boots code... this returns false which is good... This is looking really good.
However, return to Postman - Status: 403 Forbidden. Oh - how? Looking at the logs for Spring Boot I picked out the following which is related to my url entrypoint:
2022-04-16 08:36:47.267 DEBUG 21000 --- [nio-8102-exec-4] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1683533175<open>)] after transaction
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.a.dao.DaoAuthenticationProvider : Failed to authenticate since no credentials provided
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.s.s.w.a.Http403ForbiddenEntryPoint : Pre-authenticated entry point called. Rejecting access
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] s.s.w.c.SecurityContextPersistenceFilter : Cleared SecurityContextHolder to complete request
2022-04-16 08:36:47.275 DEBUG 21000 --- [nio-8102-exec-4] o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/error]
2022-04-16 08:36:47.285 DEBUG 21000 --- [nio-8102-exec-4] o.s.security.web.FilterChainProxy : Securing GET /error
Being new to this, I'm not sure how I should proceed to fix this.
EDIT:
This is my current security configuration:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Lazy
private final UserDetailsService userDetailsService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.cors();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests().antMatchers("/login/**", "/api/login/**", "/api/token/refresh/**").permitAll();
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/users/**").hasAnyAuthority("ROLE_USER");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/user/save/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().antMatchers(HttpMethod.POST, "/api/role/assign-to-user/**").hasAnyAuthority("ROLE_ADMIN");
http.authorizeRequests().anyRequest().authenticated();
http.addFilter(new AuthenticationFilter(authenticationManagerBean()));
http.addFilterBefore(new AuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
protected CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("*"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
}
AuthenticationFilter:
@Slf4j
public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public AuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException {
String username = request.getParameter("username");
String password = request.getParameter("password");
log.info("Username: \"" username "\", password: \"" password "\"");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(authenticationToken);
}
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authentication
) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
// TODO: Generate secret key that is a little more secure than this.
Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
String accessToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() 10 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.withClaim("roles", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.sign(algorithm);
String refreshToken = JWT.create()
.withSubject(user.getUsername())
.withExpiresAt(new Date(System.currentTimeMillis() 30 * 60 * 1000))
.withIssuer(request.getRequestURL().toString())
.sign(algorithm);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", accessToken);
tokens.put("refresh_token", refreshToken);
response.setContentType(APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getOutputStream(), tokens);
}
}
CodePudding user response:
I noticed that AuthorizationFilter creates authorities but then does not set them in the UsernamePasswordAuthenticationToken. Your security matchers require an authority.
