I am trying to implement Spring Security with securityFilterChain, that uses requestMatchers. When I set it to permitAll() everything works, but when I restrict by role, then authorization headers are received as missing in the JwtAuthenticationFilter. Security configuration code:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)// TODO check how this should be in prod
.authorizeHttpRequests(auth -> auth
.requestMatchers("api/v1/auth/**").permitAll()// here should be no business logic besides auth functionality to allow this
.requestMatchers("api/v1/users/**").hasRole(USER.name())
.requestMatchers("api/v1/observer/*").hasRole(OBSERVER.name())
.requestMatchers("api/v1/user/*").hasRole(USER.name())
.requestMatchers("api/v1/admin/*").hasRole(ADMIN.name())
.anyRequest()
.authenticated()
)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.logout(logout ->
logout.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
)
;
return http.build();
}
}
Authentication filter code:
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
private final TokenRepository tokenRepository;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userName;
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
userName = jwtService.extractUserName(jwt);
if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userName);
var isTokenValid = tokenRepository.findByToken(jwt)
.map(t -> !t.isExpired() && !t.isRevoked())
.orElse(false);
if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
Also frontend gets CORS error, that I see from the Network tab in the browser, when hasRole() restriction is added, but not when permitAll() is used. I also see, that bearer is sent.
I expected to get bearer token in both situations, as user should have correct role set, but it worked only with permitAll(). I checked that controllers have @CrossOrigin correctly added. When debugging, the final String authHeader is empty when role is required, other ways it is received nicely.