From e1069882ddf02971d2d23ffa259b3af256a78b7d Mon Sep 17 00:00:00 2001 From: Krzysztof Walczewski Date: Thu, 15 Feb 2024 18:23:52 +0100 Subject: [PATCH] check consents for scopes from subject token Closes #20702 Signed-off-by: Krzysztof Walczewski --- .../oidc/DefaultTokenExchangeProvider.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java index cf658ef9a814..6d30ba6f1fa4 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/DefaultTokenExchangeProvider.java @@ -38,6 +38,7 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; import org.keycloak.models.ClientSessionContext; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderMapperModel; @@ -278,13 +279,13 @@ protected Response exchangeToIdentityProvider(UserModel targetUser, UserSessionM event.error(Errors.NOT_ALLOWED); throw new CorsErrorResponseException(cors, OAuthErrorException.ACCESS_DENIED, "Client not allowed to exchange", Response.Status.FORBIDDEN); } - Response response = ((ExchangeTokenToIdentityProviderToken)provider).exchangeFromToken(session.getContext().getUri(), event, client, targetUserSession, targetUser, formParams); + Response response = ((ExchangeTokenToIdentityProviderToken) provider).exchangeFromToken(session.getContext().getUri(), event, client, targetUserSession, targetUser, formParams); return cors.builder(Response.fromResponse(response)).build(); } protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel targetUserSession, - AccessToken token, boolean disallowOnHolderOfTokenMismatch) { + AccessToken token, boolean disallowOnHolderOfTokenMismatch) { String requestedTokenType = formParams.getFirst(OAuth2Constants.REQUESTED_TOKEN_TYPE); if (requestedTokenType == null) { requestedTokenType = OAuth2Constants.REFRESH_TOKEN_TYPE; @@ -311,12 +312,6 @@ protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel } } - if (targetClient.isConsentRequired()) { - event.detail(Details.REASON, "audience requires consent"); - event.error(Errors.CONSENT_DENIED); - throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Client requires user consent", Response.Status.BAD_REQUEST); - } - boolean isClientTheAudience = client.equals(targetClient); if (isClientTheAudience) { @@ -365,6 +360,21 @@ protected Response exchangeClientToClient(UserModel targetUser, UserSessionModel scope = Arrays.stream(scope.split(" ")).filter(s -> "openid".equals(s) || (targetClientScopes.contains(Profile.isFeatureEnabled(Profile.Feature.DYNAMIC_SCOPES) ? s.split(":")[0] : s))).collect(Collectors.joining(" ")); } + + if (scope != null) { + // verification of existing users consents for requested scopes of target client + if (!TokenManager.verifyConsentStillAvailable(session, targetUser, targetClient, TokenManager.getRequestedClientScopes(scope, client))) { + event.error(Errors.NOT_ALLOWED); + event.detail(Details.REASON, String.format( + "Missing some consents of [%s] for audience [%s]", + TokenManager.getRequestedClientScopes(scope, client) + .map(ClientScopeModel::getName) + .collect(Collectors.joining(",")), + targetClient.getClientId())); + throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_SCOPE, "Client requires user consent", Response.Status.BAD_REQUEST); + } + } + switch (requestedTokenType) { case OAuth2Constants.ACCESS_TOKEN_TYPE: case OAuth2Constants.REFRESH_TOKEN_TYPE: