Handling OPTIONS and CORS when using a sign in filter instead of controller
cors, jwt, spring, spring-security
Solution
Alright, finally found out how to fix this. After hours of tinkering and searching, I found that I needed to use a filter-based CORS configuration and then handle CORS preflights (OPTIONS requests) in the sign-in filter by simply returning 200 OK. The CORS filter will then add appropriate headers.
Updated configuration below (note that my `CorsConfig` is no longer needed, since we have a CORS filter in `SecurityConfig`, and `JwtAuthenticationFilter` is the same as before).
Security Config
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*"); // TODO: lock down before deploying
config.addAllowedHeader("*");
config.addExposedHeader(HttpHeaders.AUTHORIZATION);
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Bean
public JwtSignInFilter signInFilter() throws Exception {
return new JwtSignInFilter(
new AntPathRequestMatcher("/sign-in"),
authenticationManager()
);
}
@Bean
public JwtAuthenticationFilter authFilter() {
return new JwtAuthenticationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/sign-in").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(
signInFilter(),
UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
authFilter(),
UsernamePasswordAuthenticationFilter.class
);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Sign In Filter
public class JwtSignInFilter extends AbstractAuthenticationProcessingFilter {
@Autowired
private TokenAuthenticationService tokenAuthService;
public JwtSignInFilter(RequestMatcher requestMatcher, AuthenticationManager authManager) {
super(requestMatcher);
setAuthenticationManager(authManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
if (CorsUtils.isPreFlightRequest(req)) {
res.setStatus(HttpServletResponse.SC_OK);
return null;
}
if (!req.getMethod().equals(HttpMethod.POST.name())) {
res.setStatus(HttpServletResponse.SC_NOT_FOUND);
return null;
}
SignInRequest creds = new ObjectMapper().readValue(
req.getInputStream(),
SignInRequest.class
);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
creds.getEmail(),
creds.getPassword(),
emptyList()
)
);
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
tokenAuthService.addAuthentication(res, auth.getName());
}
}
Problem
I've got an `AbstractAuthenticationProcessingFilter` that I'm using to handle POST requests at path `/sign-in`. CORS preflight requests are coming back 404 because there is no path that matches. This makes sense to me. What I would like to know is if there is a way to inform Spring that there is a filter handling the POST (rather than a controller), so that Spring can dispatch the OPTIONS in the same way it would if a controller were handling the POST. Would it be bad practice to write a controller with one `PostMapping`? I'm not sure how that would behave since technically the filter handles the POST. Thanks for your help! Update Here's my setup. I originally posted from my phone so wasn't able to add these details then. See below. To reiterate, there is no controller for `/sign-in`. The POST is handled by the `JwtSignInFilter`. CORS Config ``` @EnableWebMvc @Configuration public class CorsConfig extends WebMvcConfigurerAdapter { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") // TODO: Lock this down before deploying .allowedHeaders("*") .allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name()) .allowCredentials(true); } } ``` Security Config ``` @EnableWebSecurity @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public JwtSignInFilter signInFilter() throws Exception { return new JwtSignInFilter( new AntPathRequestMatcher("/sign-in", HttpMethod.POST.name()), authenticationManager() ); } @Bean public JwtAuthenticationFilter authFilter() { return new JwtAuthenticationFilter(); } @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() .antMatchers(HttpMethod.POST, "/sign-in").permitAll() .anyRequest().authenticated() .and() .addFilterBefore( signInFilter(), UsernamePasswordAuthenticationFilter.class ) .addFilterBefore( authFilter(), UsernamePasswordAuthenticationFilter.class ); } } ``` Sign In Filter ``` public class JwtSignInFilter extends AbstractAuthenticationProcessingFilter { @Autowired private TokenAuthenticationService tokenAuthService; public JwtSignInFilter(RequestMatcher requestMatcher, AuthenticationManager authManager) { super(requestMatcher); setAuthenticationManager(authManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException { SignInRequest creds = new ObjectMapper().readValue( req.getInputStream(), SignInRequest.class ); return getAuthenticationManager().authenticate( new UsernamePasswordAuthenticationToken( creds.getEmail(), creds.getPassword(), emptyList() ) ); } @Override protected void successfulAuthentication( HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { tokenAuthService.addAuthentication(res, auth.getName()); } } ``` Authentication Filter ``` public class JwtAuthenticationFilter extends GenericFilterBean { @Autowired private TokenAuthenticationService tokenAuthService; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { Authentication authentication = tokenAuthService.getAuthentication((HttpServletRequest)request); SecurityContextHolder .getContext() .setAuthentication(authentication); filterChain.doFilter(request, response); } } ```