A hacky but working solution.
In you spring boot project create a package:
org.keycloak.adapters.tomcat
In the above package create a JAVA file called AbstractKeycloakAuthenticatorValve.java with the following content:
package org.keycloak.adapters.tomcat;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.catalina.*;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.*;
import org.keycloak.adapters.spi.AuthChallenge;
import org.keycloak.adapters.spi.AuthOutcome;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.enums.TokenStore;
import org.springframework.http.MediaType;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticator implements LifecycleListener {
public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
private static final Logger log = Logger.getLogger(AbstractKeycloakAuthenticatorValve.class);
protected CatalinaUserSessionManagement userSessionManagement = new CatalinaUserSessionManagement();
protected AdapterDeploymentContext deploymentContext;
protected NodesRegistrationManagement nodesRegistrationManagement;
public AbstractKeycloakAuthenticatorValve() {
}
public void lifecycleEvent(LifecycleEvent event) {
if ("start".equals(event.getType())) {
this.cache = false;
} else if ("after_start".equals(event.getType())) {
this.keycloakInit();
} else if (event.getType() == "before_stop") {
this.beforeStop();
}
}
protected void logoutInternal(Request request) {
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
if (ksc != null) {
CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, (HttpServletResponse)null);
KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
if (ksc instanceof RefreshableKeycloakSecurityContext) {
((RefreshableKeycloakSecurityContext)ksc).logout(deployment);
}
AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
tokenStore.logout();
request.removeAttribute(KeycloakSecurityContext.class.getName());
}
request.setUserPrincipal((Principal)null);
}
protected void beforeStop() {
if (this.nodesRegistrationManagement != null) {
this.nodesRegistrationManagement.stop();
}
}
public void keycloakInit() {
String configResolverClass = this.context.getServletContext().getInitParameter("keycloak.config.resolver");
if (configResolverClass != null) {
try {
KeycloakConfigResolver configResolver = (KeycloakConfigResolver)this.context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
this.deploymentContext = new AdapterDeploymentContext(configResolver);
log.debugv("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
} catch (Exception var4) {
log.errorv("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", configResolverClass, var4.getMessage());
this.deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
}
} else {
InputStream configInputStream = getConfigInputStream(this.context);
KeycloakDeployment kd;
if (configInputStream == null) {
log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
kd = new KeycloakDeployment();
} else {
kd = KeycloakDeploymentBuilder.build(configInputStream);
}
this.deploymentContext = new AdapterDeploymentContext(kd);
log.debug("Keycloak is using a per-deployment configuration.");
}
this.context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), this.deploymentContext);
AbstractAuthenticatedActionsValve actions = this.createAuthenticatedActionsValve(this.deploymentContext, this.getNext(), this.getContainer());
this.setNext(actions);
this.nodesRegistrationManagement = new NodesRegistrationManagement();
}
private static InputStream getJSONFromServletContext(ServletContext servletContext) {
String json = servletContext.getInitParameter("org.keycloak.json.adapterConfig");
if (json == null) {
return null;
} else {
log.trace("**** using org.keycloak.json.adapterConfig");
return new ByteArrayInputStream(json.getBytes());
}
}
private static InputStream getConfigInputStream(Context context) {
InputStream is = getJSONFromServletContext(context.getServletContext());
if (is == null) {
String path = context.getServletContext().getInitParameter("keycloak.config.file");
if (path == null) {
log.trace("**** using /WEB-INF/keycloak.json");
is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json");
} else {
try {
is = new FileInputStream(path);
} catch (FileNotFoundException var4) {
log.errorv("NOT FOUND {0}", path);
throw new RuntimeException(var4);
}
}
}
return (InputStream)is;
}
public void invoke(Request request, Response response) throws IOException, ServletException {
CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response);
Manager sessionManager = request.getContext().getManager();
CatalinaUserSessionManagementWrapper sessionManagementWrapper = new CatalinaUserSessionManagementWrapper(this.userSessionManagement, sessionManager);
PreAuthActionsHandler handler = new PreAuthActionsHandler(sessionManagementWrapper, this.deploymentContext, facade);
if (!handler.handleRequest()) {
this.checkKeycloakSession(request, facade);
super.invoke(request, response);
}
}
protected abstract PrincipalFactory createPrincipalFactory();
protected abstract boolean forwardToErrorPageInternal(Request var1, HttpServletResponse var2, Object var3) throws IOException;
protected abstract AbstractAuthenticatedActionsValve createAuthenticatedActionsValve(AdapterDeploymentContext var1, Valve var2, Container var3);
protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException {
CatalinaHttpFacade facade = new OIDCCatalinaHttpFacade(request, response);
KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
if (deployment != null && deployment.isConfigured()) {
AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
this.nodesRegistrationManagement.tryRegister(deployment);
CatalinaRequestAuthenticator authenticator = this.createRequestAuthenticator(request, facade, deployment, tokenStore);
AuthOutcome outcome = authenticator.authenticate();
if (outcome == AuthOutcome.AUTHENTICATED) {
return !facade.isEnded();
} else {
AuthChallenge challenge = authenticator.getChallenge();
if (challenge != null) {
challenge.challenge(facade);
}
writeResponse(response, 401, "Failed to verify token");
return false;
}
} else {
facade.getResponse().sendError(401);
return false;
}
}
private void writeResponse(HttpServletResponse response, int status, String message) throws IOException {
// Generate your intended response here, e.g.:
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(status);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, String> body = new HashMap<>();
body.put("message", message);
response.getOutputStream().println(objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(body));
}
protected CatalinaRequestAuthenticator createRequestAuthenticator(Request request, CatalinaHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
return new CatalinaRequestAuthenticator(deployment, tokenStore, facade, request, this.createPrincipalFactory());
}
protected void checkKeycloakSession(Request request, HttpFacade facade) {
KeycloakDeployment deployment = this.deploymentContext.resolveDeployment(facade);
AdapterTokenStore tokenStore = this.getTokenStore(request, facade, deployment);
tokenStore.checkCurrentToken();
}
public void keycloakSaveRequest(Request request) throws IOException {
this.saveRequest(request, request.getSessionInternal(true));
}
public boolean keycloakRestoreRequest(Request request) {
try {
return this.restoreRequest(request, request.getSessionInternal());
} catch (IOException var3) {
throw new RuntimeException(var3);
}
}
protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) {
AdapterTokenStore store = (AdapterTokenStore)request.getNote("TOKEN_STORE_NOTE");
if (store != null) {
return store;
} else {
Object store1;
if (resolvedDeployment.getTokenStore() == TokenStore.SESSION) {
store1 = this.createSessionTokenStore(request, resolvedDeployment);
} else {
store1 = new CatalinaCookieTokenStore(request, facade, resolvedDeployment, this.createPrincipalFactory());
}
request.setNote("TOKEN_STORE_NOTE", store1);
return (AdapterTokenStore)store1;
}
}
private AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment) {
AdapterTokenStore store = new CatalinaSessionTokenStore(request, resolvedDeployment, this.userSessionManagement, this.createPrincipalFactory(), this);
return store;
}
}
Basically we are modifying the file AbstractKeycloakAuthenticatorValve present in Keycloak to handle failed Authentication.