Working with OPTIONS and CORS when using a signed filter instead of a controller
I have AbstractAuthenticationProcessingFilter
one that I am using to handle POST requests on a path /sign-in
. CORS preflight requests return 404 because there is no path that matches. This makes sense to me.
I would like to know if there is a way to tell Spring that there is a filter that handles POST (and not a controller) so that Spring can send OPTIONS the same as if the controller was handling POST. Would it be bad practice to write a controller with one PostMapping
? I'm not sure how this would look since the filter technically handles POST.
Thank you for your help!
Update
Here's my setup. I was originally sent from a phone, so I was unable to add this details. See below. To repeat, there is no controller for /sign-in
. POST is being processed JwtSignInFilter
.
CORS configuration
@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 configuration
@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
);
}
}
Input 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);
}
}
source to share
Ok, finally figured out how to fix this. After hours of tinkering and searching, I found that I need to use a filter based CORS configuration and then handle CORS pre-programs (OPTIONS requests) in a signal filter, just returning 200 OK. The CORS filter will then add the appropriate headers.
Updated config below (note that mine is CorsConfig
no longer needed since we have a CORS filter in SecurityConfig
, but JwtAuthenticationFilter
the same as before).
Security configuration
@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();
}
}
Input 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());
}
}
source to share