Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
Breaking changes are identified as those that might require changes for existing users to their configurations or applications.
In minor or patch releases, {project_name} will only introduce breaking changes to fix bugs.

=== Authorization endpoint JSON errors
// https://github.com/keycloak/keycloak/issues/54134

When an OpenID Connect authorization request with `Accept: application/json` is rejected during client or request validation, the JSON response now contains an OAuth error code in `error` and the human-readable message in `error_description`.
For example, an unknown client returns `{"error":"invalid_client","error_description":"Client not found."}`.
Applications should use `error` rather than matching translated messages. HTTP status codes and HTML error pages retain their existing behavior, and errors sent to a validated redirect URI continue to use that redirect.
Other JSON error pages, including direct-grant authenticator errors, are unchanged.

=== Realm-level default client scopes require `manage-realm`
// https://github.com/keycloak/keycloak/issues/50588

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ public interface LoginFormsProvider extends Provider {

Response createErrorPage(Response.Status status);

/**
* Creates an error page, using the supplied OAuth error code for JSON responses.
* Providers that do not support OAuth error codes retain their existing behavior.
*/
default Response createErrorPage(Response.Status status, String error) {
return createErrorPage(status);
}

Response createWebAuthnErrorPage();

Response createOAuthGrant();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -782,9 +782,19 @@ public Response createIdpLinkEmailPage() {

@Override
public Response createErrorPage(Response.Status status) {
return createErrorPage(status, null);
}

@Override
public Response createErrorPage(Response.Status status, String error) {
if (MediaTypeMatcher.isJsonRequest(session.getContext().getRequestHeaders())) {
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation();
errorRep.setError(formatMessage(getFirstMessage()));
if (error == null) {
errorRep.setError(formatMessage(getFirstMessage()));
} else {
errorRep.setError(error);
errorRep.setErrorDescription(formatMessage(getFirstMessage()));
}

return Response.status(status)
.type(MediaType.APPLICATION_JSON_TYPE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import jakarta.ws.rs.core.Response;

import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.Profile;
import org.keycloak.constants.AdapterConstants;
Expand Down Expand Up @@ -148,7 +149,7 @@ private Response process(final MultivaluedMap<String, String> params) {
event.detail(Details.CLIENT_POLICY_ERROR, cpe.getError());
event.detail(Details.CLIENT_POLICY_ERROR_DETAIL, cpe.getErrorDetail());
event.error(cpe.getError());
throw new ErrorPageException(session, authenticationSession, cpe.getErrorStatus(), cpe.getErrorDetail());
throw ErrorPageException.oauthError(session, authenticationSession, cpe.getErrorStatus(), cpe.getError(), cpe.getErrorDetail());
}
checkClient(clientId);

Expand Down Expand Up @@ -285,25 +286,25 @@ private void checkClient(String clientId) {
if (clientId == null) {
event.detail(Details.REASON, "Missing parameter: " + OIDCLoginProtocol.CLIENT_ID_PARAM);
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
throw ErrorPageException.oauthError(session, authenticationSession, Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, Messages.MISSING_PARAMETER, OIDCLoginProtocol.CLIENT_ID_PARAM);
}

event.client(clientId);

client = realm.getClientByClientId(clientId);
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.CLIENT_NOT_FOUND);
throw ErrorPageException.oauthError(session, authenticationSession, Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_CLIENT, Messages.CLIENT_NOT_FOUND);
}

if (!client.isEnabled()) {
event.error(Errors.CLIENT_DISABLED);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.CLIENT_DISABLED);
throw ErrorPageException.oauthError(session, authenticationSession, Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_CLIENT, Messages.CLIENT_DISABLED);
}

if (client.isBearerOnly()) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorPageException(session, authenticationSession, Response.Status.FORBIDDEN, Messages.BEARER_ONLY);
throw ErrorPageException.oauthError(session, authenticationSession, Response.Status.FORBIDDEN, OAuthErrorException.UNAUTHORIZED_CLIENT, Messages.BEARER_ONLY);
}

String protocol = client.getProtocol();
Expand All @@ -315,7 +316,7 @@ private void checkClient(String clientId) {
String errorMessage = "Wrong client protocol.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CLIENT);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, errorMessage);
throw ErrorPageException.oauthError(session, authenticationSession, Response.Status.BAD_REQUEST, OAuthErrorException.UNAUTHORIZED_CLIENT, errorMessage);
}

session.getContext().setClient(client);
Expand Down Expand Up @@ -368,7 +369,7 @@ private void updateAuthenticationSession() {
logger.errorf("Requested essential acr value list contains values lower than the client minimum. Please doublecheck the client configuration or correct ACR passed in the 'claims' parameter.");
event.detail(Details.REASON, "Invalid requested essential acr value");
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.CLAIMS_PARAM);
throw ErrorPageException.oauthError(session, authenticationSession, Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.CLAIMS_PARAM);
}
authenticationSession.setClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION, "true");
}
Expand All @@ -384,7 +385,7 @@ private void updateAuthenticationSession() {
logger.errorf("Requested essential acr value '%s' is not a number and it is not mapped in the ACR-To-Loa mappings of realm or client. Please doublecheck ACR-to-LOA mapping or correct ACR passed in the 'claims' parameter.", acr);
event.detail(Details.REASON, "Invalid requested essential acr value");
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.CLAIMS_PARAM);
throw ErrorPageException.oauthError(session, authenticationSession, Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.CLAIMS_PARAM);
}
}).min().ifPresent(loa -> authenticationSession.setClientNote(Constants.REQUESTED_LEVEL_OF_AUTHENTICATION, String.valueOf(loa)));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,10 @@ private void checkParamsForPkceNotEnforcedClient(String codeChallengeMethod, Str
}

public void throwAsErrorPageException(AuthenticationSessionModel authenticationSession, AuthorizationCheckException ex) {
throw new ErrorPageException(session, authenticationSession, ex.status, ex.error, ex.errorDescription);
if (Messages.INVALID_PARAMETER.equals(ex.error)) {
throw ErrorPageException.oauthError(session, authenticationSession, ex.status, OAuthErrorException.INVALID_REQUEST, ex.error, ex.errorDescription);
}
throw ErrorPageException.oauthError(session, authenticationSession, ex.status, ex.error, ex.errorDescription != null ? ex.errorDescription : ex.error);
}

public void throwAsCorsErrorResponseException(Cors cors, AuthorizationCheckException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import jakarta.ws.rs.core.Response;

import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.Profile;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.events.Details;
Expand Down Expand Up @@ -114,6 +115,9 @@ public static AuthorizationEndpointRequest parseRequest(EventBuilder event, Keyc
ServicesLogger.LOGGER.invalidRequest(e);
event.detail(Details.REASON, e.getMessage());
event.error(Errors.INVALID_REQUEST);
if (endpointType == EndpointType.OIDC_AUTH_ENDPOINT) {
throw ErrorPageException.oauthError(session, null, Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, Messages.INVALID_REQUEST);
}
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
}
}
Expand All @@ -127,7 +131,7 @@ public static String getClientId(EventBuilder event, KeycloakSession session, Mu
logger.warnf(errorMessage);
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
throw ErrorPageException.oauthError(session, null, Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, Messages.INVALID_REQUEST);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import jakarta.ws.rs.core.Response;

import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.sessions.AuthenticationSessionModel;

Expand All @@ -35,6 +36,16 @@ public ErrorPageException(KeycloakSession session, AuthenticationSessionModel au
super(errorMessage, ErrorPage.error(session, authSession, status, errorMessage, parameters));
}

public static ErrorPageException oauthError(KeycloakSession session, AuthenticationSessionModel authSession,
Response.Status status, String error, String errorMessage, Object... parameters) {
session.getTransactionManager().setRollbackOnly();
Response response = session.getProvider(LoginFormsProvider.class)
.setAuthenticationSession(authSession)
.setError(errorMessage, parameters)
.createErrorPage(status, error);
return new ErrorPageException(response);
}

public ErrorPageException(Response response) {
super((Throwable) null, response);
}
Expand Down
Loading
Loading