From f9d06d56fd5dbb44a4325687d99f3e2af5b2f63e Mon Sep 17 00:00:00 2001 From: vimukthiRajapaksha Date: Tue, 2 Jul 2024 16:18:54 +0530 Subject: [PATCH] accept and persist - Accept 'authorization_details' field in the authorization request. - Persist code and consent authorization details in the database. - Add support for oauth.rar and oauth.rar.common modules. - Read custom implementations of AuthorizationDetailsProvider from SPI. - Display rich authorization details in the consent UI. --- .../pom.xml | 5 + .../endpoint/authz/OAuth2AuthzEndpoint.java | 101 +++++- .../oauth/endpoint/util/EndpointUtil.java | 8 + .../authz/OAuth2AuthzEndpointTest.java | 10 + .../pom.xml | 60 ++++ .../common/dao/AuthorizationDetailsDAO.java | 69 ++++ .../dao/AuthorizationDetailsDAOImpl.java | 189 +++++++++++ .../oauth2/rar/common/dao/SQLQueries.java | 68 ++++ .../dto/AuthorizationDetailsCodeDTO.java | 22 ++ .../dto/AuthorizationDetailsConsentDTO.java | 38 +++ .../common/dto/AuthorizationDetailsDTO.java | 53 ++++ .../rar/common/model/AuthorizationDetail.java | 298 ++++++++++++++++++ .../common/model/AuthorizationDetails.java | 126 ++++++++ .../util/AuthorizationDetailsCommonUtils.java | 149 +++++++++ .../util/AuthorizationDetailsConstants.java | 37 +++ .../pom.xml | 108 +++++++ .../rar/AuthorizationDetailsService.java | 278 ++++++++++++++++ .../rar/AuthorizationDetailsValidator.java | 171 ++++++++++ .../core/AuthorizationDetailsProvider.java | 108 +++++++ .../AuthorizationDetailsProviderFactory.java | 133 ++++++++ ...thorizationDetailsProcessingException.java | 30 ++ .../RARAccessTokenResponseHandler.java | 61 ++++ .../AuthorizationDetailsDataHolder.java | 94 ++++++ .../AuthorizationDetailsServiceComponent.java | 69 ++++ .../model/AuthorizationDetailsContext.java | 98 ++++++ .../oauth2/rar/model/ValidationResult.java | 134 ++++++++ .../rar/util/AuthorizationDetailsUtils.java | 56 ++++ .../org.wso2.carbon.identity.oauth/pom.xml | 5 + ...tityOAuth2AuthorizationDetailsService.java | 134 ++++++++ .../authz/AuthorizationHandlerManager.java | 2 +- .../authz/OAuthAuthzReqMessageContext.java | 25 ++ .../handlers/AbstractResponseTypeHandler.java | 3 + .../handlers/CodeResponseTypeHandler.java | 2 + .../dao/OAuthTokenPersistenceFactory.java | 17 + .../oauth2/model/OAuth2Parameters.java | 25 ++ pom.xml | 18 ++ 36 files changed, 2802 insertions(+), 2 deletions(-) create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/pom.xml create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAO.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAOImpl.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/SQLQueries.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsCodeDTO.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsConsentDTO.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsDTO.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetail.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetails.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsCommonUtils.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsConstants.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/pom.xml create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsService.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsValidator.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProvider.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProviderFactory.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/exception/AuthorizationDetailsProcessingException.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/handler/RARAccessTokenResponseHandler.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsDataHolder.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsServiceComponent.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetailsContext.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/ValidationResult.java create mode 100644 components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsUtils.java create mode 100644 components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/IdentityOAuth2AuthorizationDetailsService.java diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml b/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml index 82b7cdb9737..564cacdef7b 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml +++ b/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml @@ -176,6 +176,11 @@ jackson-core provided + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar + provided + diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpoint.java b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpoint.java index af69213459b..47f2f21f5b2 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpoint.java +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpoint.java @@ -119,6 +119,7 @@ import org.wso2.carbon.identity.oauth2.IdentityOAuth2UnauthorizedScopeException; import org.wso2.carbon.identity.oauth2.OAuth2Service; import org.wso2.carbon.identity.oauth2.RequestObjectException; +import org.wso2.carbon.identity.oauth2.authz.AuthorizationHandlerManager; import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; import org.wso2.carbon.identity.oauth2.bean.OAuthClientAuthnContext; import org.wso2.carbon.identity.oauth2.device.api.DeviceAuthService; @@ -134,6 +135,12 @@ import org.wso2.carbon.identity.oauth2.model.FederatedTokenDO; import org.wso2.carbon.identity.oauth2.model.HttpRequestHeaderHandler; import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsValidator; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; import org.wso2.carbon.identity.oauth2.responsemode.provider.AuthorizationResponseDTO; import org.wso2.carbon.identity.oauth2.responsemode.provider.ResponseModeProvider; import org.wso2.carbon.identity.oauth2.scopeservice.ScopeMetadataService; @@ -280,6 +287,9 @@ public class OAuth2AuthzEndpoint { private static ScopeMetadataService scopeMetadataService; private static DeviceAuthService deviceAuthService; + + private static AuthorizationDetailsService authorizationDetailsService; + private static final String AUTH_SERVICE_RESPONSE = "authServiceResponse"; private static final String IS_API_BASED_AUTH_HANDLED = "isApiBasedAuthHandled"; private static final ApiAuthnHandler API_AUTHN_HANDLER = new ApiAuthnHandler(); @@ -304,6 +314,16 @@ public static void setScopeMetadataService(ScopeMetadataService scopeMetadataSer OAuth2AuthzEndpoint.scopeMetadataService = scopeMetadataService; } + public static AuthorizationDetailsService getAuthorizationDetailsService() { + + return authorizationDetailsService; + } + + public static void setAuthorizationDetailsService(AuthorizationDetailsService authorizationDetailsService) { + + OAuth2AuthzEndpoint.authorizationDetailsService = authorizationDetailsService; + } + private static Class oAuthAuthzRequestClass; @GET @@ -1697,10 +1717,18 @@ private void storeUserConsent(OAuthMessage oAuthMessage, String consent) throws if (approvedAlways) { OpenIDConnectUserRPStore.getInstance().putUserRPToStore(loggedInUser, applicationName, true, clientId); + final AuthorizationDetails userConsentedAuthorizationDetails = + authorizationDetailsService.getUserConsentedAuthorizationDetails( + oAuthMessage.getRequest().getParameterMap(), oauth2Params); + if (hasPromptContainsConsent(oauth2Params)) { EndpointUtil.storeOAuthScopeConsent(loggedInUser, oauth2Params, true); + authorizationDetailsService.storeOrReplaceUserConsentedAuthorizationDetails(loggedInUser, + clientId, oauth2Params, userConsentedAuthorizationDetails); } else { EndpointUtil.storeOAuthScopeConsent(loggedInUser, oauth2Params, false); + authorizationDetailsService.storeUserConsentedAuthorizationDetails(loggedInUser, + clientId, oauth2Params, userConsentedAuthorizationDetails); } } } @@ -2577,6 +2605,12 @@ private String populateOauthParameters(OAuth2Parameters params, OAuthMessage oAu params.setEssentialClaims(oauthRequest.getParam(CLAIMS)); } + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(oauthRequest)) { + final String authorizationDetailsJson = oauthRequest + .getParam(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS); + params.setAuthorizationDetails(new AuthorizationDetails(authorizationDetailsJson)); + } + handleMaxAgeParameter(oauthRequest, params); Object isMtls = oAuthMessage.getRequest().getAttribute(OAuthConstants.IS_MTLS_REQUEST); @@ -2980,6 +3014,22 @@ private String doUserAuthorization(OAuthMessage oAuthMessage, String sessionData return handleAuthorizationFailureBeforeConsent(oAuthMessage, oauth2Params, authorizeRespDTO); } + try { + validateAuthorizationDetailsBeforeConsent(oAuthMessage, oauth2Params, authzReqDTO); + } catch (AuthorizationDetailsProcessingException e) { + log.debug("Error occurred while validating authorization details. Caused by, ", e); + + authorizationResponseDTO.setError(HttpServletResponse.SC_FOUND, + AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_MSG, + AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_CODE); + + OAuth2AuthorizeRespDTO oAuth2AuthorizeRespDTO = new OAuth2AuthorizeRespDTO(); + oAuth2AuthorizeRespDTO.setErrorMsg(AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_MSG); + oAuth2AuthorizeRespDTO.setErrorCode(AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_CODE); + oAuth2AuthorizeRespDTO.setCallbackURI(authzReqDTO.getCallbackUrl()); + return handleAuthorizationFailureBeforeConsent(oAuthMessage, oauth2Params, oAuth2AuthorizeRespDTO); + } + boolean hasUserApproved = isUserAlreadyApproved(oauth2Params, authenticatedUser); if (hasPromptContainsConsent(oauth2Params)) { @@ -3673,7 +3723,8 @@ private boolean isUserAlreadyApproved(OAuth2Parameters oauth2Params, Authenticat throws OAuthSystemException { try { - return EndpointUtil.isUserAlreadyConsentedForOAuthScopes(user, oauth2Params); + return EndpointUtil.isUserAlreadyConsentedForOAuthScopes(user, oauth2Params) && + authorizationDetailsService.isUserAlreadyConsentedForAuthorizationDetails(user, oauth2Params); } catch (IdentityOAuth2ScopeException | IdentityOAuthAdminException e) { throw new OAuthSystemException("Error occurred while checking user has already approved the consent " + "required OAuth scopes.", e); @@ -4746,4 +4797,52 @@ private Response handleUnsupportedGrantForApiBasedAuth() { new AuthServiceClientException(AuthServiceConstants.ErrorMessage.ERROR_INVALID_AUTH_REQUEST.code(), "App native authentication is only supported with code response type."), log); } + + /** + * Validates the authorization details in the provided OAuth message before user consent. + * + *

This method checks if the request is a rich authorization request. If it is, it + * retrieves and validates the authorization details, updating the parameters and context + * accordingly. If any validation errors occur, it logs the issue and throws an appropriate + * exception.

+ * + * @param oAuthMessage The {@link OAuthMessage} containing the authorization request details. + * @param oAuth2Parameters The {@link OAuth2Parameters} object holding the parameters of the OAuth request. + * @param oAuth2AuthorizeReqDTO The {@link OAuth2AuthorizeReqDTO} object containing the authorization request. + * @throws OAuthSystemException If there is an error during the validation process. + * @throws AuthorizationDetailsProcessingException If there is an error processing the authorization details. + */ + private void validateAuthorizationDetailsBeforeConsent(final OAuthMessage oAuthMessage, + final OAuth2Parameters oAuth2Parameters, + final OAuth2AuthorizeReqDTO oAuth2AuthorizeReqDTO) + throws OAuthSystemException, AuthorizationDetailsProcessingException { + + if (!AuthorizationDetailsUtils.isRichAuthorizationRequest(oAuth2Parameters)) { + log.debug("Authorization request is not a rich authorization request. Skipping validation."); + return; + } + + try { + final OAuthAppDO oAuthAppDO = AuthorizationHandlerManager.getInstance() + .getAppInformation(oAuth2AuthorizeReqDTO); + // Validate the authorization details + final AuthorizationDetails validatedAuthorizationDetails = new AuthorizationDetailsValidator() + .getValidatedAuthorizationDetails(oAuth2Parameters, oAuthAppDO, oAuth2AuthorizeReqDTO.getUser()); + + if (log.isDebugEnabled()) { + log.debug("Authorization details validated successfully for user: " + + oAuth2AuthorizeReqDTO.getUser().getLoggableMaskedUserId()); + } + // update oAuth2Parameters with validated authorization details + oAuth2Parameters.setAuthorizationDetails(validatedAuthorizationDetails); + + // Update the authorization message context with validated authorization details + OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext = + oAuthMessage.getSessionDataCacheEntry().getAuthzReqMsgCtx(); + oAuthAuthzReqMessageContext.setAuthorizationDetails(validatedAuthorizationDetails); + } catch (IdentityOAuth2Exception | InvalidOAuthClientException e) { + log.error("Error occurred while validating authorization details. Caused by, ", e); + throw new OAuthSystemException("Error occurred while validating requested authorization details", e); + } + } } diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtil.java b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtil.java index 616870d296d..4129b752b78 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtil.java +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtil.java @@ -103,6 +103,8 @@ import org.wso2.carbon.identity.oauth2.model.CarbonOAuthAuthzRequest; import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; import org.wso2.carbon.identity.oauth2.model.OAuth2ScopeConsentResponse; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; import org.wso2.carbon.identity.oauth2.scopeservice.OAuth2Resource; import org.wso2.carbon.identity.oauth2.scopeservice.ScopeMetadataService; import org.wso2.carbon.identity.oauth2.util.AuthzUtil; @@ -868,6 +870,12 @@ public static String getUserConsentURL(OAuth2Parameters params, String loggedInU (consentRequiredScopes, UTF_8) + "&" + OAuthConstants.SESSION_DATA_KEY_CONSENT + "=" + URLEncoder.encode(sessionDataKeyConsent, UTF_8) + "&" + "&spQueryParams=" + queryString; + // Append authorization details to consent page url + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(params)) { + consentPageUrl = consentPageUrl + "&" + AuthorizationDetailsConstants.AUTHORIZATION_DETAILS + "=" + + URLEncoder.encode(params.getAuthorizationDetails().toJsonString(), UTF_8); + } + // Append scope metadata to additionalQueryParams. String scopeMetadataQueryParam = getScopeMetadataQueryParam(params.getConsentRequiredScopes(), params.getTenantDomain()); diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpointTest.java b/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpointTest.java index 34489873791..9f990810f4f 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpointTest.java +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/authz/OAuth2AuthzEndpointTest.java @@ -112,6 +112,7 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2ClientValidationResponseDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; import org.wso2.carbon.identity.oauth2.responsemode.provider.ResponseModeProvider; import org.wso2.carbon.identity.oauth2.responsemode.provider.impl.DefaultResponseModeProvider; import org.wso2.carbon.identity.oauth2.responsemode.provider.impl.FormPostResponseModeProvider; @@ -284,6 +285,9 @@ public class OAuth2AuthzEndpointTest extends TestOAuthEndpointBase { @Mock private CentralLogMgtServiceComponentHolder centralLogMgtServiceComponentHolderMock; + @Mock + private AuthorizationDetailsService authorizationDetailsService; + private static final String ERROR_PAGE_URL = "https://localhost:9443/authenticationendpoint/oauth2_error.do"; private static final String LOGIN_PAGE_URL = "https://localhost:9443/authenticationendpoint/login.do"; private static final String USER_CONSENT_URL = @@ -803,6 +807,10 @@ public void testAuthorizeForAuthenticationResponse(boolean isResultInRequest, bo when(oAuth2ScopeService.hasUserProvidedConsentForAllRequestedScopes( anyString(), isNull(), anyInt(), anyList())).thenReturn(true); + when(authorizationDetailsService.isUserAlreadyConsentedForAuthorizationDetails( + any(AuthenticatedUser.class), any(OAuth2Parameters.class))).thenReturn(true); + OAuth2AuthzEndpoint.setAuthorizationDetailsService(authorizationDetailsService); + mockServiceURLBuilder(serviceURLBuilder); setSupportedResponseModes(); Response response = oAuth2AuthzEndpoint.authorize(httpServletRequest, httpServletResponse); @@ -1644,6 +1652,8 @@ public void testHandleUserConsent(boolean isRespDTONull, String consent, boolean OAuthAuthzReqMessageContext authzReqMsgCtx = new OAuthAuthzReqMessageContext(authorizeReqDTO); when(consentCacheEntry.getAuthzReqMsgCtx()).thenReturn(authzReqMsgCtx); + OAuth2AuthzEndpoint.setAuthorizationDetailsService(authorizationDetailsService); + Response response; try { setSupportedResponseModes(); diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/pom.xml b/components/org.wso2.carbon.identity.oauth.rar.common/pom.xml new file mode 100644 index 00000000000..1ac995d59f0 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/pom.xml @@ -0,0 +1,60 @@ + + + org.wso2.carbon.identity.inbound.auth.oauth2 + identity-inbound-auth-oauth + 7.0.107-SNAPSHOT + ../../pom.xml + + + 4.0.0 + org.wso2.carbon.identity.oauth.rar.common + jar + WSO2 Carbon - Rich Authorization Requests Common + http://wso2.org + + + UTF-8 + + + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.core + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + + + + com.github.spotbugs + spotbugs-maven-plugin + + High + 2048 + + + + + + diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAO.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAO.java new file mode 100644 index 00000000000..a21b68b4d79 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAO.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.common.dao; + +import org.wso2.carbon.identity.oauth2.rar.common.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; + +import java.sql.SQLException; +import java.util.List; +import java.util.Set; + +/** + * Provides methods to interact with the database to manage authorization details. + */ +public interface AuthorizationDetailsDAO { + + /** + * Adds authorization details against a given OAuth2 code. + * + * @param authorizationCodeID The ID of the authorization code. + * @param authorizationDetails The authorization details to store. + * @param tenantId The tenant ID. + * @return An array of positive integers indicating the number of rows affected for each batch operation, + * or negative integers if any of the batch operations fail. + * @throws SQLException If a database access error occurs. + */ + int[] addOAuth2CodeAuthorizationDetails(String authorizationCodeID, AuthorizationDetails authorizationDetails, + int tenantId) throws SQLException; + + /** + * Adds user consented authorization details. + * + * @param authorizationDetailsConsentDTOs List of user consented authorization details DTOs. + * {@link AuthorizationDetailsConsentDTO } + * @return An array of positive integers indicating the number of rows affected for each batch operation, + * or negative integers if any of the batch operations fail. + * @throws SQLException If a database access error occurs. + */ + int[] addUserConsentedAuthorizationDetails(List authorizationDetailsConsentDTOs) + throws SQLException; + + int deleteUserConsentedAuthorizationDetails(String consentId, int tenantId) + throws SQLException; + + // add a todo and mention to move this to consent module + String getConsentIdByUserIdAndAppId(String userId, String appId, int tenantId) throws SQLException; + + Set getUserConsentedAuthorizationDetails(String consentId, int tenantId) + throws SQLException; + + int[] updateUserConsentedAuthorizationDetails(List authorizationDetailsConsentDTOs) + throws SQLException; +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAOImpl.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAOImpl.java new file mode 100644 index 00000000000..839f23c4642 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/AuthorizationDetailsDAOImpl.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.common.dao; + +import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; +import org.wso2.carbon.identity.oauth2.rar.common.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Implementation of {@link AuthorizationDetailsDAO}. + * This class provides methods to add authorization details to the database. + */ +public class AuthorizationDetailsDAOImpl implements AuthorizationDetailsDAO { + + /** + * Stores authorization details against the provided OAuth2 authorization code. + * + * @param authorizationCodeID The ID of the authorization code. + * @param authorizationDetails The details to be added. + * @param tenantId The tenant ID. + * @return An array of positive integers indicating the number of rows affected for each batch operation, + * or negative integers if any of the batch operations fail. + * @throws SQLException If a database access error occurs. + */ + @Override + public int[] addOAuth2CodeAuthorizationDetails(final String authorizationCodeID, + final AuthorizationDetails authorizationDetails, + final int tenantId) throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.ADD_OAUTH2_CODE_AUTHORIZATION_DETAILS)) { + + for (AuthorizationDetail authorizationDetail : authorizationDetails.getDetails()) { + ps.setString(1, authorizationCodeID); + ps.setString(2, authorizationDetail.getType()); + ps.setInt(3, tenantId); + ps.setString(4, authorizationDetail.toJsonString()); + ps.setInt(5, tenantId); + ps.addBatch(); + } + return ps.executeBatch(); + } + } + + /** + * Stores user consented authorization details. + * + * @param authorizationDetailsConsentDTOs The user consented authorization details DTOs + * @return An array of positive integers indicating the number of rows affected for each batch operation, + * or negative integers if any of the batch operations fail. + * @throws SQLException If a database access error occurs. + */ + @Override + public int[] addUserConsentedAuthorizationDetails( + final List authorizationDetailsConsentDTOs) throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.ADD_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS)) { + + for (AuthorizationDetailsConsentDTO consentDTO : authorizationDetailsConsentDTOs) { + ps.setString(1, consentDTO.getConsentId()); + ps.setString(2, consentDTO.getAuthorizationDetail().getType()); + ps.setInt(3, consentDTO.getTenantId()); + ps.setString(4, consentDTO.getAuthorizationDetail().toJsonString()); + ps.setBoolean(5, consentDTO.isConsentActive()); + ps.setInt(6, consentDTO.getTenantId()); + ps.addBatch(); + } + return ps.executeBatch(); + } + } + + @Override + public int[] updateUserConsentedAuthorizationDetails( + final List authorizationDetailsConsentDTOs) throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.UPDATE_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS)) { + + for (AuthorizationDetailsConsentDTO consentDTO : authorizationDetailsConsentDTOs) { + ps.setString(1, consentDTO.getAuthorizationDetail().toJsonString()); + ps.setBoolean(2, consentDTO.isConsentActive()); + ps.setString(3, consentDTO.getConsentId()); + ps.setInt(4, consentDTO.getTenantId()); + ps.addBatch(); + } + return ps.executeBatch(); + } + } + + @Override + public int deleteUserConsentedAuthorizationDetails(final String consentId, final int tenantId) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.DELETE_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS)) { + + ps.setString(1, consentId); + ps.setInt(2, tenantId); + return ps.executeUpdate(); + } + } + + @Override + public Set getUserConsentedAuthorizationDetails(final String consentId, + final int tenantId) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.GET_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS)) { + + ps.setString(1, consentId); + ps.setInt(2, tenantId); + try (ResultSet rs = ps.executeQuery()) { + + final Set authorizationDetailsConsentDTOs = new HashSet<>(); + while (rs.next()) { + final String id = rs.getString(1); + final int typeId = rs.getInt(2); + final String authorizationDetail = rs.getString(3); + final boolean isConsentActive = rs.getBoolean(4); + + authorizationDetailsConsentDTOs.add(new AuthorizationDetailsConsentDTO(id, consentId, typeId, + authorizationDetail, isConsentActive, tenantId)); + } + return authorizationDetailsConsentDTOs; + } + } + } + + /** + * Retrieves the first consent ID for a given user ID and application ID. + * + * @param userId The ID of the user. + * @param appId The ID of the application. + * @param tenantId The tenant ID. + * @return The first consent ID found, or null if no consent ID is found. + * @throws SQLException If a database access error occurs. + */ + @Override + public String getConsentIdByUserIdAndAppId(final String userId, final String appId, final int tenantId) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.GET_IDN_OAUTH2_USER_CONSENT_CONSENT_ID)) { + + ps.setString(1, userId); + ps.setString(2, appId); + ps.setInt(3, tenantId); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getString(1); + } + } + } + return null; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/SQLQueries.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/SQLQueries.java new file mode 100644 index 00000000000..79faa31c9ec --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dao/SQLQueries.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.common.dao; + +/** + * The {@code SQLQueries} class contains SQL query constants used for performing + * database operations related to OAuth2 Rich Authorization Requests. + */ +public class SQLQueries { + + private SQLQueries() { + // Private constructor to prevent instantiation + } + + public static final String ADD_OAUTH2_CODE_AUTHORIZATION_DETAILS = + "INSERT INTO IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS " + + "(CODE_ID, TYPE_ID, AUTHORIZATION_DETAILS, TENANT_ID) VALUES " + + "(?, (SELECT ID FROM IDN_OAUTH2_AUTHORIZATION_DETAILS_TYPES WHERE TYPE=? AND TENANT_ID=?), " + + "? FORMAT JSON, ?)"; + + public static final String GET_OAUTH2_CODE_AUTHORIZATION_DETAILS = + "SELECT IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS.AUTHORIZATION_DETAILS " + + "FROM IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS " + + "INNER JOIN IDN_OAUTH2_AUTHORIZATION_CODE " + + "ON IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS.CODE_ID = IDN_OAUTH2_AUTHORIZATION_CODE.CODE_ID " + + "WHERE IDN_OAUTH2_AUTHORIZATION_CODE.AUTHORIZATION_CODE=? " + + "AND IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS.TENANT_ID=?"; + + public static final String ADD_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS = + "INSERT INTO IDN_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS " + + "(CONSENT_ID, TYPE_ID, AUTHORIZATION_DETAILS, CONSENT, TENANT_ID) VALUES " + + "(?,(SELECT ID FROM IDN_OAUTH2_AUTHORIZATION_DETAILS_TYPES WHERE TYPE=? AND TENANT_ID=?), " + + "? FORMAT JSON, ?, ?)"; + + public static final String UPDATE_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS = + "UPDATE IDN_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS " + + "SET AUTHORIZATION_DETAILS=? CONSENT=? WHERE CONSENT_ID=? AND TENANT_ID=?"; + + public static final String GET_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS = + "SELECT ID, TYPE_ID, AUTHORIZATION_DETAILS, CONSENT FROM IDN_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS " + + "WHERE CONSENT_ID=? AND TENANT_ID=?"; + + public static final String DELETE_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS = + "DELETE FROM IDN_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS WHERE CONSENT_ID=? AND TENANT_ID=?"; + + public static final String CREATE_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS = + "INSERT INTO IDN_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS " + + "(AUTHORIZATION_DETAILS_TYPE, AUTHORIZATION_DETAILS, TOKEN_ID, TENANT_ID) VALUES (?, ?, ?, ?)"; + + public static final String GET_IDN_OAUTH2_USER_CONSENT_CONSENT_ID = + "SELECT CONSENT_ID FROM IDN_OAUTH2_USER_CONSENT WHERE USER_ID=? AND APP_ID=? AND TENANT_ID=?"; +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsCodeDTO.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsCodeDTO.java new file mode 100644 index 00000000000..bc143a93257 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsCodeDTO.java @@ -0,0 +1,22 @@ +package org.wso2.carbon.identity.oauth2.rar.common.dto; + +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; + +/** + * + */ +public class AuthorizationDetailsCodeDTO extends AuthorizationDetailsDTO { + + final String codeId; + + public AuthorizationDetailsCodeDTO(final String id, final String codeId, final int typeId, + final AuthorizationDetail authorizationDetail, final int tenantId) { + + super(id, typeId, authorizationDetail, tenantId); + this.codeId = codeId; + } + + public String getCodeId() { + return codeId; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsConsentDTO.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsConsentDTO.java new file mode 100644 index 00000000000..0cf61dc2306 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsConsentDTO.java @@ -0,0 +1,38 @@ +package org.wso2.carbon.identity.oauth2.rar.common.dto; + +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; + +/** + * + */ +public class AuthorizationDetailsConsentDTO extends AuthorizationDetailsDTO { + + final String consentId; + final boolean isConsentActive; + + public AuthorizationDetailsConsentDTO(final String id, final String consentId, final int typeId, + final String authorizationDetail, + final boolean isConsentActive, final int tenantId) { + + super(id, typeId, authorizationDetail, tenantId); + this.consentId = consentId; + this.isConsentActive = isConsentActive; + } + + public AuthorizationDetailsConsentDTO(final String consentId, + final AuthorizationDetail authorizationDetail, + final boolean isConsentActive, final int tenantId) { + + super(authorizationDetail, tenantId); + this.consentId = consentId; + this.isConsentActive = isConsentActive; + } + + public boolean isConsentActive() { + return isConsentActive; + } + + public String getConsentId() { + return consentId; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsDTO.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsDTO.java new file mode 100644 index 00000000000..fa73865c8b4 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/dto/AuthorizationDetailsDTO.java @@ -0,0 +1,53 @@ +package org.wso2.carbon.identity.oauth2.rar.common.dto; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsCommonUtils; + +/** + * + */ +public class AuthorizationDetailsDTO { + + final String id; + final int typeId; + final AuthorizationDetail authorizationDetail; + final int tenantId; + + public AuthorizationDetailsDTO(final String id, final int typeId, final AuthorizationDetail authorizationDetail, + final int tenantId) { + + this.id = id; + this.typeId = typeId; + this.authorizationDetail = authorizationDetail; + this.tenantId = tenantId; + } + + public AuthorizationDetailsDTO(final String id, final int typeId, final String authorizationDetailJson, + final int tenantId) { + + this(id, typeId, AuthorizationDetailsCommonUtils + .fromJSON(authorizationDetailJson, AuthorizationDetail.class, new ObjectMapper()), tenantId); + } + + public AuthorizationDetailsDTO(final AuthorizationDetail authorizationDetail, final int tenantId) { + + this(null, 0, authorizationDetail, tenantId); + } + + public String getId() { + return this.id; + } + + public int getTypeId() { + return this.typeId; + } + + public AuthorizationDetail getAuthorizationDetail() { + return this.authorizationDetail; + } + + public int getTenantId() { + return this.tenantId; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetail.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetail.java new file mode 100644 index 00000000000..96194b34856 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetail.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.common.model; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.StringUtils; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsCommonUtils; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; + +/** + * Represents an individual authorization details object which specifies the authorization requirements for a + * specific resource type within the {@code authorization_details} request parameter used in OAuth 2.0 flows + * (as defined in RFC 9396: OAuth 2.0 Rich Authorization + * Requests). + * + *

This class encapsulates the various attributes and their corresponding values that can be included within an + * authorization details object. The mandatory {@code type} field identifies the resource type or access requirement + * being described.

+ *

+ * Here is an example of {@code authorization_details} with + * Common Data Fields. + *

 {@code
+ * [
+ *   {
+ *     "type": "customer_information",
+ *     "locations": [
+ *       "https://example.com/customers"
+ *     ],
+ *     "actions": [
+ *       "read",
+ *       "write"
+ *     ],
+ *     "datatypes": [
+ *       "contacts",
+ *       "photos"
+ *     ],
+ *     "identifier":"account-14-32-32-3",
+ *     "privileges": [
+ *       "admin"
+ *     ]
+ *   }
+ * ]
+ * } 
+ * + *

Refer to + * OAuth 2.0 Rich Authorization Requests for detailed information on the Authorization Details structure.

+ * + * @since 7.0.26.9 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AuthorizationDetail implements Serializable { + + private static final long serialVersionUID = -3928636285264078857L; + + @JsonIgnore + private String id; + private String type; + private List locations; + private List actions; + @JsonProperty("datatypes") + private List dataTypes; + private String identifier; + private List privileges; + private Map details; + private String consentDescription; + + public AuthorizationDetail() { + this.setId(UUID.randomUUID().toString()); + } + + public String getId() { + return this.id; + } + + @JsonProperty("_id") + public void setId(final String id) { + this.id = id; + } + + /** + * Gets the value of the type field associated with the authorization details object. + * + *

{@code type} is a unique identifier for the authorization details type as a string. The value of the type + * field determines the allowable contents of the object that contains it.

+ * + * @return The String value of the type field + * @see + * Authorization Details Types + */ + public String getType() { + return this.type; + } + + public void setType(final String type) { + this.type = type; + } + + /** + * Gets the optional list of locations associated with the authorization details object. + * + *

{@code locations} is an array of strings representing the location of the resource or RS. These strings are + * typically URIs identifying the location of the RS. This field can allow a client to specify a particular RS.

+ * + * @return A list of locations or {@code null} if the {@code locations} field is not present. + */ + public List getLocations() { + return this.locations; + } + + public void setLocations(final List locations) { + this.locations = locations; + } + + /** + * Gets the optional list of actions associated with the authorization details object. + * + *

{@code actions} is an array of strings representing the kinds of actions to be taken at the resource. + * + * @return A list of actions or {@code null} if the {@code actions} field is not present. + */ + public List getActions() { + return this.actions; + } + + public void setActions(final List actions) { + this.actions = actions; + } + + /** + * Gets the optional list of data types associated with the authorization details object. + * + *

{@code datatypes} is an array of strings representing what kinds of data being requested from the resource. + * + * @return A list of datatypes or {@code null} if the {@code datatypes} field is not present. + */ + public List getDataTypes() { + return this.dataTypes; + } + + public void setDataTypes(final List dataTypes) { + this.dataTypes = dataTypes; + } + + /** + * Gets the optional String identifier associated with the authorization details object. + * + *

{@code identifier} is a string identifier indicating a specific resource available at the API. + * + * @return The String value of the identifier or {@code null} if the {@code identifier} field is not present. + */ + public String getIdentifier() { + return this.identifier; + } + + public void setIdentifier(final String identifier) { + this.identifier = identifier; + } + + /** + * Gets the optional list of privileges associated with the authorization details object. + * + *

{@code privileges} is an array of strings representing the types or levels of privilege being requested + * at the resource. + * + * @return The String value of the privileges or {@code null} if the {@code privileges} field is not present. + */ + public List getPrivileges() { + return this.privileges; + } + + public void setPrivileges(final List privileges) { + this.privileges = privileges; + } + + /** + * Gets a map containing API-specific fields from the authorization details object. The presence and structure + * of these fields can vary depending on the specific API being accessed. + * + * @return A map containing API-specific fields or {@code null} if no fields are present. + */ + @JsonAnyGetter + public Map getDetails() { + return this.details; + } + + public void setDetails(final Map details) { + this.details = details; + } + + @JsonAnySetter + public void setDetail(final String key, final Object value) { + if (this.details == null) { + setDetails(new HashMap<>()); + } + this.details.put(key, value); + } + + /** + * Returns the consent description of an {@link AuthorizationDetail} instance. + * This value is only available after the enrichment process. The consent description provides a human-readable + * representation of the {@code authorization_details}, typically in the form of a sentence derived from the + * JSON object. + * + * @return A string representing the consent description of the {@code authorization_details}. + */ + public String getConsentDescription() { + return this.consentDescription; + } + + /** + * Sets a human-readable sentence that describes the {@code authorization_details}. This sentence is used to + * display to the user and obtain their consent for the current {@link AuthorizationDetail AuthorizationDetail}. + * + * @param consentDescription A string representing the description of the authorization detail. + * This description should be clear and understandable to the user, + * explaining what they are consenting to. + */ + public void setConsentDescription(final String consentDescription) { + this.consentDescription = consentDescription; + } + + /** + * Returns the consent description if present; otherwise, returns a value supplied by the provided {@link Function}. + * Example usage: + *

 {@code
+     * // Example 1: Using a simple default function that returns the "type", if description is missing
+     * AuthorizationDetail detail = new AuthorizationDetail();
+     * detail.setType("user_information");
+     * detail.setConsentDescription(""); // Empty description
+     * String result = detail.getConsentDescriptionOrDefault(authDetail -> authDetail.getType());
+     * // result will be "user_information"
+     *
+     * // Example 2: Consent description is already set and not empty
+     * AuthorizationDetail detail = new AuthorizationDetail();
+     * detail.setType("user_information");
+     * detail.setConsentDescription("User consented to data usage");
+     * String result = detail.getConsentDescriptionOrDefault(authDetail -> "Default Description");
+     * // result will be "User consented to data usage"
+     * } 
+ * + * @param defaultFunction the Function that provides a default value if the consent description is not present + * @return the consent description if present, otherwise the value from the Function + */ + public String getConsentDescriptionOrDefault(Function defaultFunction) { + return StringUtils.isNotEmpty(this.getConsentDescription()) ? + this.getConsentDescription() : defaultFunction.apply(this); + } + + /** + * Converts the current authorization detail instance to a JSON string. + * + * @return The JSON representation of the authorization detail. + */ + public String toJsonString() { + return AuthorizationDetailsCommonUtils.toJSON(this, new ObjectMapper()); + } + + @Override + public String toString() { + return "AuthorizationDetails {" + + "type='" + this.type + '\'' + + ", locations=" + this.locations + + ", actions=" + this.actions + + ", datatypes=" + this.dataTypes + + ", identifier=" + this.identifier + + ", privileges=" + this.privileges + + ", details=" + this.details + + ", consentDescription=" + this.consentDescription + + '}'; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetails.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetails.java new file mode 100644 index 00000000000..0befab02b11 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/model/AuthorizationDetails.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.common.model; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.StringUtils; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsCommonUtils; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsConstants; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a set of {@link AuthorizationDetail} objects which specifies the authorization requirements for a + * specific resource type within the {@code authorization_details} request parameter used in OAuth 2.0 flows + * (as defined in RFC 9396: OAuth 2.0 Rich Authorization + * Requests). + * + *

Refer to + * OAuth 2.0 Rich Authorization Requests for detailed information on the Authorization Details structure.

+ * + * @see AuthorizationDetail + */ +public class AuthorizationDetails implements Serializable { + + private static final long serialVersionUID = -663187547075070618L; + + private final Set authorizationDetails; + + /** + * Constructs an empty set of {@link AuthorizationDetail}. + */ + public AuthorizationDetails() { + this(Collections.emptySet()); + } + + /** + * Constructs an immutable set of {@link AuthorizationDetail}. + * + * @param authorizationDetails The set of authorization details. If null, an empty set is assigned. + */ + public AuthorizationDetails(final Set authorizationDetails) { + this.authorizationDetails = Optional.ofNullable(authorizationDetails) + .map(Collections::unmodifiableSet) + .orElse(Collections.emptySet()); + } + + /** + * Constructs an immutable set of {@link AuthorizationDetail} from a JSON string. + * + * @param authorizationDetailsJson The JSON string representing the authorization details. + */ + public AuthorizationDetails(final String authorizationDetailsJson) { + this(AuthorizationDetailsCommonUtils.fromJSONArray( + authorizationDetailsJson, AuthorizationDetail.class, new ObjectMapper())); + } + + /** + * Returns a set of the {@code authorization_details}. + * + * @return A set of {@link AuthorizationDetail}. + */ + public Set getDetails() { + return this.authorizationDetails; + } + + /** + * Returns a set of the {@code authorization_details} filtered by provided type. + * + * @return A set of {@link AuthorizationDetail}. + */ + public Set getDetailsByType(final String type) { + return this.stream() + .filter(Objects::nonNull) + .filter(authorizationDetail -> StringUtils.equals(authorizationDetail.getType(), type)) + .collect(Collectors.toSet()); + } + + /** + * Converts the current set of authorization details to a JSON string. + * + * @return The JSON representation of the authorization details. + */ + public String toJsonString() { + return AuthorizationDetailsCommonUtils.toJSON(this.getDetails(), new ObjectMapper()); + } + + /** + * Converts the set of authorization details to a human-readable string. + * Each detail's consent description is obtained or the type if the description is unavailable. + * + * @return A string representing the authorization details in a human-readable format. + */ + public String toReadableText() { + + return this.stream() + .map(authorizationDetail -> + authorizationDetail.getConsentDescriptionOrDefault(AuthorizationDetail::getType)) + .collect(Collectors.joining(AuthorizationDetailsConstants.PARAM_SEPARATOR)); + } + + public Stream stream() { + return this.getDetails().stream(); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsCommonUtils.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsCommonUtils.java new file mode 100644 index 00000000000..c908656d18f --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsCommonUtils.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.common.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; + +import java.util.HashSet; +import java.util.Set; + +/** + * Utility class for handling OAuth2 Rich Authorization Requests. + */ +public class AuthorizationDetailsCommonUtils { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsCommonUtils.class); + private static final String empty_json = "{}"; + private static final String empty_json_array = "[]"; + + private AuthorizationDetailsCommonUtils() { + // Private constructor to prevent instantiation + } + + /** + * Parses the given JSON array string into a set of {@link AuthorizationDetail} objects. + * + * @param authorizationDetailsJson A JSON string containing authorization details which comes in the + * OAuth 2.0 authorization request or token request + * @param objectMapper A Jackson {@link ObjectMapper} used for parsing + * @param clazz A Class that extends {@link AuthorizationDetail} to be parsed + * @param the type parameter extending {@code AuthorizationDetail} + * @return an immutable set of {@link AuthorizationDetail} objects parsed from the given JSON string, + * or an empty set if parsing fails + * @see org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails + */ + public static Set fromJSONArray( + final String authorizationDetailsJson, final Class clazz, final ObjectMapper objectMapper) { + + try { + if (StringUtils.isNotEmpty(authorizationDetailsJson)) { + return objectMapper.readValue(authorizationDetailsJson, + objectMapper.getTypeFactory().constructCollectionType(Set.class, clazz)); + } + } catch (JsonProcessingException e) { + log.debug("Error occurred while parsing String to AuthorizationDetails. Caused by, ", e); + } + return new HashSet<>(); + } + + /** + * Parses the given JSON object string into an {@link AuthorizationDetail} object. + * + * @param authorizationDetailJson A JSON string containing authorization detail object + * @param objectMapper A Jackson {@link ObjectMapper} used for parsing + * @param clazz A Class that extends {@link AuthorizationDetail} to be parsed + * @param the type parameter extending {@code AuthorizationDetail} + * @return an {@link AuthorizationDetail} objects parsed from the given JSON string, + * or null if parsing fails + * @see org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail + */ + public static T fromJSON( + final String authorizationDetailJson, final Class clazz, final ObjectMapper objectMapper) { + + try { + if (StringUtils.isNotEmpty(authorizationDetailJson)) { + return objectMapper.readValue(authorizationDetailJson, clazz); + } + } catch (JsonProcessingException e) { + log.debug("Error occurred while parsing String to AuthorizationDetails. Caused by, ", e); + } + return null; + } + + /** + * Converts a set of {@code AuthorizationDetail} objects into a JSON string. + *

+ * If the input set is {@code null} or an exception occurs during the conversion, + * an empty JSON array ({@code []}) is returned. + *

+ * + * @param authorizationDetails the set of {@code AuthorizationDetail} objects to convert + * @param objectMapper the {@code ObjectMapper} instance to use for serialization + * @param the type parameter extending {@code AuthorizationDetail} + * @return a JSON string representation of the authorization details set, + * or an empty JSON array if null or an error occurs + * @see org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail + * @see org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails + */ + public static String toJSON( + final Set authorizationDetails, final ObjectMapper objectMapper) { + + try { + if (authorizationDetails != null) { + return objectMapper.writeValueAsString(authorizationDetails); + } + } catch (JsonProcessingException e) { + log.debug("Error occurred while parsing AuthorizationDetails to String. Caused by, ", e); + } + return empty_json_array; + } + + /** + * Converts a single {@code AuthorizationDetail} object into a JSON string. + *

+ * If the input object is {@code null} or an exception occurs during the conversion, + * an empty JSON object ({@code {}}) is returned. + *

+ * + * @param authorizationDetail the {@code AuthorizationDetail} object to convert + * @param objectMapper the {@code ObjectMapper} instance to use for serialization + * @param the type parameter extending {@code AuthorizationDetail} + * @return a JSON string representation of the authorization detail, + * or an empty JSON object if null or an error occurs + * @see org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail + * @see org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails + */ + public static String toJSON( + final T authorizationDetail, final ObjectMapper objectMapper) { + + try { + if (authorizationDetail != null) { + return objectMapper.writeValueAsString(authorizationDetail); + } + } catch (JsonProcessingException e) { + log.debug("Error occurred while parsing AuthorizationDetail to String. Caused by, ", e); + } + return empty_json; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsConstants.java b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsConstants.java new file mode 100644 index 00000000000..5b1ab3d97f4 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar.common/src/main/java/org/wso2/carbon/identity/oauth2/rar/common/util/AuthorizationDetailsConstants.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.common.util; + +/** + * Stores constants related to OAuth2 Rich Authorization Requests. + */ +public final class AuthorizationDetailsConstants { + + private AuthorizationDetailsConstants() { + // Private constructor to prevent instantiation + } + + public static final String AUTHORIZATION_DETAILS = "authorization_details"; + public static final String AUTHORIZATION_DETAILS_ID_PREFIX = "authorization_detail_id_"; + public static final String PARAM_SEPARATOR = "&&"; + + public static final String TYPE_NOT_SUPPORTED_ERR_MSG_FORMAT = "%s is not a supported authorization details type"; + public static final String VALIDATION_FAILED_ERR_MSG = "Authorization details validation failed"; + public static final String VALIDATION_FAILED_ERR_CODE = "invalid_authorization_details"; +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/pom.xml b/components/org.wso2.carbon.identity.oauth.rar/pom.xml new file mode 100644 index 00000000000..bb6bc7c8617 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/pom.xml @@ -0,0 +1,108 @@ + + + + + + org.wso2.carbon.identity.inbound.auth.oauth2 + identity-inbound-auth-oauth + ../../pom.xml + 7.0.107-SNAPSHOT + + + 4.0.0 + org.wso2.carbon.identity.oauth.rar + bundle + WSO2 Carbon - Rich Authorization Requests + http://wso2.org + + + UTF-8 + + + + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth + provided + + + + com.google.auto.service + auto-service + + + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar.common + + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + ${project.artifactId} + + + org.wso2.carbon.identity.oauth2.rar.internal + + + org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}", + org.osgi.service.component; version="${osgi.service.component.imp.pkg.version.range}", + org.wso2.carbon.identity.oauth2.*; version="${identity.inbound.auth.oauth.exp.pkg.version}", + + + !org.wso2.carbon.identity.oauth2.rar.internal, + org.wso2.carbon.identity.oauth2.rar.*; + version="${identity.inbound.auth.oauth.exp.pkg.version}", + + * + <_dsannotations>* + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + + + + + com.github.spotbugs + spotbugs-maven-plugin + + High + 2048 + + + + + + diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsService.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsService.java new file mode 100644 index 00000000000..00f4c97c965 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsService.java @@ -0,0 +1,278 @@ +package org.wso2.carbon.identity.oauth2.rar; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.wso2.carbon.identity.application.authentication.framework.exception.UserIdNotFoundException; +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.application.common.model.ServiceProvider; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.common.dao.AuthorizationDetailsDAO; +import org.wso2.carbon.identity.oauth2.rar.common.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProvider; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProviderFactory; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; + +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsConstants.AUTHORIZATION_DETAILS_ID_PREFIX; + +/** + * + */ +public class AuthorizationDetailsService extends IdentityOAuth2AuthorizationDetailsService { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsService.class); + private final AuthorizationDetailsProviderFactory authorizationDetailsProviderFactory; + + public AuthorizationDetailsService() { + + this(OAuthTokenPersistenceFactory.getInstance().getAuthorizationDetailsDAO(), + AuthorizationDetailsProviderFactory.getInstance()); + } + + public AuthorizationDetailsService(final AuthorizationDetailsDAO authorizationDetailsDAO, + final AuthorizationDetailsProviderFactory authorizationDetailsProviderFactory) { + + super(authorizationDetailsDAO); + this.authorizationDetailsProviderFactory = authorizationDetailsProviderFactory; + } + + public void storeUserConsentedAuthorizationDetails(final AuthenticatedUser authenticatedUser, final String clientId, + final OAuth2Parameters oAuth2Parameters, + final AuthorizationDetails userConsentedAuthorizationDetails) + throws OAuthSystemException { + + if (!AuthorizationDetailsService.isRichAuthorizationRequest(oAuth2Parameters)) { + return; + } + + try { + final int tenantId = OAuth2Util.getTenantId(oAuth2Parameters.getTenantDomain()); + final Optional consentId = this.getConsentId(authenticatedUser, clientId, tenantId); + + if (consentId.isPresent()) { + + super.authorizationDetailsDAO.addUserConsentedAuthorizationDetails( + generateAuthorizationDetailsConsentDTOs(consentId.get(), + userConsentedAuthorizationDetails, tenantId)); + } + } catch (SQLException | IdentityOAuth2Exception e) { + log.error("Error occurred while storing user consented authorization details. Caused by, ", e); + throw new OAuthSystemException("Error occurred while storing authorization details", e); + } + } + + public void updateUserConsentedAuthorizationDetails(final AuthenticatedUser authenticatedUser, + final String clientId, final OAuth2Parameters oAuth2Parameters, + final AuthorizationDetails userConsentedAuthorizationDetails) + throws OAuthSystemException { + + if (!AuthorizationDetailsService.isRichAuthorizationRequest(oAuth2Parameters)) { + return; + } + + try { + final int tenantId = OAuth2Util.getTenantId(oAuth2Parameters.getTenantDomain()); + final Optional consentId = this.getConsentId(authenticatedUser, clientId, tenantId); + + if (consentId.isPresent()) { + + super.authorizationDetailsDAO.updateUserConsentedAuthorizationDetails( + generateAuthorizationDetailsConsentDTOs(consentId.get(), + userConsentedAuthorizationDetails, tenantId)); + } + } catch (SQLException | IdentityOAuth2Exception e) { + log.error("Error occurred while updating user consented authorization details. Caused by, ", e); + throw new OAuthSystemException("Error occurred while updating authorization details", e); + } + } + + public void deleteUserConsentedAuthorizationDetails(final AuthenticatedUser authenticatedUser, + final String clientId, final OAuth2Parameters oAuth2Parameters) + throws OAuthSystemException { + + if (!AuthorizationDetailsService.isRichAuthorizationRequest(oAuth2Parameters)) { + return; + } + + try { + final int tenantId = OAuth2Util.getTenantId(oAuth2Parameters.getTenantDomain()); + final Optional consentId = this.getConsentId(authenticatedUser, clientId, tenantId); + + if (consentId.isPresent()) { + + super.authorizationDetailsDAO.deleteUserConsentedAuthorizationDetails(consentId.get(), tenantId); + } + } catch (SQLException | IdentityOAuth2Exception e) { + log.error("Error occurred while deleting user consented authorization details. Caused by, ", e); + throw new OAuthSystemException("Error occurred while storing authorization details", e); + } + } + + public void storeOrReplaceUserConsentedAuthorizationDetails( + final AuthenticatedUser authenticatedUser, final String clientId, final OAuth2Parameters oAuth2Parameters, + final AuthorizationDetails userConsentedAuthorizationDetails) throws OAuthSystemException { + + this.deleteUserConsentedAuthorizationDetails(authenticatedUser, clientId, oAuth2Parameters); + this.storeUserConsentedAuthorizationDetails(authenticatedUser, clientId, oAuth2Parameters, + userConsentedAuthorizationDetails); + } + + /** + * Check if the user has already given consent to requested authorization details. + * + * @param authenticatedUser Authenticated user. + * @param oAuth2Parameters OAuth2 parameters. + * @return True if user has given consent to all the requested authorization details. + */ + public boolean isUserAlreadyConsentedForAuthorizationDetails(final AuthenticatedUser authenticatedUser, + final OAuth2Parameters oAuth2Parameters) + throws OAuthSystemException { + + try { + final String userId = this.getUserId(authenticatedUser); + final String appId = this.getApplicationResourceIdFromClientId(oAuth2Parameters.getClientId()); + final int tenantId = OAuth2Util.getTenantId(oAuth2Parameters.getTenantDomain()); + final Optional consentId = this.getConsentIdByUserIdAndAppId(userId, appId, tenantId); + + if (consentId.isPresent()) { + final Set consentedAuthorizationDetailsDTOs = + super.authorizationDetailsDAO.getUserConsentedAuthorizationDetails(consentId.get(), tenantId); + + final Map> consentedAuthorizationDetailsByType = + consentedAuthorizationDetailsDTOs + .stream() + .filter(AuthorizationDetailsConsentDTO::isConsentActive) + .map(AuthorizationDetailsConsentDTO::getAuthorizationDetail) + .collect(Collectors.groupingBy(AuthorizationDetail::getType, + Collectors.mapping(Function.identity(), Collectors.toSet()))); + + for (final AuthorizationDetail requestedAuthorizationDetail : + oAuth2Parameters.getAuthorizationDetails().getDetails()) { + + if (consentedAuthorizationDetailsByType.containsKey(requestedAuthorizationDetail.getType())) { + + final Optional provider = authorizationDetailsProviderFactory + .getProviderByType(requestedAuthorizationDetail.getType()); + if (provider.isPresent()) { + + final AuthorizationDetails existingAuthorizationDetails = new AuthorizationDetails( + consentedAuthorizationDetailsByType.get(requestedAuthorizationDetail.getType())); + if (!provider.get() + .isEqualOrSubset(requestedAuthorizationDetail, existingAuthorizationDetails)) { + + if (log.isDebugEnabled()) { + log.debug("User has not consented for the requested authorization details type: " + + requestedAuthorizationDetail.getType()); + + } + return false; + } + } + } + } + return true; + } + return false; + } catch (IdentityOAuth2Exception | SQLException e) { + log.error("Error occurred while extracting user consented authorization details. Caused by, ", e); + throw new OAuthSystemException("Error occurred while extracting user consented authorization details", e); + } + } + + /** + * Retrieves the user-consented authorization details based on the provided parameter map and OAuth2 parameters. + *

+ * This method is used to extract and return the authorization details that the user has consented to, + * filtering them based on a provided authorization details in the parameter map. + *

+ * + * @param parameterMap A map of query parameters. + * @param oAuth2Parameters The OAuth2 parameters that include the details of the authorization request. + * @return The {@link AuthorizationDetails} object containing the details the user has consented to. + */ + public AuthorizationDetails getUserConsentedAuthorizationDetails( + final Map parameterMap, final OAuth2Parameters oAuth2Parameters) { + + if (!isRichAuthorizationRequest(oAuth2Parameters)) { + return new AuthorizationDetails(); + } + + // Extract consented authorization detail IDs from the parameter map + final Set consentedAuthorizationDetailIDs = parameterMap.keySet().stream() + .filter(parameterName -> parameterName.startsWith(AUTHORIZATION_DETAILS_ID_PREFIX)) + .map(parameterName -> parameterName.substring(AUTHORIZATION_DETAILS_ID_PREFIX.length())) + .collect(Collectors.toSet()); + + // Filter and collect the consented authorization details + final Set consentedAuthorizationDetails = oAuth2Parameters.getAuthorizationDetails() + .stream() + .filter(authorizationDetail -> consentedAuthorizationDetailIDs.contains(authorizationDetail.getId())) + .collect(Collectors.toSet()); + + return new AuthorizationDetails(consentedAuthorizationDetails); + } + + public Optional getConsentIdByUserIdAndAppId(final String userId, final String appId, final int tenantId) + throws OAuthSystemException { + + try { + return Optional + .ofNullable(super.authorizationDetailsDAO.getConsentIdByUserIdAndAppId(userId, appId, tenantId)); + } catch (SQLException e) { + log.error(String.format("Error occurred while retrieving user consent by " + + "userId: %s and appId: %s. Caused by, ", userId, appId), e); + throw new OAuthSystemException("Error occurred while retrieving user consent", e); + } + } + + private String getApplicationResourceIdFromClientId(final String clientId) throws IdentityOAuth2Exception { + + final ServiceProvider serviceProvider = OAuth2Util.getServiceProvider(clientId); + if (serviceProvider != null) { + return serviceProvider.getApplicationResourceId(); + } + throw new IdentityOAuth2Exception("Unable to find a service provider for client Id: " + clientId); + } + + private String getUserId(final AuthenticatedUser authenticatedUser) throws OAuthSystemException { + try { + return authenticatedUser.getUserId(); + } catch (UserIdNotFoundException e) { + log.error("Error occurred while extracting userId from authenticated user. Caused by, ", e); + throw new OAuthSystemException( + "User id is not found for user: " + authenticatedUser.getLoggableMaskedUserId(), e); + } + } + + private List generateAuthorizationDetailsConsentDTOs( + final String consentId, final AuthorizationDetails userConsentedAuthorizationDetails, final int tenantId) { + + return userConsentedAuthorizationDetails.stream() + .map(authorizationDetail -> + new AuthorizationDetailsConsentDTO(consentId, authorizationDetail, true, tenantId)) + .collect(Collectors.toList()); + } + + private Optional getConsentId(final AuthenticatedUser authenticatedUser, final String clientId, + final int tenantId) + throws OAuthSystemException, IdentityOAuth2Exception { + + final String userId = this.getUserId(authenticatedUser); + final String appId = this.getApplicationResourceIdFromClientId(clientId); + + return this.getConsentIdByUserIdAndAppId(userId, appId, tenantId); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsValidator.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsValidator.java new file mode 100644 index 00000000000..6b67f226d83 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsValidator.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProvider; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProviderFactory; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetailsContext; +import org.wso2.carbon.identity.oauth2.rar.model.ValidationResult; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * {@code AuthorizationDetailsValidator} class responsible for managing and validating authorization details. + */ +public class AuthorizationDetailsValidator { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsValidator.class); + private final AuthorizationDetailsProviderFactory authorizationDetailsProviderFactory; + + public AuthorizationDetailsValidator() { + + this(AuthorizationDetailsProviderFactory.getInstance()); + } + + public AuthorizationDetailsValidator(AuthorizationDetailsProviderFactory authorizationDetailsProviderFactory) { + + this.authorizationDetailsProviderFactory = authorizationDetailsProviderFactory; + } + + /** + * Retrieves and validates the authorization details for a given OAuth2 parameters context. + * + * @param oAuth2Parameters The OAuth2 parameters associated with the request. + * @param oAuthAppDO The OAuth application details. + * @param authenticatedUser The authenticated user information. + * @return An {@link AuthorizationDetails} object containing the validated authorization details. + */ + public AuthorizationDetails getValidatedAuthorizationDetails( + final OAuth2Parameters oAuth2Parameters, final OAuthAppDO oAuthAppDO, + final AuthenticatedUser authenticatedUser) throws AuthorizationDetailsProcessingException { + + final Set validatedAuthorizationDetails = new HashSet<>(); + final Set authorizedAuthorizationDetailsTypes = this.getAuthorizedAuthorizationDetailsTypes( + oAuth2Parameters.getClientId(), oAuth2Parameters.getTenantDomain()); + for (AuthorizationDetail authorizationDetail : oAuth2Parameters.getAuthorizationDetails().getDetails()) { + + if (!isSupportedAuthorizationDetailType(authorizationDetail.getType())) { + throw new AuthorizationDetailsProcessingException(String.format(AuthorizationDetailsConstants + .TYPE_NOT_SUPPORTED_ERR_MSG_FORMAT, authorizationDetail.getType())); + } + + if (isAuthorizedAuthorizationDetail(authorizationDetail, authorizedAuthorizationDetailsTypes)) { + + final AuthorizationDetailsContext authorizationDetailsContext = new AuthorizationDetailsContext( + oAuth2Parameters, oAuthAppDO, authenticatedUser, authorizationDetail); + + if (isValidAuthorizationDetail(authorizationDetailsContext)) { + validatedAuthorizationDetails.add(getEnrichedAuthorizationDetail(authorizationDetailsContext)); + } + } + } + + return new AuthorizationDetails(validatedAuthorizationDetails); + } + + private boolean isAuthorizedAuthorizationDetail(final AuthorizationDetail authorizationDetail, + final Set authorizedAuthorizationDetailsTypes) { + + return authorizedAuthorizationDetailsTypes.contains(authorizationDetail.getType()); + } + + private boolean isSupportedAuthorizationDetailType(final String authorizationDetailType) { + + return this.authorizationDetailsProviderFactory + .isSupportedAuthorizationDetailsType(authorizationDetailType); + } + + /** + * Checks if the provided authorization details context is valid. + * + * @param authorizationDetailsContext The context containing authorization details. + * @return {@code true} if the authorization details are valid; {@code false} otherwise. + */ + private boolean isValidAuthorizationDetail(final AuthorizationDetailsContext authorizationDetailsContext) + throws AuthorizationDetailsProcessingException { + + Optional optionalProvider = this.authorizationDetailsProviderFactory + .getProviderByType(authorizationDetailsContext.getAuthorizationDetail().getType()); + + if (optionalProvider.isPresent()) { + + final ValidationResult validationResult = optionalProvider.get().validate(authorizationDetailsContext); + if (log.isDebugEnabled() && validationResult.isInvalid()) { + + log.debug(String.format("Authorization details validation failed for type %s. Caused by, %s", + authorizationDetailsContext.getAuthorizationDetail().getType(), validationResult.getReason())); + + } + return validationResult.isValid(); + } + throw new AuthorizationDetailsProcessingException(String.format( + AuthorizationDetailsConstants.TYPE_NOT_SUPPORTED_ERR_MSG_FORMAT, + authorizationDetailsContext.getAuthorizationDetail().getType())); + } + + /** + * Enriches the authorization details using the provided context. + * + * @param authorizationDetailsContext The context containing authorization details. + * @return An enriched {@link AuthorizationDetail} object. + */ + private AuthorizationDetail getEnrichedAuthorizationDetail( + final AuthorizationDetailsContext authorizationDetailsContext) { + + return this.authorizationDetailsProviderFactory + .getProviderByType(authorizationDetailsContext.getAuthorizationDetail().getType()) + .map(authorizationDetailsService -> authorizationDetailsService.enrich(authorizationDetailsContext)) + // If provider is missing, return the original authorization detail instance + .orElse(authorizationDetailsContext.getAuthorizationDetail()); + } + + /** + * Retrieves the set of authorized authorization types for the given client and tenant domain. + * + * @param clientID The client ID. + * @param tenantDomain The tenant domain. + * @return A set of strings representing the authorized authorization types. + */ + private Set getAuthorizedAuthorizationDetailsTypes(final String clientID, final String tenantDomain) { + +// try { +// final String appId = OAuth2Util +// .getApplicationResourceIDByClientId(clientID, tenantDomain, this.applicationMgtService); +// +//// OAuth2ServiceComponentHolder.getInstance().getAuthorizedAPIManagementService() +// .getAuthorizedAuthorizationDetailsTypes(appId, tenantDomain); +// } catch (IdentityOAuth2Exception e) { +// throw new RuntimeException(e); +// } + Set authorizedAuthorizationDetailsTypes = new HashSet<>(); + authorizedAuthorizationDetailsTypes.add("payment_initiation"); + return authorizedAuthorizationDetailsTypes; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProvider.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProvider.java new file mode 100644 index 00000000000..aa05dec6250 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProvider.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.core; + +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetailsContext; +import org.wso2.carbon.identity.oauth2.rar.model.ValidationResult; + +/** + * The {@code AuthorizationDetailsProvider} interface defines a contract for implementing + * different types of authorization detail providers in a Service Provider Interface (SPI) setup. + *

+ * Implementing classes are expected to provide mechanisms to validate, enrich, and identify + * authorization details specific to various types. + *

+ * + * @see Java SPI + */ +public interface AuthorizationDetailsProvider { + + /** + * Validates the provided authorization details context when a new Rich Authorization Request is received. + *

+ * This method is invoked once a new Rich Authorization Request is received to ensure that the + * authorization details are valid and meet the required criteria. The validation logic should + * be specific to the type of authorization details handled by the implementing class. + *

+ * + * @param authorizationDetailsContext the context containing the authorization details to be validated. + * @return a {@code ValidationResult} indicating the outcome of the validation process. Returns a valid result + * if the authorization details are correct and meet the criteria, otherwise returns an invalid result with an + * appropriate error message. + * @throws AuthorizationDetailsProcessingException if the validation fails and the authorization flow needs + * to be interrupted. + * @see AuthorizationDetailsContext + * @see ValidationResult + */ + ValidationResult validate(AuthorizationDetailsContext authorizationDetailsContext) + throws AuthorizationDetailsProcessingException; + + /** + * Retrieves the type of authorization details handled by this provider. + *

+ * Each implementation should return a unique type identifier that represents the kind of + * authorization details it processes. This identifier is used to differentiate between + * various providers in a service-oriented architecture. + *

+ * + * @return a {@code String} representing the type of authorization details managed by this provider + * @see AuthorizationDetail#getType() + */ + String getType(); + + // The existing authorization detail that was previously accepted by the resource owner. + boolean isEqualOrSubset(AuthorizationDetail requestedAuthorizationDetail, + AuthorizationDetails existingAuthorizationDetails); + + /** + *

+ * This method is invoked prior to presenting the consent UI to the user. Its purpose is to + * enhance or augment the authorization details, providing additional context or information + * that may be necessary for informed consent. This may include adding more descriptive + * information, default values, or other relevant details that are crucial for the user to + * understand the authorization request fully. + *

+ *

+ * It is also a responsibility of this method to generate a human-readable consent + * description from the provided authorization details, which will be displayed to the user for approval. + * The consent description should provide a clear, human-readable summary of the {@code authorization_details} + * object. + *

+ *

+ * This enrichment process aligns with the concepts outlined in + * RFC 9396, + * which describes the requirements for enriched authorization details to ensure clarity and transparency + * in consent management. + *

+ * + * @param authorizationDetailsContext the context containing the authorization details to be enriched. + * @return an enriched {@code AuthorizationDetail} object with additional information or context. + * This enriched object is intended to provide users with a clearer understanding of the + * authorization request when they are presented with the consent form. + * @see AuthorizationDetailsContext + * @see AuthorizationDetail + * @see AuthorizationDetail#setConsentDescription + * @see + * Enriched Authorization Details + */ + AuthorizationDetail enrich(AuthorizationDetailsContext authorizationDetailsContext); +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProviderFactory.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProviderFactory.java new file mode 100644 index 00000000000..123e3cc3dd2 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProviderFactory.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.core; + +import org.wso2.carbon.identity.oauth2.rar.internal.AuthorizationDetailsDataHolder; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * A factory class to manage and provide instances of {@link AuthorizationDetailsProvider} Service Provider Interface. + * This class follows the Singleton pattern to ensure only one instance is created. + * It uses {@link ServiceLoader} to dynamically load and manage {@link AuthorizationDetailsProvider} implementations. + *

Example usage: + *

 {@code
+ * // Get a specific provider by type
+ * AuthorizationDetailsProviderFactory.getInstance()
+ *     .getProviderByType("customer_information")
+ *     .ifPresentOrElse(
+ *         p -> log.debug("Provider for type " + type + ": " + p.getClass().getName()),
+ *         () -> log.debug("No provider found for type " + type)
+ *     );
+ * } 

+ * + * @see AuthorizationDetailsProvider AuthorizationDetailsService + * @see + * Request Parameter "authorization_details" + * @since 7.0.26.9 + */ +public class AuthorizationDetailsProviderFactory { + + private static volatile AuthorizationDetailsProviderFactory instance; + private final Map supportedAuthorizationDetailsTypes; + + /** + * Private constructor to initialize the factory. + *

This constructor is intentionally private to prevent direct instantiation of the + * {@code AuthorizationDetailsProviderFactory} class. + * Instead, use the {@link #getInstance()} method to obtain the singleton instance.

+ */ + private AuthorizationDetailsProviderFactory() { + + this.supportedAuthorizationDetailsTypes = this.loadSupportedAuthorizationDetailsTypes(); + } + + /** + * Loads supported authorization details types from the provided {@link ServiceLoader}. + * + * @return Map of authorization details types with their corresponding services. + */ + private Map loadSupportedAuthorizationDetailsTypes() { + + return AuthorizationDetailsDataHolder.getInstance() + .getAuthorizationDetailsProviders() + .stream() + .collect(Collectors.toMap(AuthorizationDetailsProvider::getType, Function.identity())); + } + + /** + * Provides the singleton instance of {@code AuthorizationDetailsProviderFactory}. + * + * @return Singleton instance of {@code AuthorizationDetailsProviderFactory}. + */ + public static AuthorizationDetailsProviderFactory getInstance() { + + if (instance == null) { + synchronized (AuthorizationDetailsProviderFactory.class) { + if (instance == null) { + instance = new AuthorizationDetailsProviderFactory(); + } + } + } + return instance; + } + + /** + * Returns the {@link AuthorizationDetailsProvider} provider for the given type. + * + * @param type A supported authorization details type. + * @return {@link Optional} containing the {@link AuthorizationDetailsProvider} if present, otherwise empty. + * @see AuthorizationDetailsProvider#getType() getAuthorizationDetailsType + */ + public Optional getProviderByType(final String type) { + + return Optional.ofNullable(this.supportedAuthorizationDetailsTypes.get(type)); + } + + /** + * Checks if a given type has a valid service provider implementation. + * + * @param type The type to check. + * @return {@code true} if the type is supported, {@code false} otherwise. + * @see AuthorizationDetailsProvider AuthorizationDetailsService + */ + public boolean isSupportedAuthorizationDetailsType(final String type) { + + return this.supportedAuthorizationDetailsTypes.containsKey(type); + } + + /** + * Returns a {@link Collections#unmodifiableSet} of all supported authorization details types. + *

To be included as a supported authorization details type, there must be a custom implementation + * of the {@link AuthorizationDetailsProvider} Service Provider Interface (SPI) available in the classpath + * for the specified type.

+ * + * @return A set of supported authorization details types. + */ + public Set getSupportedAuthorizationDetailTypes() { + + return Collections.unmodifiableSet(this.supportedAuthorizationDetailsTypes.keySet()); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/exception/AuthorizationDetailsProcessingException.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/exception/AuthorizationDetailsProcessingException.java new file mode 100644 index 00000000000..62a65d9859c --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/exception/AuthorizationDetailsProcessingException.java @@ -0,0 +1,30 @@ +package org.wso2.carbon.identity.oauth2.rar.exception; + +/** + * + */ +public class AuthorizationDetailsProcessingException extends RuntimeException { + + private static final long serialVersionUID = -206212512259482200L; + + /** + * Constructs a new exception with an error message. + * + * @param message The detail message. + */ + public AuthorizationDetailsProcessingException(String message) { + + super(message); + } + + /** + * Constructs a new exception with the message and cause. + * + * @param message The detail message. + * @param cause The cause. + */ + public AuthorizationDetailsProcessingException(String message, Throwable cause) { + + super(message, cause); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/handler/RARAccessTokenResponseHandler.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/handler/RARAccessTokenResponseHandler.java new file mode 100644 index 00000000000..a35e8bd0ae5 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/handler/RARAccessTokenResponseHandler.java @@ -0,0 +1,61 @@ +package org.wso2.carbon.identity.oauth2.rar.handler; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONArray; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.token.handlers.response.AccessTokenResponseHandler; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + */ +public class RARAccessTokenResponseHandler implements AccessTokenResponseHandler { + private static final ObjectMapper OBJECT_MAPPER = new JsonMapper(); + private static final Log LOG = LogFactory.getLog(RARAccessTokenResponseHandler.class); + + @Override +// public Map getAdditionalTokenResponseAttributes(OAuthTokenReqMessageContext tokReqMsgCtx) +// throws IdentityOAuth2Exception { +// +// List authorizationDetails = AuthorizationDetailService.getInstance() +// .getConsentedAuthorizationDetailsByAuthzCode(tokReqMsgCtx.getOauth2AccessTokenReqDTO() +// .getAuthorizationCode()); +// +// Map additionalAttributes = new HashMap<>(); +// if (isNotEmptyList(authorizationDetails)) { +// additionalAttributes.put(AUTHORIZATION_DETAILS, convertToJsonArray(authorizationDetails)); +// } +// return additionalAttributes; +// } + + public Map getAdditionalTokenResponseAttributes(OAuthTokenReqMessageContext tokReqMsgCtx) + throws IdentityOAuth2Exception { + + Map additionalAttributes = new HashMap<>(); + + return additionalAttributes; + } + + + private boolean isNotEmptyList(List list) { + return list != null && !list.isEmpty(); + } + + private JSONArray convertToJsonArray(List authorizationDetails) { + try { + return OBJECT_MAPPER.convertValue(OBJECT_MAPPER.writeValueAsString(authorizationDetails), JSONArray.class); + } catch (JsonProcessingException e) { + LOG.error("Serialization error. Caused by, ", e); + } + return new JSONArray(authorizationDetails); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsDataHolder.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsDataHolder.java new file mode 100644 index 00000000000..bd0c92f5f3f --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsDataHolder.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.internal; + +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProvider; + +import java.util.HashSet; +import java.util.Set; + +/** + * Singleton class that holds rich authorization details data. + *

This class uses the singleton design pattern to ensure there is only one instance + * managing the authorization details providers. The instance is lazily initialized + * with double-checked locking to ensure thread safety.

+ *

The class provides methods to retrieve and set the authorization details data, + * which can be used in different parts of the application to manage rich authorization details.

+ */ +public class AuthorizationDetailsDataHolder { + + private static volatile AuthorizationDetailsDataHolder instance; + private Set authorizationDetailsProviders; + + /** + * Private constructor to prevent instantiation from outside the class. + */ + private AuthorizationDetailsDataHolder() { + + this.authorizationDetailsProviders = new HashSet<>(); + } + + /** + * Returns the singleton instance of {@link AuthorizationDetailsDataHolder}. + * + *

This method uses double-checked locking to ensure that the instance is initialized + * only once and in a thread-safe manner. If the instance is not already created, it + * will be created and returned; otherwise, the existing instance will be returned.

+ * + * @return The singleton instance of {@link AuthorizationDetailsDataHolder}. + */ + public static AuthorizationDetailsDataHolder getInstance() { + + if (instance == null) { + synchronized (AuthorizationDetailsDataHolder.class) { + if (instance == null) { + instance = new AuthorizationDetailsDataHolder(); + } + } + } + return instance; + } + + /** + * Returns the current set of {@link AuthorizationDetailsProvider} instances. + * + *

This method provides access to the authorization details providers. + * The returned set can be used to query or modify the authorization details providers.

+ * + * @return A {@link Set} of {@link AuthorizationDetailsProvider} instances. + */ + public Set getAuthorizationDetailsProviders() { + + return this.authorizationDetailsProviders; + } + + /** + * Sets the set of {@link AuthorizationDetailsProvider} instances to the provided value. + * + *

This method replaces the current set of authorization details providers with the + * provided set. It can be used to update the list of providers that the application + * uses to manage authorization details.

+ * + * @param authorizationDetailsProviders The new {@link Set} of {@link AuthorizationDetailsProvider} instances. + */ + public void setAuthorizationDetailsProviders(Set authorizationDetailsProviders) { + + this.authorizationDetailsProviders = authorizationDetailsProviders; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsServiceComponent.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsServiceComponent.java new file mode 100644 index 00000000000..2cd272dc71d --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/internal/AuthorizationDetailsServiceComponent.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProvider; + +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Authorization Details OSGI service component. + */ +@Component(name = "org.wso2.carbon.identity.oauth.rar.internal.AuthorizationDetailsServiceComponent") +public class AuthorizationDetailsServiceComponent { + private static final Log log = LogFactory.getLog(AuthorizationDetailsServiceComponent.class); + + @Activate + protected void activate(ComponentContext context) { + + AuthorizationDetailsDataHolder.getInstance().setAuthorizationDetailsProviders( + loadAuthorizationDetailsProviders(ServiceLoader.load(AuthorizationDetailsProvider.class, + this.getClass().getClassLoader()))); + + log.debug("AuthorizationDetailsServiceComponent is activated"); + } + + @Deactivate + protected void deactivate(ComponentContext context) { + + log.debug("AuthorizationDetailsServiceComponent is deactivated"); + } + + /** + * Loads supported authorization details providers from the provided {@link ServiceLoader}. + * + * @param serviceLoader {@link ServiceLoader} for {@link AuthorizationDetailsProvider}. + * @return Set of authorization details providers. + */ + private Set loadAuthorizationDetailsProviders( + final ServiceLoader serviceLoader) { + + return StreamSupport.stream(serviceLoader.spliterator(), false) + .collect(Collectors.toSet()); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetailsContext.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetailsContext.java new file mode 100644 index 00000000000..00cd4de10d6 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetailsContext.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.model; + +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetail; + +import java.util.Objects; + +/** + * Represents the context for rich authorization requests in an OAuth2 flow. + *

+ * This class holds relevant details such as OAuth2 parameters, application details, the authenticated user, + * and specific authorization details. It is immutable to ensure that the context remains consistent throughout its use. + *

+ */ +public class AuthorizationDetailsContext { + + private final OAuth2Parameters oAuth2Parameters; + private final OAuthAppDO oAuthAppDO; + private final AuthenticatedUser authenticatedUser; + private final AuthorizationDetail authorizationDetail; + + /** + * Constructs a new {@code AuthorizationDetailsContext}. + *

+ * This constructor ensures that all necessary details for an authorization context are provided. + *

+ * + * @param oAuth2Parameters the OAuth2 parameters. + * @param oAuthAppDO the OAuth application details. + * @param authenticatedUser the authenticated user. + * @param authorizationDetail the specific authorization detail. + * @throws NullPointerException if any of the arguments are {@code null}. + */ + public AuthorizationDetailsContext(final OAuth2Parameters oAuth2Parameters, final OAuthAppDO oAuthAppDO, + final AuthenticatedUser authenticatedUser, + final AuthorizationDetail authorizationDetail) { + this.oAuth2Parameters = Objects.requireNonNull(oAuth2Parameters, "oAuth2Parameters cannot be null"); + this.oAuthAppDO = Objects.requireNonNull(oAuthAppDO, "oAuthAppDO cannot be null"); + this.authenticatedUser = Objects.requireNonNull(authenticatedUser, "authenticatedUser cannot be null"); + this.authorizationDetail = Objects.requireNonNull(authorizationDetail, "authorizationDetail cannot be null"); + } + + /** + * Returns the {@code AuthorizationDetail} instance. + * + * @return the {@link AuthorizationDetail} instance. + */ + public AuthorizationDetail getAuthorizationDetail() { + return this.authorizationDetail; + } + + /** + * Returns the OAuth2 parameters. + * + * @return the {@link OAuth2Parameters} instance. + */ + public OAuth2Parameters getOAuth2Parameters() { + return this.oAuth2Parameters; + } + + /** + * Returns the OAuth application details. + * + * @return the {@link OAuthAppDO} instance. + */ + public OAuthAppDO getoAuthAppDO() { + return this.oAuthAppDO; + } + + /** + * Returns the authenticated user. + * + * @return the {@link AuthenticatedUser} instance. + */ + public AuthenticatedUser getAuthenticatedUser() { + return this.authenticatedUser; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/ValidationResult.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/ValidationResult.java new file mode 100644 index 00000000000..5bf5c9ed0cc --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/ValidationResult.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.model; + +import java.util.Map; + +/** + * Represents the result of authorization details validation operation, encapsulating the status, reason for failure, + * and additional metadata. + *

+ * This class provides a way to create validation results that can be either valid or invalid, with + * optional metadata for further context. + *

+ */ +public class ValidationResult { + + private final boolean status; + private final String reason; + private final Map meta; + + /** + * Constructs a new {@code ValidationResult}. + * + * @param status whether the validation is successful. + * @param reason the reason for validation failure, if applicable. + * @param meta additional metadata related to the validation result. + */ + public ValidationResult(final boolean status, final String reason, final Map meta) { + this.status = status; + this.reason = reason; + this.meta = meta; + } + + /** + * Creates a new {@code ValidationResult} indicating a successful validation. + *

+ * This method should be used to indicate that the validation passed without any issues. + *

+ * + * @return a {@code ValidationResult} indicating a successful validation. + */ + public static ValidationResult valid() { + return new ValidationResult(true, null, null); + } + + /** + * Creates a new {@code ValidationResult} indicating a failed validation with a specified reason. + *

+ * This method should be used to indicate that the validation failed and provide a reason for the failure. + *

+ * + * @param reason the reason why the validation failed. + * @return a {@code ValidationResult} indicating a failed validation. + */ + public static ValidationResult invalid(final String reason) { + return new ValidationResult(false, reason, null); + } + + /** + * Creates a new {@code ValidationResult} indicating a failed validation with a specified reason and metadata. + *

+ * This method should be used to indicate that the validation failed, provide a reason, and include + * additional context or metadata. + *

+ * + * @param reason the reason why the validation failed. + * @param meta additional metadata related to the validation result. + * @return a {@code ValidationResult} indicating a failed validation with metadata. + */ + public static ValidationResult invalid(final String reason, final Map meta) { + return new ValidationResult(false, reason, meta); + } + + /** + * Returns whether the validation was successful. + * + * @return {@code true} if the validation was successful, {@code false} otherwise. + */ + public boolean isValid() { + return this.status; + } + + /** + * Returns whether the validation failed. + * + * @return {@code true} if the validation failed, {@code false} otherwise. + */ + public boolean isInvalid() { + return !this.isValid(); + } + + /** + * Returns the reason for validation failure, if applicable. + * + * @return the reason for validation failure, or {@code null} if the validation was successful. + */ + public String getReason() { + return this.reason; + } + + /** + * Returns additional metadata related to the validation result. + * + * @return an unmodifiable map of metadata, or an empty map if no metadata is present. + */ + public Map getMeta() { + return this.meta; + } + + @Override + public String toString() { + return "ValidationResult{" + + "status=" + status + + ", reason='" + reason + '\'' + + ", meta=" + meta + + '}'; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsUtils.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsUtils.java new file mode 100644 index 00000000000..34af49529c6 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsUtils.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.oauth2.rar.util; + +import org.apache.commons.lang.StringUtils; +import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.common.util.AuthorizationDetailsConstants; + +/** + * Utility class for handling and validating authorization details in OAuth2 requests. + */ +public class AuthorizationDetailsUtils { + + /** + * Determines if the given {@link OAuth2Parameters} object contains + * {@link org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails AuthorizationDetails}. + * + * @param oAuth2Parameters The requested OAuth2 parameters to check. + * @return {@code true} if the OAuth2 parameters contain non-empty authorization details array, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final OAuth2Parameters oAuth2Parameters) { + + return oAuth2Parameters.getAuthorizationDetails() != null && + !oAuth2Parameters.getAuthorizationDetails().getDetails().isEmpty(); + } + + /** + * Determines if the given {@link OAuthAuthzRequest} object contains {@code authorization_details}. + * + * @param oauthRequest The OAuth Authorization Request to check. + * @return {@code true} if the OAuth authorization request contains a non-blank authorization details parameter, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final OAuthAuthzRequest oauthRequest) { + + return StringUtils.isNotBlank(oauthRequest.getParam(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS)); + } +} diff --git a/components/org.wso2.carbon.identity.oauth/pom.xml b/components/org.wso2.carbon.identity.oauth/pom.xml index 68e661c2aa4..92f3e93171e 100644 --- a/components/org.wso2.carbon.identity.oauth/pom.xml +++ b/components/org.wso2.carbon.identity.oauth/pom.xml @@ -258,6 +258,11 @@ org.wso2.orbit.javax.xml.bind jaxb-api
+ + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar.common + provided + org.testng diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/IdentityOAuth2AuthorizationDetailsService.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/IdentityOAuth2AuthorizationDetailsService.java new file mode 100644 index 00000000000..21ac2f67fd4 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/IdentityOAuth2AuthorizationDetailsService.java @@ -0,0 +1,134 @@ +package org.wso2.carbon.identity.oauth2; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; +import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.common.dao.AuthorizationDetailsDAO; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; + +import java.sql.SQLException; +import java.util.Objects; + +/** + * IdentityOAuth2AuthorizationDetailsService is responsible for managing and handling OAuth2 authorization details, + * specifically in the context of rich authorization requests. + *

+ * This class integrates with the {@link AuthorizationDetailsDAO} to persist these details in the underlying data store. + * It also provides utility methods to check if a request contains rich authorization details. + *

+ * + * @see AuthorizationDetailsDAO + * @see AuthorizationDetails + */ +public class IdentityOAuth2AuthorizationDetailsService { + + private static final Log log = LogFactory.getLog(IdentityOAuth2AuthorizationDetailsService.class); + protected final AuthorizationDetailsDAO authorizationDetailsDAO; + + /** + * Default constructor that initializes the service with the default {@link AuthorizationDetailsDAO}. + *

+ * This constructor uses the default DAO provided by the {@link OAuthTokenPersistenceFactory} + * to handle the persistence of authorization details. + *

+ */ + public IdentityOAuth2AuthorizationDetailsService() { + + this(OAuthTokenPersistenceFactory.getInstance().getAuthorizationDetailsDAO()); + } + + /** + * Constructor that initializes the service with a given {@link AuthorizationDetailsDAO}. + * + * @param authorizationDetailsDAO The {@link AuthorizationDetailsDAO} instance to be used for + * handling authorization details persistence. Must not be {@code null}. + */ + public IdentityOAuth2AuthorizationDetailsService(final AuthorizationDetailsDAO authorizationDetailsDAO) { + + this.authorizationDetailsDAO = Objects + .requireNonNull(authorizationDetailsDAO, "AuthorizationDetailsDAO must not be null"); + } + + /** + * Determines if the given {@link OAuthAuthzReqMessageContext} object contains {@link AuthorizationDetails}. + * + * @param oAuthAuthzReqMessageContext The requested OAuthAuthzReqMessageContext to check. + * @return {@code true} if the OAuthAuthzReqMessageContext contains non-empty authorization details set, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext) { + + return isRichAuthorizationRequest(oAuthAuthzReqMessageContext.getAuthorizationDetails()); + } + + /** + * Determines if the request is a rich authorization request using provided {@link AuthorizationDetails} object. + *

+ * This method checks if the specified {@link AuthorizationDetails} instance is not {@code null} + * and has a non-empty details set. + * + * @param authorizationDetails The {@link AuthorizationDetails} to check. + * @return {@code true} if the {@link AuthorizationDetails} is not {@code null} and has a non-empty details set, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final AuthorizationDetails authorizationDetails) { + + return authorizationDetails != null && !authorizationDetails.getDetails().isEmpty(); + } + + /** + * Determines if the given {@link OAuth2Parameters} object contains {@link AuthorizationDetails}. + * + * @param oAuth2Parameters The requested OAuth2Parameters to check. + * @return {@code true} if the OAuth2Parameters contains non-empty authorization details set, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final OAuth2Parameters oAuth2Parameters) { + + return isRichAuthorizationRequest(oAuth2Parameters.getAuthorizationDetails()); + } + + /** + * Stores the OAuth2 code authorization details if the request is a rich authorization request. + *

+ * This method checks whether the given {@link OAuthAuthzReqMessageContext} contains {@link AuthorizationDetails}. + * If it does, it retrieves the tenant ID from the request context and stores the authorization + * details using the {@link AuthorizationDetailsDAO}. + *

+ * + * @param authzCodeDO The {@link AuthzCodeDO} object containing the authorization code details. + * @param oAuthAuthzReqMessageContext The {@link OAuthAuthzReqMessageContext} containing the request context. + * @throws IdentityOAuth2Exception If an error occurs while storing the authorization details. + */ + public void storeOAuth2CodeAuthorizationDetails(final AuthzCodeDO authzCodeDO, + final OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext) + throws IdentityOAuth2Exception { + + if (!isRichAuthorizationRequest(oAuthAuthzReqMessageContext)) { + log.debug("Request is not a rich authorization request. Skipping storage of code authorization details."); + return; + } + + try { + final int tenantID = OAuth2Util.getTenantId( + oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getTenantDomain()); + // Storing the authorization details. + this.authorizationDetailsDAO.addOAuth2CodeAuthorizationDetails( + authzCodeDO.getAuthzCodeId(), + oAuthAuthzReqMessageContext.getAuthorizationDetails(), + tenantID); + + if (log.isDebugEnabled()) { + log.debug("Successfully stored OAuth2 Code authorization details for code Id: " + + authzCodeDO.getAuthzCodeId()); + } + } catch (SQLException e) { + log.error("Error occurred while storing OAuth2 Code authorization details. Caused by, ", e); + throw new IdentityOAuth2Exception("Error occurred while storing authorization details", e); + } + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/AuthorizationHandlerManager.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/AuthorizationHandlerManager.java index c2c0d44ecfe..a654475ddb4 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/AuthorizationHandlerManager.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/AuthorizationHandlerManager.java @@ -676,7 +676,7 @@ private boolean isInvalidResponseType(OAuth2AuthorizeReqDTO authzReqDTO, OAuth2A return false; } - private OAuthAppDO getAppInformation(OAuth2AuthorizeReqDTO authzReqDTO) throws IdentityOAuth2Exception, + public OAuthAppDO getAppInformation(OAuth2AuthorizeReqDTO authzReqDTO) throws IdentityOAuth2Exception, InvalidOAuthClientException { OAuthAppDO oAuthAppDO = AppInfoCache.getInstance().getValueFromCache(authzReqDTO.getConsumerKey()); if (oAuthAppDO != null) { diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java index 35d57287f70..2df67714c2e 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/OAuthAuthzReqMessageContext.java @@ -19,6 +19,7 @@ package org.wso2.carbon.identity.oauth2.authz; import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeReqDTO; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; import java.io.Serializable; import java.util.Properties; @@ -56,6 +57,8 @@ public class OAuthAuthzReqMessageContext implements Serializable { private Properties properties = new Properties(); + private AuthorizationDetails authorizationDetails; + public OAuthAuthzReqMessageContext(OAuth2AuthorizeReqDTO authorizationReqDTO) { this.authorizationReqDTO = authorizationReqDTO; @@ -212,4 +215,26 @@ public void setSubjectTokenFlow(boolean subjectTokenFlow) { isSubjectTokenFlow = subjectTokenFlow; } + + /** + * Retrieves the current authorization details. + * + * @return the {@link AuthorizationDetails} instance representing the current authorization information. + * If no authorization details are available, it will return {@code null}. + */ + public AuthorizationDetails getAuthorizationDetails() { + + return this.authorizationDetails; + } + + /** + * Sets the authorization details. + * This method updates the authorization details with the provided {@link AuthorizationDetails} instance. + * + * @param authorizationDetails the {@link AuthorizationDetails} to set. + */ + public void setAuthorizationDetails(final AuthorizationDetails authorizationDetails) { + + this.authorizationDetails = authorizationDetails; + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/AbstractResponseTypeHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/AbstractResponseTypeHandler.java index bc1b5a78bb0..287d2d8b57c 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/AbstractResponseTypeHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/AbstractResponseTypeHandler.java @@ -32,6 +32,7 @@ import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2AuthorizationDetailsService; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeReqDTO; @@ -59,6 +60,7 @@ public abstract class AbstractResponseTypeHandler implements ResponseTypeHandler protected boolean cacheEnabled; protected OAuthCache oauthCache; private OAuthCallbackManager callbackManager; + protected IdentityOAuth2AuthorizationDetailsService identityOAuth2AuthorizationDetailsService; @Override public void init() throws IdentityOAuth2Exception { @@ -68,6 +70,7 @@ public void init() throws IdentityOAuth2Exception { if (cacheEnabled) { oauthCache = OAuthCache.getInstance(); } + this.identityOAuth2AuthorizationDetailsService = new IdentityOAuth2AuthorizationDetailsService(); } @Override diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/CodeResponseTypeHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/CodeResponseTypeHandler.java index adb9dfefd11..489226250f3 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/CodeResponseTypeHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/CodeResponseTypeHandler.java @@ -47,6 +47,8 @@ public OAuth2AuthorizeRespDTO issue(OAuthAuthzReqMessageContext oauthAuthzMsgCtx AuthzCodeDO authorizationCode = ResponseTypeHandlerUtil.generateAuthorizationCode(oauthAuthzMsgCtx, cacheEnabled); + super.identityOAuth2AuthorizationDetailsService + .storeOAuth2CodeAuthorizationDetails(authorizationCode, oauthAuthzMsgCtx); String sessionDataKey = oauthAuthzMsgCtx.getAuthorizationReqDTO().getSessionDataKey(); if (log.isDebugEnabled()) { diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dao/OAuthTokenPersistenceFactory.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dao/OAuthTokenPersistenceFactory.java index c90c4285e63..68aa46f7d82 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dao/OAuthTokenPersistenceFactory.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dao/OAuthTokenPersistenceFactory.java @@ -21,6 +21,8 @@ package org.wso2.carbon.identity.oauth2.dao; import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder; +import org.wso2.carbon.identity.oauth2.rar.common.dao.AuthorizationDetailsDAO; +import org.wso2.carbon.identity.oauth2.rar.common.dao.AuthorizationDetailsDAOImpl; import org.wso2.carbon.identity.openidconnect.dao.CacheBackedScopeClaimMappingDAOImpl; import org.wso2.carbon.identity.openidconnect.dao.RequestObjectDAO; import org.wso2.carbon.identity.openidconnect.dao.RequestObjectDAOImpl; @@ -40,6 +42,7 @@ public class OAuthTokenPersistenceFactory { private ScopeClaimMappingDAO scopeClaimMappingDAO; private TokenBindingMgtDAO tokenBindingMgtDAO; private OAuthUserConsentedScopesDAO oauthUserConsentedScopesDAO; + private final AuthorizationDetailsDAO authorizationDetailsDAO; public OAuthTokenPersistenceFactory() { @@ -51,6 +54,7 @@ public OAuthTokenPersistenceFactory() { this.scopeClaimMappingDAO = new CacheBackedScopeClaimMappingDAOImpl(); this.tokenBindingMgtDAO = new TokenBindingMgtDAOImpl(); this.oauthUserConsentedScopesDAO = new CacheBackedOAuthUserConsentedScopesDAOImpl(); + this.authorizationDetailsDAO = new AuthorizationDetailsDAOImpl(); } public static OAuthTokenPersistenceFactory getInstance() { @@ -107,4 +111,17 @@ public OAuthUserConsentedScopesDAO getOAuthUserConsentedScopesDAO() { return oauthUserConsentedScopesDAO; } + + /** + * Retrieves the DAO for authorization details. + *

+ * This method returns an {@link AuthorizationDetailsDAO} singleton instance that provides access to the + * {@link org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails} data. This DAO is used to interact + * with the underlying data store to fetch and manipulate authorization information. + *

+ * @return the {@link AuthorizationDetailsDAO} instance that provides access to authorization details data. + */ + public AuthorizationDetailsDAO getAuthorizationDetailsDAO() { + return this.authorizationDetailsDAO; + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/OAuth2Parameters.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/OAuth2Parameters.java index 5bc77e5bea6..868a5ff1640 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/OAuth2Parameters.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/OAuth2Parameters.java @@ -18,6 +18,8 @@ package org.wso2.carbon.identity.oauth2.model; +import org.wso2.carbon.identity.oauth2.rar.common.model.AuthorizationDetails; + import java.io.Serializable; import java.util.LinkedHashSet; import java.util.Set; @@ -55,6 +57,7 @@ public class OAuth2Parameters implements Serializable { private boolean isRequestObjectFlow; private boolean isMtlsRequest; private String requestedSubjectId; + private AuthorizationDetails authorizationDetails; public String getRequestedSubjectId() { @@ -328,4 +331,26 @@ public void setIsMtlsRequest(boolean isMtlsRequest) { this.isMtlsRequest = isMtlsRequest; } + + /** + * Retrieves the current authorization details. + * + * @return the {@link AuthorizationDetails} instance representing the current authorization information. + * If no authorization details are available, it will return {@code null}. + */ + public AuthorizationDetails getAuthorizationDetails() { + + return this.authorizationDetails; + } + + /** + * Sets the authorization details. + * This method updates the authorization details with the provided {@link AuthorizationDetails} instance. + * + * @param authorizationDetails the {@link AuthorizationDetails} to set. + */ + public void setAuthorizationDetails(final AuthorizationDetails authorizationDetails) { + + this.authorizationDetails = authorizationDetails; + } } diff --git a/pom.xml b/pom.xml index 93aaedf6c6f..278ca1bfc96 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,8 @@ features/org.wso2.carbon.identity.oauth.server.feature features/org.wso2.carbon.identity.oauth.ui.feature features/org.wso2.carbon.identity.oauth.dcr.server.feature + components/org.wso2.carbon.identity.oauth.rar.common + components/org.wso2.carbon.identity.oauth.rar @@ -402,6 +404,11 @@ gson ${com.google.code.gson.version}
+ + com.google.auto.service + auto-service + ${com.google.auto.service.version} + @@ -547,6 +554,16 @@ org.wso2.carbon.identity.oauth.extension ${project.version} + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar + ${project.version} + + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar.common + ${project.version} + @@ -994,6 +1011,7 @@ 2.4.7 5.2 9.2 + 1.1.1 5.1.2