diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 74441c97f2..da79e31c95 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -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 { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java index 5732771e79..999adbcb65 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpointChecker.java @@ -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); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java index 88b5d06dde..6323205491 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -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); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index 179ec0e47d..332eb57605 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -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); } } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java index 868a16e58e..58ff5a6a68 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequestParserProcessor.java @@ -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); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java index df871a687d..949cbee541 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/AuthorizationCodeGrantType.java @@ -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> 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); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java index bbd1e67ca7..640932cc79 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ClientCredentialsGrantType.java @@ -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); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java index a1c9fd8ee3..5fe0fe0378 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/OAuth2GrantTypeBase.java @@ -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; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantType.java index 3f3742b5d1..667e2d9c26 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantType.java @@ -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); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java index d93c3f6824..2a0f510f06 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/PreAuthorizedCodeGrantType.java @@ -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()) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java index aca671b26d..81b36bf09a 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ResourceOwnerPasswordCredentialsGrantType.java @@ -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()); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java index 76cd724e38..c1ea3850b5 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java @@ -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); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java index 4105dc2de6..8b027fc405 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java @@ -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, diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java index 9afce96ac9..0f21197ce0 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java @@ -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); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/PkceUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/PkceUtils.java index 0ba06945dd..d51d25f84a 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/PkceUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/PkceUtils.java @@ -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); diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/PKCEEnforcerExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/PKCEEnforcerExecutor.java index 28390550bb..76f914ff28 100644 --- a/services/src/main/java/org/keycloak/services/clientpolicy/executor/PKCEEnforcerExecutor.java +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/PKCEEnforcerExecutor.java @@ -144,7 +144,7 @@ public class PKCEEnforcerExecutor implements ClientPolicyExecutorProvider