This is a dummy project so some of the code will be example code.
Here is my Spring Boot security config:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CookieAuthenticationFilter cookieAuthenticationFilter;
public SecurityConfig(CookieAuthenticationFilter customFilter) {
this.cookieAuthenticationFilter = customFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(
s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(
a -> a
.requestMatchers("/un/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(cookieAuthenticationFilter, BasicAuthenticationFilter.class);
return http.build();
}
}
I have written CookieAuthenticationFilter
for handling authentication by a cookie token. However, whenever I try to add in response.setStatus()
within this filter, and then ping /test
it causes the AccessDenied
error to be unhandled.
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Unable to handle the Spring Security Exception because the response is already committed.] with root cause
org.springframework.security.access.AccessDeniedException: Access Denied
(rest of the stack trace not shown)
Here is the CookieAuthenticationFilter
code:
@Component
public class CookieAuthenticationFilter extends OncePerRequestFilter {
private final AuthService authService;
private final ObjectMapper objectMapper;
public CookieAuthenticationFilter(
AuthService authService,
ObjectMapper objectMapper) {
this.authService = authService;
this.objectMapper = objectMapper;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// (code that extracts the auth cookie from the request object)
// Fetch user associated with the token
UserDto user = null;
try {
user = authService.getUserFromAuthenticationToken(
new AuthenticationTokenValueDto(authCookie.getValue())
);
} catch (CustomAuthException e) {
// Map exception to request
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
objectMapper.writeValue(response.getOutputStream(), CustomAuthException.MESSAGE);
// Exit function
filterChain.doFilter(request, response); return;
}
// Add authentication to context
Authentication authentication = new PreAuthenticatedAuthenticationToken(
user,
authCookie.getValue(),
List.of()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// Run the rest of the filters
filterChain.doFilter(request, response);
}
}
Whenever I remove the response.setStatus()
and objectMapper.writeValue()
lines from the code, it causes no errors. I debugged and figured out that in both cases the error AccessDenied
is thrown, but it is not handled by Spring when I set the response status. I checked and its because ExceptionTranslationFilter
throws a ServletException
whenever response.commited()
is true. And its true after running the response.setStatus()
and objectMapper.writeValue()
lines.
So how do I avoid triggering this error, while still being able to write custom error messages to the response body and setting response codes, in the cookie filter? Do I need to delegate error handling to another component? Do I need to turn off ExceptionTranslationFilter
somehow?
After fiddling around, I found this solution. I'm not sure if it is a proper way of doing this, but it works for me
I create a new CookieAuthenticationEntryPoint
:
@Component
public class CookieAuthenticationEntryPoint implements AuthenticationEntryPoint {
public static String COOKIE_AUTH_ERROR_REQUEST_ATTR_KEY = "CookieAuthenticationError";
private final ObjectMapper objectMapper;
public CookieAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// Handle cookie authentication errors if they exist
Exception cookieAuthException;
if ((cookieAuthException = (Exception) request.getAttribute(COOKIE_AUTH_ERROR_REQUEST_ATTR_KEY)) != null) {
// Set appropriate status code and content type
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
// Write error message to response body
objectMapper.writeValue(response.getOutputStream(),
Map.of(
"code", HttpServletResponse.SC_UNAUTHORIZED,
"message", cookieAuthException.getMessage()
));
// Exit method early
return;
}
// Default behavior for all other cases
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
What it does is, it checks if there is an exception to be handled that is stored in the request attributes. If there is, it does the logic that I wrote in the CookieAuthenticationFilter
originally.
That section of the CookieAuthenticationFilter
is refactored to this:
...
} catch (CustomAuthException e) {
// Set error as request attribute
request.setAttribute(CookieAuthenticationEntryPoint.COOKIE_AUTH_ERROR_REQUEST_ATTR_KEY, e);
// Run the rest of the filters
filterChain.doFilter(request, response); return;
}
...
What that does is writes the CustomException
to the request attributes, which will be picked up later by CookieAuthenticationEntryPoint
.
Finally I just needed to register CookieAuthenticationEntryPoint
in the SecurityConfig
, like this:
...
.exceptionHandling(e -> e.authenticationEntryPoint(cookieAuthenticationEntryPoint));
...
I know that this is very tight coupling and using string values and all that might be a bad idea, but it works. If anyone knows of any other way, let me know