KeycloakErrorHandler.java
package org.keycloak.services.error;
import static org.keycloak.services.resources.KeycloakApplication.getSessionFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.OAuthErrorException;
import org.keycloak.forms.login.freemarker.model.UrlBean;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionTaskWithResult;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelIllegalStateException;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.theme.Theme;
import org.keycloak.theme.beans.LocaleBean;
import org.keycloak.theme.beans.MessageBean;
import org.keycloak.theme.beans.MessageFormatterMethod;
import org.keycloak.forms.login.MessageType;
import org.keycloak.theme.freemarker.FreeMarkerProvider;
import org.keycloak.utils.KeycloakSessionUtil;
import org.keycloak.utils.MediaType;
import org.keycloak.utils.MediaTypeMatcher;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Provider
public class KeycloakErrorHandler implements ExceptionMapper<Throwable> {
private static final Logger logger = Logger.getLogger(KeycloakErrorHandler.class);
private static final Pattern realmNamePattern = Pattern.compile(".*/realms/([^/]+).*");
public static final String UNCAUGHT_SERVER_ERROR_TEXT = "Uncaught server error";
public static final String ERROR_RESPONSE_TEXT = "Error response {0}";
@Override
public Response toResponse(Throwable throwable) {
KeycloakSession session = KeycloakSessionUtil.getKeycloakSession();
if (session == null) {
// errors might be thrown when handling errors from JAX-RS before the session is available
return KeycloakModelUtils.runJobInTransactionWithResult(getSessionFactory(),
new KeycloakSessionTaskWithResult<Response>() {
@Override
public Response run(KeycloakSession session) {
return getResponse(session, throwable);
}
});
}
return getResponse(session, throwable);
}
public static Response getResponse(KeycloakSession session, Throwable throwable) {
KeycloakTransaction tx = session.getTransactionManager();
tx.setRollbackOnly();
Response.Status responseStatus = getResponseStatus(throwable);
boolean isServerError = responseStatus.getFamily().equals(Response.Status.Family.SERVER_ERROR);
if (isServerError) {
logger.error(UNCAUGHT_SERVER_ERROR_TEXT, throwable);
} else {
logger.debugv(throwable, ERROR_RESPONSE_TEXT, responseStatus);
}
HttpHeaders headers = session.getContext().getRequestHeaders();
if (!MediaTypeMatcher.isHtmlRequest(headers)) {
OAuth2ErrorRepresentation error = new OAuth2ErrorRepresentation();
error.setError(getErrorCode(throwable));
if (throwable.getCause() instanceof ModelException) {
error.setErrorDescription(throwable.getMessage());
} else if (throwable instanceof JsonProcessingException || throwable.getCause() instanceof JsonProcessingException) {
error.setErrorDescription("Cannot parse the JSON");
} else if (isServerError) {
error.setErrorDescription("For more on this error consult the server log.");
}
return Response.status(responseStatus)
.header(HttpHeaders.CONTENT_TYPE, jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE.toString())
.entity(error)
.build();
}
try {
RealmModel realm = resolveRealm(session);
Theme theme = session.theme().getTheme(Theme.Type.LOGIN);
Locale locale = session.getContext().resolveLocale(null);
FreeMarkerProvider freeMarker = session.getProvider(FreeMarkerProvider.class);
Map<String, Object> attributes = initAttributes(session, realm, theme, locale, responseStatus);
String templateName = "error.ftl";
String content = freeMarker.processTemplate(attributes, templateName, theme);
return Response.status(responseStatus).type(MediaType.TEXT_HTML_UTF_8_TYPE).entity(content).build();
} catch (Throwable t) {
logger.error("Failed to create error page", t);
return Response.serverError().build();
}
}
private static Response.Status getResponseStatus(Throwable throwable) {
if (throwable instanceof WebApplicationException ex) {
return Response.Status.fromStatusCode(ex.getResponse().getStatus());
}
if (throwable instanceof JsonProcessingException || throwable instanceof ModelValidationException) {
return Response.Status.BAD_REQUEST;
}
if (throwable instanceof ModelIllegalStateException) {
return Response.Status.INTERNAL_SERVER_ERROR;
}
if (throwable instanceof ModelDuplicateException) {
return Response.Status.CONFLICT;
}
return Response.Status.INTERNAL_SERVER_ERROR;
}
private static String getErrorCode(Throwable throwable) {
Throwable cause = throwable.getCause();
if (cause instanceof JsonParseException) {
return OAuthErrorException.INVALID_REQUEST;
}
if (throwable instanceof WebApplicationException && throwable.getMessage() != null) {
return throwable.getMessage();
}
return "unknown_error";
}
private static RealmModel resolveRealm(KeycloakSession session) {
String path = session.getContext().getUri().getPath();
Matcher m = realmNamePattern.matcher(path);
String realmName;
if(m.matches()) {
realmName = m.group(1);
} else {
realmName = Config.getAdminRealm();
}
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName);
if (realm == null) {
realm = realmManager.getRealmByName(Config.getAdminRealm());
}
session.getContext().setRealm(realm);
return realm;
}
private static Map<String, Object> initAttributes(KeycloakSession session, RealmModel realm, Theme theme, Locale locale, Response.Status responseStatus) throws IOException {
Map<String, Object> attributes = new HashMap<>();
Properties messagesBundle = theme.getMessages(locale);
attributes.put("statusCode", responseStatus.getStatusCode());
attributes.put("realm", realm);
attributes.put("url", new UrlBean(realm, theme, session.getContext().getUri().getBaseUri(), null));
attributes.put("locale", new LocaleBean(realm, locale, session.getContext().getUri().getRequestUriBuilder(), messagesBundle));
String errorKey = responseStatus == Response.Status.NOT_FOUND ? Messages.PAGE_NOT_FOUND : Messages.INTERNAL_SERVER_ERROR;
String errorMessage = messagesBundle.getProperty(errorKey);
attributes.put("message", new MessageBean(errorMessage, MessageType.ERROR));
// Default fallback in case an error occurs determining the dark mode later on.
attributes.put("darkMode", true);
try {
attributes.put("msg", new MessageFormatterMethod(locale, theme.getMessages(locale)));
} catch (IOException e) {
e.printStackTrace();
}
try {
Properties properties = theme.getProperties();
attributes.put("properties", properties);
attributes.put("darkMode", "true".equals(properties.getProperty("darkMode"))
&& realm.getAttribute("darkMode", true));
} catch (IOException e) {
e.printStackTrace();
}
return attributes;
}
}