Improve details for user error events in OIDC protocol endpoints

Closes #29166

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
This commit is contained in:
Thomas Darimont 2024-05-06 08:32:31 +02:00 committed by GitHub
parent 32d25f43d0
commit ba43a10a6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 235 additions and 91 deletions

View file

@ -229,6 +229,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
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);
}
@ -257,8 +258,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
}
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
String errorMessage = "Wrong client protocol.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CLIENT);
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, "Wrong client protocol.");
throw new ErrorPageException(session, authenticationSession, Response.Status.BAD_REQUEST, errorMessage);
}
session.getContext().setClient(client);
@ -314,6 +317,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
boolean essential = Boolean.parseBoolean(authenticationSession.getClientNote(Constants.FORCE_LEVEL_OF_AUTHENTICATION));
if (essential) {
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);
} else {

View file

@ -139,8 +139,10 @@ public class AuthorizationEndpointChecker {
if (responseType == null) {
ServicesLogger.LOGGER.missingParameter(OAuth2Constants.RESPONSE_TYPE);
String errorMessage = "Missing parameter: response_type";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Missing parameter: response_type");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
event.detail(Details.RESPONSE_TYPE, responseType);
@ -148,6 +150,7 @@ public class AuthorizationEndpointChecker {
try {
this.parsedResponseType = OIDCResponseType.parse(responseType);
} catch (IllegalArgumentException iae) {
event.detail(Details.REASON, iae.getMessage());
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.UNSUPPORTED_RESPONSE_TYPE, null);
}
@ -157,8 +160,10 @@ public class AuthorizationEndpointChecker {
parsedResponseMode = OIDCResponseMode.parse(request.getResponseMode(), parsedResponseType);
} catch (IllegalArgumentException iae) {
ServicesLogger.LOGGER.invalidParameter(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
String errorMessage = "Invalid parameter: " + OIDCLoginProtocol.RESPONSE_MODE_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: response_mode");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
event.detail(Details.RESPONSE_MODE, parsedResponseMode.toString().toLowerCase());
@ -166,8 +171,10 @@ public class AuthorizationEndpointChecker {
// Disallowed by OIDC specs
if (parsedResponseType.isImplicitOrHybridFlow() && parsedResponseMode == OIDCResponseMode.QUERY) {
ServicesLogger.LOGGER.responseModeQueryNotAllowed();
String errorMessage = "Response_mode 'query' not allowed for implicit or hybrid flow";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Response_mode 'query' not allowed for implicit or hybrid flow");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
this.parsedResponseMode = parsedResponseMode;
@ -176,20 +183,26 @@ public class AuthorizationEndpointChecker {
(!StringUtil.isNotBlank(client.getAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ALG)) ||
!StringUtil.isNotBlank(client.getAttribute(OIDCConfigAttributes.AUTHORIZATION_ENCRYPTED_RESPONSE_ENC)))) {
ServicesLogger.LOGGER.responseModeQueryJwtNotAllowed();
String errorMessage = "Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Response_mode 'query.jwt' is allowed only when the authorization response token is encrypted");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
if ((parsedResponseType.hasResponseType(OIDCResponseType.CODE) || parsedResponseType.hasResponseType(OIDCResponseType.NONE)) && !client.isStandardFlowEnabled()) {
ServicesLogger.LOGGER.flowNotAllowed("Standard");
String errorMessage = "Client is not allowed to initiate browser login with given response_type. Standard flow is disabled for the client.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new AuthorizationCheckException(Response.Status.UNAUTHORIZED, OAuthErrorException.UNAUTHORIZED_CLIENT, "Client is not allowed to initiate browser login with given response_type. Standard flow is disabled for the client.");
throw new AuthorizationCheckException(Response.Status.UNAUTHORIZED, OAuthErrorException.UNAUTHORIZED_CLIENT, errorMessage);
}
if (parsedResponseType.isImplicitOrHybridFlow() && !client.isImplicitFlowEnabled()) {
ServicesLogger.LOGGER.flowNotAllowed("Implicit");
String errorMessage = "Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new AuthorizationCheckException(Response.Status.UNAUTHORIZED, OAuthErrorException.UNAUTHORIZED_CLIENT, "Client is not allowed to initiate browser login with given response_type. Implicit flow is disabled for the client.");
throw new AuthorizationCheckException(Response.Status.UNAUTHORIZED, OAuthErrorException.UNAUTHORIZED_CLIENT, errorMessage);
}
}
@ -219,8 +232,10 @@ public class AuthorizationEndpointChecker {
}
if (!validScopes) {
ServicesLogger.LOGGER.invalidParameter(OIDCLoginProtocol.SCOPE_PARAM);
String errorMessage = "Invalid scopes: " + request.getScope();
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_SCOPE, "Invalid scopes: " + request.getScope());
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_SCOPE, errorMessage);
}
}
@ -233,8 +248,10 @@ public class AuthorizationEndpointChecker {
if (parsedResponseType.hasResponseType(OIDCResponseType.ID_TOKEN) && request.getNonce() == null) {
ServicesLogger.LOGGER.missingParameter(OIDCLoginProtocol.NONCE_PARAM);
String errorMessage = "Missing parameter: " + OIDCLoginProtocol.NONCE_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Missing parameter: nonce");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
return;
@ -270,8 +287,10 @@ public class AuthorizationEndpointChecker {
return;
}
ServicesLogger.LOGGER.missingParameter(OIDCLoginProtocol.REQUEST_URI_PARAM);
String errorMessage = "Pushed Authorization Request is only allowed.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Pushed Authorization Request is only allowed.");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
// https://tools.ietf.org/html/rfc7636#section-4
@ -292,34 +311,44 @@ public class AuthorizationEndpointChecker {
// check whether code challenge method is specified
if (codeChallengeMethod == null) {
logger.info("PKCE enforced Client without code challenge method.");
String errorMessage = "Missing parameter: " + OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge_method");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
// check whether specified code challenge method is configured one in advance
if (!codeChallengeMethod.equals(pkceCodeChallengeMethod)) {
logger.info("PKCE enforced Client code challenge method is not configured one.");
logger.info("PKCE enforced Client code challenge method is not matching the configured one.");
String errorMessage = "Invalid parameter: code challenge method is not matching the configured one";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code challenge method is not configured one");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
// check whether code challenge is specified
if (codeChallenge == null) {
logger.info("PKCE supporting Client without code challenge");
String errorMessage = "Missing parameter: " + OIDCLoginProtocol.CODE_CHALLENGE_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
// check whether code challenge is formatted along with the PKCE specification
if (!isValidPkceCodeChallenge(codeChallenge)) {
logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge);
String errorMessage = "Invalid parameter: " + OIDCLoginProtocol.CODE_CHALLENGE_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
}
private void checkParamsForPkceNotEnforcedClient(String codeChallengeMethod, String pkceCodeChallengeMethod, String codeChallenge) throws AuthorizationCheckException {
if (codeChallenge == null && codeChallengeMethod != null) {
logger.info("PKCE supporting Client without code challenge");
String errorMessage = "Missing parameter: " + OIDCLoginProtocol.CODE_CHALLENGE_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
// based on code_challenge value decide whether this client(RP) supports PKCE
@ -333,8 +362,10 @@ public class AuthorizationEndpointChecker {
// plain or S256
if (!codeChallengeMethod.equals(OIDCLoginProtocol.PKCE_METHOD_S256) && !codeChallengeMethod.equals(OIDCLoginProtocol.PKCE_METHOD_PLAIN)) {
logger.infof("PKCE supporting Client with invalid code challenge method not specified in PKCE, codeChallengeMethod = %s", codeChallengeMethod);
String errorMessage = "Invalid parameter: " + OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge_method");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
} else {
// https://tools.ietf.org/html/rfc7636#section-4.3
@ -344,8 +375,10 @@ public class AuthorizationEndpointChecker {
if (!isValidPkceCodeChallenge(codeChallenge)) {
logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge);
String errorMessage = "Invalid parameter: " + OIDCLoginProtocol.CODE_CHALLENGE_PARAM;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge");
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_REQUEST, errorMessage);
}
}

View file

@ -165,17 +165,20 @@ public class LogoutEndpoint {
if (!providerConfig.isLegacyLogoutRedirectUri()) {
if (deprecatedRedirectUri != null) {
event.event(EventType.LOGOUT);
String errorMessage = "Parameter 'redirect_uri' no longer supported.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
logger.warnf("Parameter 'redirect_uri' no longer supported. Please use 'post_logout_redirect_uri' with 'id_token_hint' for this endpoint. Alternatively you can enable backwards compatibility option '%s' of oidc login protocol in the server configuration.",
OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
logger.warnf("%s Please use 'post_logout_redirect_uri' with 'id_token_hint' for this endpoint. Alternatively you can enable backwards compatibility option '%s' of oidc login protocol in the server configuration.",
errorMessage, OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM);
}
if (postLogoutRedirectUri != null && encodedIdToken == null && clientId == null) {
event.event(EventType.LOGOUT);
String errorMessage = "Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
logger.warnf(
"Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.");
logger.warnf(errorMessage);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER,
OIDCLoginProtocol.ID_TOKEN_HINT);
}
@ -199,6 +202,7 @@ public class LogoutEndpoint {
TokenVerifier.createWithoutSignature(idToken).tokenType(Arrays.asList(TokenUtil.TOKEN_TYPE_ID)).verify();
} catch (OAuthErrorException | VerificationException e) {
event.event(EventType.LOGOUT);
event.detail(Details.REASON, e.getMessage());
event.error(Errors.INVALID_TOKEN);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.ID_TOKEN_HINT);
}
@ -216,8 +220,10 @@ public class LogoutEndpoint {
if (!idToken.getIssuedFor().equals(clientId)) {
event.event(EventType.LOGOUT);
event.client(clientId);
String errorMessage = "Parameter client_id is different than the client for which ID Token was issued.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_TOKEN);
logger.warnf("Parameter client_id is different than the client for which ID Token was issued. Parameter client_id: '%s', ID Token issued for: '%s'.", clientId, idToken.getIssuedFor());
logger.warnf("%s Parameter client_id: '%s', ID Token issued for: '%s'.", errorMessage, clientId, idToken.getIssuedFor());
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.ID_TOKEN_HINT);
} else {
confirmationNeeded = false;
@ -359,11 +365,13 @@ public class LogoutEndpoint {
checks.initialVerify();
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.LOGGING_OUT.name(), ClientSessionCode.ActionType.USER) || !checks.isActionRequest() || !formData.containsKey("confirmLogout")) {
AuthenticationSessionModel logoutSession = checks.getAuthenticationSession();
logger.debugf("Failed verification during logout. logoutSessionId=%s, clientId=%s, tabId=%s",
logoutSession != null ? logoutSession.getParentSession().getId() : "unknown", clientId, tabId);
String errorMessage = "Failed verification during logout.";
logger.debugf( "%s logoutSessionId=%s, clientId=%s, tabId=%s",
errorMessage, logoutSession != null ? logoutSession.getParentSession().getId() : "unknown", clientId, tabId);
SystemClientUtil.checkSkipLink(session, logoutSession);
event.detail(Details.REASON, errorMessage);
event.error(Errors.SESSION_EXPIRED);
return ErrorPage.error(session, logoutSession, Response.Status.BAD_REQUEST, Messages.FAILED_LOGOUT);
@ -391,12 +399,14 @@ public class LogoutEndpoint {
SessionCodeChecks checks = new LogoutSessionCodeChecks(realm, session.getContext().getUri(), request, clientConnection, session, event, null, clientId, tabId);
AuthenticationSessionModel logoutSession = checks.initialVerifyAuthSession();
if (logoutSession == null) {
logger.debugf("Failed verification when changing locale logout. clientId=%s, tabId=%s", clientId, tabId);
String errorMessage = "Failed verification when changing locale during logout.";
logger.debugf("%s clientId=%s, tabId=%s", errorMessage, clientId, tabId);
SystemClientUtil.checkSkipLink(session, logoutSession);
AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
event.detail(Details.REASON, errorMessage);
event.error(Errors.LOGOUT_FAILED);
return ErrorPage.error(session, logoutSession, Response.Status.BAD_REQUEST, Messages.FAILED_LOGOUT);
} else {
@ -562,15 +572,19 @@ public class LogoutEndpoint {
String encodedLogoutToken = form.getFirst(OAuth2Constants.LOGOUT_TOKEN);
if (encodedLogoutToken == null) {
String errorMessage = "No logout token";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No logout token",
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, errorMessage,
Response.Status.BAD_REQUEST);
}
LogoutTokenValidationCode validationCode = tokenManager.verifyLogoutToken(session, realm, encodedLogoutToken);
if (!validationCode.equals(LogoutTokenValidationCode.VALIDATION_SUCCESS)) {
String errorMessage = validationCode.getErrorMessage();
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, validationCode.getErrorMessage(),
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, errorMessage,
Response.Status.BAD_REQUEST);
}
@ -594,9 +608,10 @@ public class LogoutEndpoint {
}
if (!backchannelLogoutResponse.getLocalLogoutSucceeded()) {
String errorMessage = "There was an error during the local logout";
event.detail(Details.REASON, errorMessage);
event.error(Errors.LOGOUT_FAILED);
throw new ErrorResponseException(OAuthErrorException.SERVER_ERROR,
"There was an error in the local logout",
throw new ErrorResponseException(OAuthErrorException.SERVER_ERROR, errorMessage,
Response.Status.NOT_IMPLEMENTED);
}

View file

@ -170,6 +170,7 @@ public class UserInfoEndpoint {
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN);
if (tokenForUserInfo.getToken() == null) {
event.detail(Details.REASON, "Missing token");
event.error(Errors.INVALID_TOKEN);
throw error.unauthorized();
}
@ -186,8 +187,10 @@ public class UserInfoEndpoint {
token = verifier.verify().getToken();
if (!TokenUtil.hasScope(token.getScope(), OAuth2Constants.SCOPE_OPENID)) {
String errorMessage = "Missing openid scope";
event.detail(Details.REASON, errorMessage);
event.error(Errors.ACCESS_DENIED);
throw error.insufficientScope("Missing openid scope");
throw error.insufficientScope(errorMessage);
}
clientModel = realm.getClientByClientId(token.getIssuedFor());
@ -205,13 +208,16 @@ public class UserInfoEndpoint {
if (clientModel == null) {
cors.allowAllOrigins();
}
event.detail(Details.REASON, e.getMessage());
event.error(Errors.INVALID_TOKEN);
throw error.invalidToken("Token verification failed");
}
if (!clientModel.getProtocol().equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
String errorMessage = "Wrong client protocol";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CLIENT);
throw error.invalidToken("Wrong client protocol");
throw error.invalidToken(errorMessage);
}
session.getContext().setClient(clientModel);
@ -243,8 +249,10 @@ public class UserInfoEndpoint {
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
if (OIDCAdvancedConfigWrapper.fromClientModel(clientModel).isUseMtlsHokToken()) {
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(token, request, session)) {
String errorMessage = "Client certificate missing, or its thumbprint and one in the refresh token did NOT match";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw error.invalidToken("Client certificate missing, or its thumbprint and one in the refresh token did NOT match");
throw error.invalidToken(errorMessage);
}
}
@ -254,8 +262,10 @@ public class UserInfoEndpoint {
DPoP dPoP = new DPoPUtil.Validator(session).request(request).uriInfo(session.getContext().getUri()).validate();
DPoPUtil.validateBinding(token, dPoP);
} catch (VerificationException ex) {
event.detail("detail", ex.getMessage()).error(Errors.NOT_ALLOWED);
throw error.invalidToken("DPoP proof and token binding verification failed");
String errorMessage = "DPoP proof and token binding verification failed";
event.detail(Details.REASON, errorMessage + ": " + ex.getMessage());
event.error(Errors.NOT_ALLOWED);
throw error.invalidToken(errorMessage);
}
}
}

View file

@ -21,6 +21,7 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.connections.httpclient.HttpClientProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
@ -119,7 +120,9 @@ public class AuthorizationEndpointRequestParserProcessor {
if (clientParam != null && clientParam.size() == 1) {
return clientParam.get(0);
} else {
logger.warnf("Parameter 'client_id' not present or present multiple times in the HTTP request parameters");
String errorMessage = "Parameter 'client_id' not present or present multiple times in the HTTP request parameters";
logger.warnf(errorMessage);
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new ErrorPageException(session, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
}

View file

@ -28,6 +28,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.Profile;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel;
@ -66,8 +67,10 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
String code = formParams.getFirst(OAuth2Constants.CODE);
if (code == null) {
String errorMessage = "Missing parameter: " + OAuth2Constants.CODE;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, errorMessage, Response.Status.BAD_REQUEST);
}
OAuth2CodeParser.ParseResult parseResult = OAuth2CodeParser.parseCode(session, code, realm, event);
@ -127,24 +130,32 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
}
if (redirectUri != null && !redirectUri.equals(redirectUriParam)) {
event.error(Errors.INVALID_CODE);
logger.tracef("Parameter 'redirect_uri' did not match originally saved redirect URI used in initial OIDC request. Saved redirectUri: %s, redirectUri parameter: %s", redirectUri, redirectUriParam);
String errorMessage = "Parameter 'redirect_uri' did not match originally saved redirect URI used in initial OIDC request. Saved redirectUri: %s, redirectUri parameter: %s";
event.detail(Details.REASON, String.format(errorMessage, redirectUri, redirectUriParam));
event.error(Errors.INVALID_REDIRECT_URI);
logger.tracef(errorMessage, redirectUri, redirectUriParam);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Incorrect redirect_uri", Response.Status.BAD_REQUEST);
}
if (!client.getClientId().equals(clientSession.getClient().getClientId())) {
event.error(Errors.INVALID_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Auth error", Response.Status.BAD_REQUEST);
String errorMessage = "Auth error: Found different client_id in clientSession";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CLIENT);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
if (!client.isStandardFlowEnabled()) {
String errorMessage = "Client not allowed to exchange code";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Client not allowed to exchange code", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
String errorMessage = "Session not active";
event.detail(Details.REASON, errorMessage);
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Session not active", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
// https://tools.ietf.org/html/rfc7636#section-4.6
@ -182,8 +193,10 @@ public class AuthorizationCodeGrantType extends OAuth2GrantTypeBase {
String scopeParam = codeData.getScope();
Supplier<Stream<ClientScopeModel>> clientScopesSupplier = () -> TokenManager.getRequestedClientScopes(scopeParam, client);
if (!TokenManager.verifyConsentStillAvailable(session, user, client, clientScopesSupplier.get())) {
String errorMessage = "Client no longer has requested consent from user";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, "Client no longer has requested consent from user", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, errorMessage, Response.Status.BAD_REQUEST);
}
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, scopeParam, session);

View file

@ -154,6 +154,7 @@ public class ClientCredentialsGrantType extends OAuth2GrantTypeBase {
try {
session.clientPolicy().triggerOnEvent(new ServiceAccountTokenResponseContext(formParams, clientSessionCtx.getClientSession(), responseBuilder));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, cpe.getErrorDetail());
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
}

View file

@ -24,7 +24,12 @@ import jakarta.ws.rs.core.Response;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.logging.Logger;
@ -34,12 +39,14 @@ import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.VerificationException;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.http.HttpRequest;
import org.keycloak.http.HttpResponse;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -128,6 +135,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
try {
session.clientPolicy().triggerOnEvent(clientPolicyContextGenerator.apply(responseBuilder));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, cpe.getErrorDetail());
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}
@ -165,9 +173,11 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
responseBuilder.getRefreshToken().setConfirmation(confirmation);
}
} else {
String errorMessage = "Client Certification missing for MTLS HoK Token Binding";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
"Client Certification missing for MTLS HoK Token Binding", Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
}
}
@ -225,6 +235,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
dPoP = new DPoPUtil.Validator(session).request(request).uriInfo(session.getContext().getUri()).validate();
session.setAttribute(DPoPUtil.DPOP_SESSION_ATTRIBUTE, dPoP);
} catch (VerificationException ex) {
event.detail(Details.REASON, ex.getMessage());
event.error(Errors.INVALID_DPOP_PROOF);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_DPOP_PROOF, ex.getMessage(), Response.Status.BAD_REQUEST);
}
@ -243,9 +254,10 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
}
if (!validScopes) {
String errorMessage = "Invalid scopes: " + scope;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, "Invalid scopes: " + scope,
Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, errorMessage, Response.Status.BAD_REQUEST);
}
return scope;

View file

@ -136,8 +136,10 @@ public class PermissionGrantType extends OAuth2GrantTypeBase {
if (rpt != null) {
AccessToken accessToken = session.tokens().decode(rpt, AccessToken.class);
if (accessToken == null) {
String errorMessage = "RPT signature is invalid";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, "invalid_rpt", "RPT signature is invalid", Response.Status.FORBIDDEN);
throw new CorsErrorResponseException(cors, "invalid_rpt", errorMessage, Response.Status.FORBIDDEN);
}
authorizationRequest.setRpt(accessToken);

View file

@ -24,6 +24,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.Profile;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel;
@ -52,9 +53,11 @@ public class PreAuthorizedCodeGrantType extends OAuth2GrantTypeBase {
String code = formParams.getFirst(OAuth2Constants.CODE);
if (code == null) {
String errorMessage = "Missing parameter: " + OAuth2Constants.CODE;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
"Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
OAuth2CodeParser.ParseResult result = OAuth2CodeParser.parseCode(session, code, realm, event);
if (result.isIllegalCode()) {

View file

@ -66,18 +66,23 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa
event.detail(Details.AUTH_METHOD, "oauth_credentials");
if (!client.isDirectAccessGrantsEnabled()) {
String errorMessage = "Client not allowed for direct access grants";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.UNAUTHORIZED_CLIENT, "Client not allowed for direct access grants", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.UNAUTHORIZED_CLIENT, errorMessage, Response.Status.BAD_REQUEST);
}
if (client.isConsentRequired()) {
String errorMessage = "Client requires user consent";
event.detail(Details.REASON, errorMessage);
event.error(Errors.CONSENT_DENIED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, errorMessage, Response.Status.BAD_REQUEST);
}
try {
session.clientPolicy().triggerOnEvent(new ResourceOwnerPasswordCredentialsContext(formParams));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, cpe.getErrorDetail());
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}
@ -116,8 +121,10 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa
if (user.getRequiredActionsStream().count() > 0 || authSession.getRequiredActions().size() > 0) {
// Remove authentication session as "Resource Owner Password Credentials Grant" is single-request scoped authentication
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
String errorMessage = "Account is not fully set up";
event.detail(Details.REASON, errorMessage);
event.error(Errors.RESOLVE_REQUIRED_ACTIONS);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Account is not fully set up", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
@ -144,6 +151,7 @@ public class ResourceOwnerPasswordCredentialsGrantType extends OAuth2GrantTypeBa
try {
session.clientPolicy().triggerOnEvent(new ResourceOwnerPasswordCredentialsResponseContext(formParams, clientSessionCtx, responseBuilder));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, cpe.getErrorDetail());
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
}

View file

@ -113,16 +113,20 @@ public class CibaGrantType extends OAuth2GrantTypeBase {
setContext(context);
if (!realm.getCibaPolicy().isOIDCCIBAGrantEnabled(client)) {
String errorMessage = "Client not allowed OIDC CIBA Grant";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT,
"Client not allowed OIDC CIBA Grant", Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
String jwe = formParams.getFirst(AUTH_REQ_ID);
if (jwe == null) {
String errorMessage = "Missing parameter: " + AUTH_REQ_ID;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + AUTH_REQ_ID, Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, errorMessage, Response.Status.BAD_REQUEST);
}
logger.tracev("CIBA Grant :: authReqId = {0}", jwe);
@ -185,8 +189,10 @@ public class CibaGrantType extends OAuth2GrantTypeBase {
if (!TokenManager
.verifyConsentStillAvailable(session,
user, client, TokenManager.getRequestedClientScopes(scopeParam, client))) {
String errorMessage = "Client no longer has requested consent from user";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, "Client no longer has requested consent from user", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, errorMessage, Response.Status.BAD_REQUEST);
}
ClientSessionContext clientSessionCtx = DefaultClientSessionContext
@ -236,8 +242,10 @@ public class CibaGrantType extends OAuth2GrantTypeBase {
authSession.setAuthenticatedUser(user);
if (user.getRequiredActionsStream().count() > 0) {
String errorMessage = "Account is not fully set up";
event.detail(Details.REASON, errorMessage);
event.error(Errors.RESOLVE_REQUIRED_ACTIONS);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Account is not fully set up", Response.Status.BAD_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
AuthenticationManager.setClientScopesInSession(authSession);

View file

@ -157,8 +157,10 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
if (deviceCodeModel != null) {
if (!client.getClientId().equals(deviceCodeModel.getClientId())) {
String errorMessage = "unauthorized client";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_OAUTH2_DEVICE_CODE);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "unauthorized client",
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, errorMessage,
Response.Status.BAD_REQUEST);
}
}
@ -210,16 +212,20 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
setContext(context);
if (!realm.getOAuth2DeviceConfig().isOAuth2DeviceAuthorizationGrantEnabled(client)) {
String errorMessage = "Client not allowed OAuth 2.0 Device Authorization Grant";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT,
"Client not allowed OAuth 2.0 Device Authorization Grant", Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
String deviceCode = formParams.getFirst(OAuth2Constants.DEVICE_CODE);
if (deviceCode == null) {
String errorMessage = "Missing parameter: " + OAuth2Constants.DEVICE_CODE;
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_OAUTH2_DEVICE_CODE);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
"Missing parameter: " + OAuth2Constants.DEVICE_CODE, Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
OAuth2DeviceCodeModel deviceCodeModel = getDeviceByDeviceCode(session, realm, client, event, deviceCode);
@ -242,9 +248,11 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
}
if (deviceCodeModel.isDenied()) {
String errorMessage = "The end user denied the authorization request";
event.detail(Details.REASON, errorMessage);
event.error(Errors.ACCESS_DENIED);
throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED,
"The end user denied the authorization request", Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
if (deviceCodeModel.isPending()) {
@ -309,14 +317,17 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
}
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
String errorMessage = "Session not active";
event.detail(Details.REASON, errorMessage);
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Session not active",
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage,
Response.Status.BAD_REQUEST);
}
try {
session.clientPolicy().triggerOnEvent(new DeviceTokenRequestContext(deviceCodeModel, formParams));
} catch (ClientPolicyException cpe) {
event.detail(Details.REASON, cpe.getErrorDetail());
event.error(cpe.getError());
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(),
Response.Status.BAD_REQUEST);
@ -326,9 +337,11 @@ public class DeviceGrantType extends OAuth2GrantTypeBase {
// (but in device_code-to-token request, it could just theoretically happen that they are not available)
String scopeParam = deviceCodeModel.getScope();
if (!TokenManager.verifyConsentStillAvailable(session, user, client, TokenManager.getRequestedClientScopes(scopeParam, client))) {
String errorMessage = "Client no longer has requested consent from user";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE,
"Client no longer has requested consent from user", Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession,

View file

@ -115,6 +115,7 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
httpRequest.getDecodedFormParameters(), AuthorizationEndpointRequestParserProcessor.EndpointType.OAUTH2_DEVICE_ENDPOINT);
if (request.getInvalidRequestMessage() != null) {
event.detail(Details.REASON, request.getInvalidRequestMessage());
event.error(Errors.INVALID_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT,
request.getInvalidRequestMessage(), Response.Status.BAD_REQUEST);
@ -128,9 +129,11 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
CacheControlUtil.noBackButtonCacheControlHeader(session);
if (!realm.getOAuth2DeviceConfig().isOAuth2DeviceAuthorizationGrantEnabled(client)) {
String errorMessage = "Client not allowed for OAuth 2.0 Device Authorization Grant";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT,
"Client not allowed for OAuth 2.0 Device Authorization Grant", Response.Status.BAD_REQUEST);
errorMessage, Response.Status.BAD_REQUEST);
}
// https://tools.ietf.org/html/rfc7636#section-4
@ -314,8 +317,8 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
* @return Verification page response with error message
*/
private Response invalidUserCodeResponse(String errorMessage, String reason) {
event.error(Errors.INVALID_OAUTH2_USER_CODE);
event.detail(Details.REASON, reason);
event.error(Errors.INVALID_OAUTH2_USER_CODE);
logger.debugf("invalid user code: %s", reason);
return createVerificationPage(errorMessage);
}
@ -394,13 +397,17 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
}
if (!realm.getOAuth2DeviceConfig().isOAuth2DeviceAuthorizationGrantEnabled(client)) {
String errorMessage = "Client is not allowed to initiate OAuth 2.0 Device Authorization Grant. The flow is disabled for the client.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(Errors.UNAUTHORIZED_CLIENT, "Client is not allowed to initiate OAuth 2.0 Device Authorization Grant. The flow is disabled for the client.", Response.Status.BAD_REQUEST);
throw new ErrorResponseException(Errors.UNAUTHORIZED_CLIENT, errorMessage, Response.Status.BAD_REQUEST);
}
if (client.isBearerOnly()) {
String errorMessage = "Bearer-only applications are not allowed to initiate browser login.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(Errors.UNAUTHORIZED_CLIENT, "Bearer-only applications are not allowed to initiate browser login.", Response.Status.FORBIDDEN);
throw new ErrorResponseException(Errors.UNAUTHORIZED_CLIENT, errorMessage, Response.Status.FORBIDDEN);
}
String protocol = client.getProtocol();
@ -410,8 +417,10 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
protocol = OIDCLoginProtocol.LOGIN_PROTOCOL;
}
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
String errorMessage = "Wrong client protocol.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException(Errors.UNAUTHORIZED_CLIENT, "Wrong client protocol." , Response.Status.BAD_REQUEST);
throw new ErrorResponseException(Errors.UNAUTHORIZED_CLIENT, errorMessage, Response.Status.BAD_REQUEST);
}
session.getContext().setClient(client);

View file

@ -5,6 +5,7 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.SecretGenerator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.CorsErrorResponseException;
@ -70,24 +71,27 @@ public class PkceUtils {
public static void checkParamsForPkceEnforcedClient(String codeVerifier, String codeChallenge, String codeChallengeMethod, String authUserId, String authUsername, EventBuilder event, Cors cors) {
// check whether code verifier is specified
if (codeVerifier == null) {
logger.warnf("PKCE code verifier not specified, authUserId = %s, authUsername = %s", authUserId, authUsername);
String errorMessage = "PKCE code verifier not specified";
event.detail(Details.REASON, errorMessage);
event.error(Errors.CODE_VERIFIER_MISSING);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE code verifier not specified", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
verifyCodeVerifier(codeVerifier, codeChallenge, codeChallengeMethod, authUserId, authUsername, event, cors);
}
public static void checkParamsForPkceNotEnforcedClient(String codeVerifier, String codeChallenge, String codeChallengeMethod, String authUserId, String authUsername, EventBuilder event, Cors cors) {
if (codeChallenge != null && codeVerifier == null) {
logger.warnf("PKCE code verifier not specified, authUserId = %s, authUsername = %s", authUserId, authUsername);
String errorMessage = "PKCE code verifier not specified";
event.detail(Details.REASON, errorMessage);
event.error(Errors.CODE_VERIFIER_MISSING);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE code verifier not specified", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
if (codeChallenge == null && codeVerifier != null) {
logger.warnf("PKCE code verifier specified but challenge not present in authorization, authUserId = %s, authUsername = %s", authUserId, authUsername);
String errorMessage = "PKCE code verifier specified but challenge not present in authorization";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_CODE_VERIFIER);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE code verifier specified but challenge not present in authorization", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
if (codeChallenge != null) {
@ -99,9 +103,11 @@ public class PkceUtils {
// check whether code verifier is formatted along with the PKCE specification
if (!isValidPkceCodeVerifier(codeVerifier)) {
logger.infof("PKCE invalid code verifier");
String errorReason = "Invalid code verifier";
String errorMessage = "PKCE verification failed: " + errorReason;
event.detail(Details.REASON, errorReason);
event.error(Errors.INVALID_CODE_VERIFIER);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE invalid code verifier", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
logger.debugf("PKCE supporting Client, codeVerifier = %s", codeVerifier);
@ -117,14 +123,18 @@ public class PkceUtils {
codeVerifierEncoded = codeVerifier;
}
} catch (Exception nae) {
logger.infof("PKCE code verification failed, not supported algorithm specified");
String errorReason = "Unsupported algorithm specified";
String errorMessage = "PKCE verification failed: " + errorReason;
event.detail(Details.REASON, errorReason);
event.error(Errors.PKCE_VERIFICATION_FAILED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE code verification failed, not supported algorithm specified", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
}
if (!codeChallenge.equals(codeVerifierEncoded)) {
logger.warnf("PKCE verification failed. authUserId = %s, authUsername = %s", authUserId, authUsername);
String errorReason = "Code mismatch";
String errorMessage = "PKCE verification failed: " + errorReason;
event.detail(Details.REASON, errorReason);
event.error(Errors.PKCE_VERIFICATION_FAILED);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "PKCE verification failed", Response.Status.BAD_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, errorMessage, Response.Status.BAD_REQUEST);
} else {
logger.debugf("PKCE verification success. codeVerifierEncoded = %s, codeChallenge = %s", codeVerifierEncoded, codeChallenge);
}
@ -132,11 +142,11 @@ public class PkceUtils {
private static boolean isValidPkceCodeVerifier(String codeVerifier) {
if (codeVerifier.length() < OIDCLoginProtocol.PKCE_CODE_VERIFIER_MIN_LENGTH) {
logger.infof(" Error: PKCE codeVerifier length under lower limit , codeVerifier = %s", codeVerifier);
logger.debugf(" Error: PKCE codeVerifier length under lower limit , codeVerifier = %s", codeVerifier);
return false;
}
if (codeVerifier.length() > OIDCLoginProtocol.PKCE_CODE_VERIFIER_MAX_LENGTH) {
logger.infof(" Error: PKCE codeVerifier length over upper limit , codeVerifier = %s", codeVerifier);
logger.debugf(" Error: PKCE codeVerifier length over upper limit , codeVerifier = %s", codeVerifier);
return false;
}
Matcher m = VALID_CODE_VERIFIER_PATTERN.matcher(codeVerifier);

View file

@ -144,7 +144,7 @@ public class PKCEEnforcerExecutor implements ClientPolicyExecutorProvider<PKCEEn
// check whether specified code challenge method is configured one in advance
if (pkceCodeChallengeMethod != null && !codeChallengeMethod.equals(pkceCodeChallengeMethod)) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code challenge method is not configured one");
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code challenge method is not matching the configured one");
}
// check whether code challenge is specified

View file

@ -596,7 +596,7 @@ public class FAPI1Test extends AbstractFAPITest {
oauth.codeChallenge("234567890_234567890123");
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN);
oauth.openLoginForm();
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Invalid parameter: code challenge method is not configured one");
assertRedirectedToClientWithError(OAuthErrorException.INVALID_REQUEST,false, "Invalid parameter: code challenge method is not matching the configured one");
}
// Assumption is that clientId is already set in "oauth" client when this method is called. Also assumption is that PKCE parameters are properly set (in case PKCE required for the client)

View file

@ -342,7 +342,7 @@ public class AccessTokenTest extends AbstractKeycloakTest {
assertEquals("invalid_grant", response.getError());
assertEquals("Incorrect redirect_uri", response.getErrorDescription());
events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_code")
events.expectCodeToToken(codeId, loginEvent.getSessionId()).error(Errors.INVALID_REDIRECT_URI)
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_TYPE)

View file

@ -448,7 +448,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
Assert.assertEquals(400, tokenResponse.getStatusCode());
Assert.assertEquals("invalid_grant", tokenResponse.getError());
Assert.assertEquals("PKCE verification failed", tokenResponse.getErrorDescription());
Assert.assertEquals("PKCE verification failed: Code mismatch", tokenResponse.getErrorDescription());
}

View file

@ -148,7 +148,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
assertEquals("PKCE verification failed", response.getErrorDescription());
assertEquals("PKCE verification failed: Code mismatch", response.getErrorDescription());
events.expectCodeToToken(codeId, sessionId).error(Errors.PKCE_VERIFICATION_FAILED).clearDetails().assertEvent();
}
@ -195,7 +195,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
assertEquals("PKCE verification failed", response.getErrorDescription());
assertEquals("PKCE verification failed: Code mismatch", response.getErrorDescription());
events.expectCodeToToken(codeId, sessionId).error(Errors.PKCE_VERIFICATION_FAILED).clearDetails().assertEvent();
}
@ -298,7 +298,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
assertEquals("PKCE invalid code verifier", response.getErrorDescription());
assertEquals("PKCE verification failed: Invalid code verifier", response.getErrorDescription());
events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent();
}
@ -326,7 +326,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
assertEquals("PKCE invalid code verifier", response.getErrorDescription());
assertEquals("PKCE verification failed: Invalid code verifier", response.getErrorDescription());
events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent();
}
@ -401,7 +401,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
assertEquals(400, response.getStatusCode());
assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
assertEquals("PKCE invalid code verifier", response.getErrorDescription());
assertEquals("PKCE verification failed: Invalid code verifier", response.getErrorDescription());
events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent();
}
@ -619,7 +619,7 @@ public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
Assert.assertTrue(errorResponse.isRedirected());
Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST);
Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code challenge method is not configured one");
Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code challenge method is not matching the configured one");
events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent();
} finally {