I managed to customize responses in the following way:
A class with arbitrary name which implements AuthenticationFailureHandler
and AccessDeniedHandler
interfaces:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.keycloak.adapters.springsecurity.authentication.KeycloakCookieBasedRedirect;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
// ... other required imports
@Component
public class CustomSecurityFailureResponseHandler implements AuthenticationFailureHandler, AccessDeniedHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
// The body of this method has been copied from its Keycloak counterpart, except for the marked line.
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
if (!response.isCommitted()) {
if (KeycloakCookieBasedRedirect.getRedirectUrlFromCookie(request) != null) {
response.addCookie(KeycloakCookieBasedRedirect.createCookieFromRedirectUrl(null));
}
writeResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed"); // <--- marked line
} else {
if (200 <= response.getStatus() && response.getStatus() < 300) {
throw new RuntimeException("Success response was committed while authentication failed!", exception);
}
}
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
writeResponse(response, HttpServletResponse.SC_FORBIDDEN, "Access denied");
}
private void writeResponse(HttpServletResponse response, int status, String message) throws IOException {
// Generate your intended response here, e.g.:
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getOutputStream().println(
objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(new MessageResponse(status, message)));
}
record ResponseMessage(int statusCode, String message) {}
}
in the keycloak security config:
@KeycloakConfiguration
@EnableGlobalMethodSecurity(jsr250Enabled = true)
@Import(KeycloakSpringBootConfigResolver.class)
@RequiredArgsConstructor
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
// An object of preceding class is injected here
private final CustomSecurityFailureResponseHandler customSecurityFailureResponseHandler;
// Other required methods has been omitted for brevity
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(customSecurityFailureResponseHandler); // for 403 Forbidden
}
// This method is overridden to customize 401 Unauthorized response
@Override
public KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = super.keycloakAuthenticationProcessingFilter();
filter.setAuthenticationFailureHandler(customSecurityFailureResponseHandler);
return filter;
}
}
The method keycloakAuthenticationProcessingFilter()
is overridden to replace favorable AuthenticationFailureHandler
.
The Keycloak 19.0.3
and the Spring boot 2.7.4
have been used.
I hope this workaround helps someone.
Any better solution is appreciated.