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); } } ```

Original source