diff --git a/components/org.wso2.carbon.identity.discovery/pom.xml b/components/org.wso2.carbon.identity.discovery/pom.xml index a3c7dcfef05..b2096af9da1 100644 --- a/components/org.wso2.carbon.identity.discovery/pom.xml +++ b/components/org.wso2.carbon.identity.discovery/pom.xml @@ -52,6 +52,10 @@ org.wso2.carbon.identity.framework org.wso2.carbon.identity.claim.metadata.mgt + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar + org.testng diff --git a/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/DiscoveryConstants.java b/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/DiscoveryConstants.java index a77e6a151fa..76a6c551c29 100644 --- a/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/DiscoveryConstants.java +++ b/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/DiscoveryConstants.java @@ -377,4 +377,11 @@ public class DiscoveryConstants { * Authorization Server. */ public static final String MTLS_ENDPOINT_ALIASES = "mtls_endpoint_aliases"; + + /** + * authorization_details_types_supported. + *

OPTIONAL. JSON array containing the authorization details types the AS supports.

+ * @see rfc9396 + */ + public static final String AUTHORIZATION_DETAILS_TYPES_SUPPORTED = "authorization_details_types_supported"; } diff --git a/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/OIDProviderConfigResponse.java b/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/OIDProviderConfigResponse.java index d249651a23f..78020c06d42 100644 --- a/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/OIDProviderConfigResponse.java +++ b/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/OIDProviderConfigResponse.java @@ -85,6 +85,7 @@ public class OIDProviderConfigResponse { private Boolean tlsClientCertificateBoundAccessTokens; private String mtlsTokenEndpoint; private String mtlsPushedAuthorizationRequestEndpoint; + private String[] authorizationDetailsTypesSupported; private static final String MUTUAL_TLS_ALIASES_ENABLED = "OAuth.MutualTLSAliases.Enabled"; @@ -530,6 +531,16 @@ public void setMtlsPushedAuthorizationRequestEndpoint(String mtlsPushedAuthoriza this.mtlsPushedAuthorizationRequestEndpoint = mtlsPushedAuthorizationRequestEndpoint; } + public String[] getAuthorizationDetailsTypesSupported() { + + return this.authorizationDetailsTypesSupported; + } + + public void setAuthorizationDetailsTypesSupported(String[] authorizationDetailsTypesSupported) { + + this.authorizationDetailsTypesSupported = authorizationDetailsTypesSupported; + } + public Map getConfigMap() { Map configMap = new HashMap(); configMap.put(DiscoveryConstants.ISSUER.toLowerCase(), this.issuer); @@ -604,6 +615,8 @@ public Map getConfigMap() { this.mtlsPushedAuthorizationRequestEndpoint); configMap.put(DiscoveryConstants.MTLS_ENDPOINT_ALIASES, mtlsAliases); } + configMap.put(DiscoveryConstants.AUTHORIZATION_DETAILS_TYPES_SUPPORTED, + this.authorizationDetailsTypesSupported); return configMap; } } diff --git a/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilder.java b/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilder.java index 7b6f78946ab..c44c6896839 100644 --- a/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilder.java +++ b/components/org.wso2.carbon.identity.discovery/src/main/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilder.java @@ -32,6 +32,7 @@ import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; import org.wso2.carbon.identity.oauth2.OAuth2Constants; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessorFactory; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import java.net.URISyntaxException; @@ -152,6 +153,13 @@ public OIDProviderConfigResponse buildOIDProviderConfig(OIDProviderRequest reque .contains(OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER)); providerConfig.setMtlsTokenEndpoint(OAuth2Util.OAuthURL.getOAuth2MTLSTokenEPUrl()); providerConfig.setMtlsPushedAuthorizationRequestEndpoint(OAuth2Util.OAuthURL.getOAuth2MTLSParEPUrl()); + + final Set authorizationDetailTypes = AuthorizationDetailsProcessorFactory.getInstance() + .getSupportedAuthorizationDetailTypes(); + if (authorizationDetailTypes != null && !authorizationDetailTypes.isEmpty()) { + providerConfig + .setAuthorizationDetailsTypesSupported(authorizationDetailTypes.stream().toArray(String[]::new)); + } return providerConfig; } } diff --git a/components/org.wso2.carbon.identity.discovery/src/test/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilderTest.java b/components/org.wso2.carbon.identity.discovery/src/test/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilderTest.java index 0f25922434d..f5f14813a84 100644 --- a/components/org.wso2.carbon.identity.discovery/src/test/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilderTest.java +++ b/components/org.wso2.carbon.identity.discovery/src/test/java/org/wso2/carbon/identity/discovery/builders/ProviderConfigBuilderTest.java @@ -37,14 +37,17 @@ import org.wso2.carbon.identity.discovery.internal.OIDCDiscoveryDataHolder; import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessorFactory; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; @@ -84,7 +87,9 @@ public void testBuildOIDProviderConfig() throws Exception { OAuthServerConfiguration mockOAuthServerConfiguration = mock(OAuthServerConfiguration.class); oAuthServerConfiguration.when( OAuthServerConfiguration::getInstance).thenReturn(mockOAuthServerConfiguration); - try (MockedStatic oAuth2Util = mockStatic(OAuth2Util.class);) { + try (MockedStatic oAuth2Util = mockStatic(OAuth2Util.class); + MockedStatic factoryMockedStatic = + mockStatic(AuthorizationDetailsProcessorFactory.class)) { OIDCDiscoveryDataHolder mockOidcDiscoveryDataHolder = spy(new OIDCDiscoveryDataHolder()); mockOidcDiscoveryDataHolder.setClaimManagementService(mockClaimMetadataManagementService); @@ -107,6 +112,11 @@ public void testBuildOIDProviderConfig() throws Exception { .thenReturn(JWSAlgorithm.RS256); when(mockOidProviderRequest.getTenantDomain()).thenReturn( MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + + AuthorizationDetailsProcessorFactory factoryMock = spy(AuthorizationDetailsProcessorFactory.class); + doReturn(Collections.emptySet()).when(factoryMock).getSupportedAuthorizationDetailTypes(); + factoryMockedStatic.when(AuthorizationDetailsProcessorFactory::getInstance).thenReturn(factoryMock); + assertNotNull(providerConfigBuilder.buildOIDProviderConfig(mockOidProviderRequest)); } } @@ -194,7 +204,9 @@ public void testBuildOIDProviderConfig4() throws Exception { MockedStatic oidcDiscoveryDataHolder = mockStatic(OIDCDiscoveryDataHolder.class); MockedStatic oAuth2Util = mockStatic(OAuth2Util.class); - MockedStatic discoveryUtil = mockStatic(DiscoveryUtil.class);) { + MockedStatic discoveryUtil = mockStatic(DiscoveryUtil.class); + MockedStatic factoryMockedStatic = + mockStatic(AuthorizationDetailsProcessorFactory.class)) { OAuthServerConfiguration mockOAuthServerConfiguration = mock(OAuthServerConfiguration.class); oAuthServerConfiguration.when( OAuthServerConfiguration::getInstance).thenReturn(mockOAuthServerConfiguration); @@ -223,9 +235,14 @@ public void testBuildOIDProviderConfig4() throws Exception { when(mockOidProviderRequest.getTenantDomain()).thenReturn(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); when(mockOAuthServerConfiguration.getUserInfoJWTSignatureAlgorithm()).thenReturn(idTokenSignatureAlgorithm); + AuthorizationDetailsProcessorFactory factoryMock = spy(AuthorizationDetailsProcessorFactory.class); + doReturn(Collections.singleton("test_type")).when(factoryMock).getSupportedAuthorizationDetailTypes(); + factoryMockedStatic.when(AuthorizationDetailsProcessorFactory::getInstance).thenReturn(factoryMock); + OIDProviderConfigResponse response = providerConfigBuilder.buildOIDProviderConfig(mockOidProviderRequest); assertNotNull(response); assertEquals(response.getIssuer(), dummyIdIssuer); + assertEquals(response.getAuthorizationDetailsTypesSupported()[0], "test_type"); } } diff --git a/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java b/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java index f4466f49f57..cb17142c3e3 100644 --- a/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java +++ b/components/org.wso2.carbon.identity.oauth.common/src/main/java/org/wso2/carbon/identity/oauth/common/OAuthConstants.java @@ -767,6 +767,9 @@ public static class ActionIDs { public static final String VALIDATE_EXISTING_CONSENT = "validate-existing-consent"; public static final String GENERATE_INTROSPECTION_RESPONSE = "generate-introspect-response"; public static final String RECEIVE_REVOKE_REQUEST = "receive-revoke-request"; + public static final String VALIDATE_AUTHORIZATION_DETAILS = "validate-authorization-details"; + public static final String VALIDATE_AUTHORIZATION_DETAILS_BEFORE_CONSENT + = "validate-authorization-details-before-consent"; } /** @@ -787,6 +790,7 @@ public static class InputKeys { public static final String PROMPT = "prompt"; public static final String APP_STATE = "app state"; public static final String IMPERSONATOR = "impersonator"; + public static final String REQUESTED_AUTHORIZATION_DETAILS = "requested authorization details"; } /** diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml b/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml index 1c4f3c90e4a..2c1ebf3912e 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml +++ b/components/org.wso2.carbon.identity.oauth.endpoint/pom.xml @@ -184,6 +184,10 @@ jackson-core provided
+ + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar + 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 c6724c68cdb..4d48df18d36 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 @@ -116,6 +116,7 @@ import org.wso2.carbon.identity.oauth2.IdentityOAuth2ClientException; import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; import org.wso2.carbon.identity.oauth2.IdentityOAuth2ScopeException; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException; import org.wso2.carbon.identity.oauth2.IdentityOAuth2UnauthorizedScopeException; import org.wso2.carbon.identity.oauth2.OAuth2Service; import org.wso2.carbon.identity.oauth2.RequestObjectException; @@ -134,6 +135,11 @@ 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.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; +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; @@ -281,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(); @@ -305,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 @@ -1650,6 +1669,7 @@ private String handleUserConsent(OAuthMessage oAuthMessage, String consent, OIDC oAuthMessage.getSessionDataCacheEntry().getAuthzReqMsgCtx(); oAuthAuthzReqMessageContext.setAuthorizationReqDTO(authzReqDTO); oAuthAuthzReqMessageContext.addProperty(OAuthConstants.IS_MTLS_REQUEST, oauth2Params.isMtlsRequest()); + oAuthAuthzReqMessageContext.setApprovedAuthorizationDetails(oauth2Params.getAuthorizationDetails()); // authorizing the request OAuth2AuthorizeRespDTO authzRespDTO = authorize(oAuthAuthzReqMessageContext); if (authzRespDTO != null && authzRespDTO.getCallbackURI() != null) { @@ -1746,10 +1766,17 @@ private void storeUserConsent(OAuthMessage oAuthMessage, String consent) throws if (approvedAlways) { OpenIDConnectUserRPStore.getInstance().putUserRPToStore(loggedInUser, applicationName, true, clientId); + final AuthorizationDetails userConsentedAuthorizationDetails = AuthorizationDetailsUtils + .extractAuthorizationDetailsFromRequest(oAuthMessage.getRequest(), oauth2Params); + if (hasPromptContainsConsent(oauth2Params)) { EndpointUtil.storeOAuthScopeConsent(loggedInUser, oauth2Params, true); + authorizationDetailsService.replaceUserConsentedAuthorizationDetails(loggedInUser, + clientId, oauth2Params, userConsentedAuthorizationDetails); } else { EndpointUtil.storeOAuthScopeConsent(loggedInUser, oauth2Params, false); + authorizationDetailsService.storeOrUpdateUserConsentedAuthorizationDetails(loggedInUser, + clientId, oauth2Params, userConsentedAuthorizationDetails); } } } @@ -1889,6 +1916,7 @@ private OAuthResponse handleSuccessAuthorization(OAuthMessage oAuthMessage, OIDC if (isResponseTypeNotIdTokenOrNone(responseType, authzRespDTO)) { setAccessToken(authzRespDTO, builder, authorizationResponseDTO); setScopes(authzRespDTO, builder, authorizationResponseDTO); + setAuthorizationDetails(authzRespDTO, builder, authorizationResponseDTO); } if (isSubjectTokenFlow(responseType, authzRespDTO)) { setSubjectToken(authzRespDTO, builder, authorizationResponseDTO); @@ -2630,6 +2658,13 @@ 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); @@ -3033,6 +3068,22 @@ private String doUserAuthorization(OAuthMessage oAuthMessage, String sessionData return handleAuthorizationFailureBeforeConsent(oAuthMessage, oauth2Params, authorizeRespDTO); } + try { + validateAuthorizationDetailsBeforeConsent(oAuthMessage, oauth2Params); + } 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)) { @@ -3726,10 +3777,14 @@ 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); + } catch (IdentityOAuth2Exception e) { + throw new OAuthSystemException("Error occurred while checking user has already approved the consent " + + "required authorization details.", e); } } @@ -3823,6 +3878,7 @@ private OAuth2AuthorizeReqDTO buildAuthRequest(OAuth2Parameters oauth2Params, Se authzReqDTO.setHttpServletRequestWrapper(new HttpServletRequestWrapper(request)); authzReqDTO.setRequestedSubjectId(oauth2Params.getRequestedSubjectId()); authzReqDTO.setMappedRemoteClaims(sessionDataCacheEntry.getMappedRemoteClaims()); + authzReqDTO.setAuthorizationDetails(oauth2Params.getAuthorizationDetails()); if (sessionDataCacheEntry.getParamMap() != null && sessionDataCacheEntry.getParamMap().get(OAuthConstants .AMR) != null) { @@ -4834,4 +4890,86 @@ private String addServiceProviderIdToRedirectURI(String redirectURI, String serv } return redirectURI; } + + /** + * 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. + * @throws AuthorizationDetailsProcessingException If there is an error processing the authorization details. + * @throws OAuthSystemException If there is an error during the validation process. + */ + private void validateAuthorizationDetailsBeforeConsent(final OAuthMessage oAuthMessage, + final OAuth2Parameters oAuth2Parameters) + throws AuthorizationDetailsProcessingException, OAuthSystemException { + + if (!AuthorizationDetailsUtils.isRichAuthorizationRequest(oAuth2Parameters)) { + log.debug("Authorization request is not a rich authorization request. Skipping validation."); + return; + } + + try { + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + OAuthConstants.LogConstants.OAUTH_INBOUND_SERVICE, + OAuthConstants.LogConstants.ActionIDs.VALIDATE_AUTHORIZATION_DETAILS_BEFORE_CONSENT); + diagnosticLogBuilder.inputParam(LogConstants.InputKeys.CLIENT_ID, oAuth2Parameters.getClientId()) + .inputParam(LogConstants.InputKeys.APPLICATION_NAME, oAuth2Parameters.getApplicationName()) + .inputParam("authorization details to be validated", + oAuth2Parameters.getAuthorizationDetails().toSet()) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) + .resultMessage("authorization details validation started") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + + final OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext = + oAuthMessage.getSessionDataCacheEntry().getAuthzReqMsgCtx(); + + // Validate the authorization details + final AuthorizationDetails validatedAuthorizationDetails = OAuth2ServiceComponentHolder.getInstance() + .getAuthorizationDetailsValidator() + .getValidatedAuthorizationDetails(oAuthAuthzReqMessageContext); + + // Update the authorization message context with validated authorization details + oAuthAuthzReqMessageContext.setRequestedAuthorizationDetails(AuthorizationDetailsUtils + .assignUniqueIDsToAuthorizationDetails(validatedAuthorizationDetails)); + oAuthMessage.getSessionDataCacheEntry().setAuthzReqMsgCtx(oAuthAuthzReqMessageContext); + + // update oAuth2Parameters with validated authorization details + oAuth2Parameters.setAuthorizationDetails(oAuthAuthzReqMessageContext.getRequestedAuthorizationDetails()); + + if (LoggerUtils.isDiagnosticLogsEnabled()) { + DiagnosticLog.DiagnosticLogBuilder diagnosticLogBuilder = new DiagnosticLog.DiagnosticLogBuilder( + OAuthConstants.LogConstants.OAUTH_INBOUND_SERVICE, + OAuthConstants.LogConstants.ActionIDs.VALIDATE_AUTHORIZATION_DETAILS_BEFORE_CONSENT); + diagnosticLogBuilder.inputParam(LogConstants.InputKeys.CLIENT_ID, oAuth2Parameters.getClientId()) + .inputParam(LogConstants.InputKeys.APPLICATION_NAME, oAuth2Parameters.getApplicationName()) + .inputParam("authorization details after validation", + oAuth2Parameters.getAuthorizationDetails().toSet()) + .resultStatus(DiagnosticLog.ResultStatus.SUCCESS) + .resultMessage("authorization details validation completed") + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION); + LoggerUtils.triggerDiagnosticLogEvent(diagnosticLogBuilder); + } + } catch (IdentityOAuth2ServerException e) { + log.error("Error occurred while validating authorization details. Caused by, ", e); + throw new OAuthSystemException("Error occurred while validating requested authorization details", e); + } + } + + private void setAuthorizationDetails(final OAuth2AuthorizeRespDTO oAuth2AuthorizeRespDTO, + final OAuthASResponse.OAuthAuthorizationResponseBuilder builder, + final AuthorizationResponseDTO authorizationResponseDTO) { + + final AuthorizationDetails authorizationDetails = oAuth2AuthorizeRespDTO.getAuthorizationDetails(); + builder.setParam(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS, + AuthorizationDetailsUtils.getUrlEncodedAuthorizationDetails(authorizationDetails)); + authorizationResponseDTO.getSuccessResponseDTO().setAuthorizationDetails(authorizationDetails); + } } diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/factory/AuthorizationDetailsServiceFactory.java b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/factory/AuthorizationDetailsServiceFactory.java new file mode 100644 index 00000000000..af195571c7e --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/factory/AuthorizationDetailsServiceFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, 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.oauth.endpoint.factory; + +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; + +/** + * This class is used to register AuthorizationDetailsService as a factory bean. + */ +public class AuthorizationDetailsServiceFactory extends AbstractFactoryBean { + + private AuthorizationDetailsService authorizationDetailsService; + + @Override + public Class getObjectType() { + + return AuthorizationDetailsService.class; + } + + @Override + protected AuthorizationDetailsService createInstance() throws Exception { + + if (this.authorizationDetailsService == null) { + this.authorizationDetailsService = + OAuth2ServiceComponentHolder.getInstance().getAuthorizationDetailsService(); + } + return this.authorizationDetailsService; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/token/OAuth2TokenEndpoint.java b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/token/OAuth2TokenEndpoint.java index acff55c6e87..a7176750d10 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/token/OAuth2TokenEndpoint.java +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/token/OAuth2TokenEndpoint.java @@ -47,6 +47,9 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO; import org.wso2.carbon.identity.oauth2.model.CarbonOAuthTokenRequest; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; import org.wso2.carbon.identity.oauth2.token.handlers.response.OAuth2TokenResponse; import org.wso2.carbon.user.core.util.UserCoreUtil; import org.wso2.carbon.utils.DiagnosticLog; @@ -424,6 +427,19 @@ private OAuth2AccessTokenReqDTO buildAccessTokenReqDTO(CarbonOAuthTokenRequest o tokenReqDTO.setWindowsToken(oauthRequest.getWindowsToken()); } tokenReqDTO.addAuthenticationMethodReference(grantType); + + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(oauthRequest)) { + final String encodedAuthorizationDetailsJson = oauthRequest + .getParam(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS); + final String authorizationDetailsJson = AuthorizationDetailsUtils + .getUrlDecodedAuthorizationDetails(encodedAuthorizationDetailsJson); + + if (log.isDebugEnabled()) { + log.debug("Adding requested authorization details to tokenReqDTO: " + authorizationDetailsJson); + } + tokenReqDTO.setAuthorizationDetails(new AuthorizationDetails(authorizationDetailsJson)); + } + return tokenReqDTO; } } 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 2c7fa73ccfb..6d74b5ce8cc 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 @@ -105,6 +105,9 @@ 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.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.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; @@ -872,6 +875,13 @@ 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)) { + additionalQueryParams = additionalQueryParams + "&" + + AuthorizationDetailsConstants.AUTHORIZATION_DETAILS + "=" + AuthorizationDetailsUtils + .getUrlEncodedAuthorizationDetails(filterConsentRequiredAuthorizationDetails(user, params)); + } + // Append scope metadata to additionalQueryParams. String scopeMetadataQueryParam = getScopeMetadataQueryParam(params.getConsentRequiredScopes(), params.getTenantDomain()); @@ -1092,7 +1102,8 @@ public static void storeOAuthScopeConsent(AuthenticatedUser user, OAuth2Paramete } try { Set userApprovedScopesSet = params.getConsentRequiredScopes(); - if (CollectionUtils.isNotEmpty(userApprovedScopesSet)) { + if (CollectionUtils.isNotEmpty(userApprovedScopesSet) || + AuthorizationDetailsUtils.isRichAuthorizationRequest(params)) { if (log.isDebugEnabled()) { log.debug("Storing user consent for approved scopes : " + userApprovedScopesSet.stream() .collect(Collectors.joining(" ")) + " of client : " + params.getClientId()); @@ -2170,4 +2181,26 @@ public static String readRequestBody(HttpServletRequest request, Charset charset } return stringBuilder.toString(); } + + private static AuthorizationDetails filterConsentRequiredAuthorizationDetails( + final AuthenticatedUser authenticatedUser, final OAuth2Parameters oAuth2Parameters) + throws IdentityOAuth2Exception { + + if (authenticatedUser != null && !isPromptContainsConsent(oAuth2Parameters)) { + + final AuthorizationDetails consentRequiredAuthorizationDetails = OAuth2ServiceComponentHolder.getInstance() + .getAuthorizationDetailsService() + .getConsentRequiredAuthorizationDetails(authenticatedUser, oAuth2Parameters); + + if (log.isDebugEnabled()) { + log.debug(String.format("Consent required authorization details for clientId %s and userId %s : %s", + oAuth2Parameters.getClientId(), authenticatedUser.getLoggableMaskedUserId(), + consentRequiredAuthorizationDetails.toJsonString())); + } + return AuthorizationDetailsUtils + .getDisplayableAuthorizationDetails(consentRequiredAuthorizationDetails); + } + return AuthorizationDetailsUtils + .getDisplayableAuthorizationDetails(oAuth2Parameters.getAuthorizationDetails()); + } } diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/webapp/WEB-INF/cxf-servlet.xml index 912d5458a4e..53f5b44d5ab 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/webapp/WEB-INF/cxf-servlet.xml +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -57,6 +57,7 @@ + @@ -103,4 +104,5 @@ + 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 145f6b2795b..aba2d958848 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 @@ -51,6 +51,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import org.testng.collections.Sets; import org.wso2.carbon.base.CarbonBaseConstants; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.identity.application.authentication.framework.AuthenticatorFlowStatus; @@ -112,6 +113,10 @@ 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.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.validator.AuthorizationDetailsValidator; 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; @@ -285,6 +290,15 @@ public class OAuth2AuthzEndpointTest extends TestOAuthEndpointBase { @Mock private CentralLogMgtServiceComponentHolder centralLogMgtServiceComponentHolderMock; + @Mock + private AuthorizationDetailsService authorizationDetailsServiceMock; + + @Mock + private AuthorizationDetailsValidator authorizationDetailsValidatorMock; + + @Mock + private OAuth2ServiceComponentHolder oAuth2ServiceComponentHolderMock; + 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 = @@ -573,8 +587,8 @@ public void testAuthorize(Object flowStatusObject, String[] clientId, String ses SessionDataCacheKey consentDataCacheKey = new SessionDataCacheKey(SESSION_DATA_KEY_CONSENT_VALUE); when(mockSessionDataCache.getValueFromCache(loginDataCacheKey)).thenReturn(loginCacheEntry); when(mockSessionDataCache.getValueFromCache(consentDataCacheKey)).thenReturn(consentCacheEntry); - when(loginCacheEntry.getoAuth2Parameters()).thenReturn(setOAuth2Parameters( - new HashSet<>(Collections.singletonList(OAuthConstants.Scope.OPENID)), APP_NAME, null, null)); + when(loginCacheEntry.getoAuth2Parameters()).thenReturn(setOAuth2Parameters(new HashSet<>(Collections + .singletonList(OAuthConstants.Scope.OPENID)), APP_NAME, null, null, null)); mockEndpointUtil(false, endpointUtil); when(oAuth2Service.getOauthApplicationState(CLIENT_ID_VALUE)).thenReturn("ACTIVE"); @@ -672,32 +686,39 @@ public void testAuthorize(Object flowStatusObject, String[] clientId, String ses @DataProvider(name = "provideAuthenticatedData") public Object[][] provideAuthenticatedData() { + final AuthorizationDetail testAuthorizationDetail = new AuthorizationDetail(); + testAuthorizationDetail.setType("test_type"); + return addDiagnosticLogStatusToExistingDataProvider(new Object[][]{ {true, true, new HashMap(), null, null, null, new HashSet<>(Arrays.asList(OAuthConstants.Scope.OPENID)), - RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL, HttpServletResponse.SC_FOUND}, + null, RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL, HttpServletResponse.SC_FOUND}, {false, true, null, null, null, null, new HashSet<>(Arrays.asList(OAuthConstants.Scope.OPENID)), - RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL, HttpServletResponse.SC_FOUND}, + null, RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL, HttpServletResponse.SC_FOUND}, - {true, true, new HashMap(), null, null, null, new HashSet<>(Arrays.asList("scope1")), "not_form_post", - APP_REDIRECT_URL, HttpServletResponse.SC_FOUND}, + {true, true, new HashMap(), null, null, null, new HashSet<>(Arrays.asList("scope1")), null, + "not_form_post", APP_REDIRECT_URL, HttpServletResponse.SC_FOUND}, {true, true, new HashMap(), null, null, null, new HashSet<>(Arrays.asList(OAuthConstants.Scope.OPENID)), - RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL_JSON, HttpServletResponse.SC_OK}, + null, RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL_JSON, HttpServletResponse.SC_OK}, - {true, true, new HashMap(), null, null, null, new HashSet<>(Arrays.asList("scope1")), + {true, true, new HashMap(), null, null, null, new HashSet<>(Arrays.asList("scope1")), null, RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL_JSON, HttpServletResponse.SC_OK}, {true, false, null, OAuth2ErrorCodes.INVALID_REQUEST, null, null, - new HashSet<>(Arrays.asList("scope1")), + new HashSet<>(Arrays.asList("scope1")), null, RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL, HttpServletResponse.SC_OK}, {true, false, null, null, "Error!", null, new HashSet<>(Arrays.asList(OAuthConstants.Scope.OPENID)), - RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL, HttpServletResponse.SC_OK}, + null, RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL, HttpServletResponse.SC_OK}, {true, false, null, null, null, "http://localhost:8080/error", - new HashSet<>(Arrays.asList(OAuthConstants.Scope.OPENID)), RESPONSE_MODE_FORM_POST, - APP_REDIRECT_URL, HttpServletResponse.SC_OK} + new HashSet<>(Arrays.asList(OAuthConstants.Scope.OPENID)), null, RESPONSE_MODE_FORM_POST, + APP_REDIRECT_URL, HttpServletResponse.SC_OK}, + + {true, true, new HashMap<>(), null, null, null, Sets.newHashSet("scope1"), + Sets.newHashSet(testAuthorizationDetail), RESPONSE_MODE_FORM_POST, APP_REDIRECT_URL_JSON, + HttpServletResponse.SC_OK} }); } @@ -705,6 +726,7 @@ public Object[][] provideAuthenticatedData() { public void testAuthorizeForAuthenticationResponse(boolean isResultInRequest, boolean isAuthenticated, Map attributes, String errorCode, String errorMsg, String errorUri, Set scopes, + Set testAuthorizationDetails, String responseMode, String redirectUri, int expected, boolean diagnosticLogsEnabled) throws Exception { @@ -725,7 +747,9 @@ public void testAuthorizeForAuthenticationResponse(boolean isResultInRequest, bo MockedStatic identityUtil = mockStatic(IdentityUtil.class, Mockito.CALLS_REAL_METHODS); MockedStatic serviceURLBuilder = mockStatic(ServiceURLBuilder.class); - MockedStatic endpointUtil = mockStatic(EndpointUtil.class, Mockito.CALLS_REAL_METHODS)) { + MockedStatic endpointUtil = mockStatic(EndpointUtil.class, Mockito.CALLS_REAL_METHODS); + MockedStatic serviceComponentHolder = + mockStatic(OAuth2ServiceComponentHolder.class, Mockito.CALLS_REAL_METHODS)) { sessionDataCache.when(SessionDataCache::getInstance).thenReturn(mockSessionDataCache); SessionDataCacheKey loginDataCacheKey = new SessionDataCacheKey(SESSION_DATA_KEY_VALUE); @@ -766,7 +790,8 @@ public void testAuthorizeForAuthenticationResponse(boolean isResultInRequest, bo identityUtil.when(() -> IdentityUtil.getServerURL(anyString(), anyBoolean(), anyBoolean())) .thenReturn("https://localhost:9443/carbon"); - OAuth2Parameters oAuth2Params = setOAuth2Parameters(scopes, APP_NAME, responseMode, redirectUri); + OAuth2Parameters oAuth2Params = + setOAuth2Parameters(scopes, APP_NAME, responseMode, redirectUri, testAuthorizationDetails); oAuth2Params.setClientId(CLIENT_ID_VALUE); oAuth2Params.setState(STATE); when(loginCacheEntry.getoAuth2Parameters()).thenReturn(oAuth2Params); @@ -792,6 +817,7 @@ public void testAuthorizeForAuthenticationResponse(boolean isResultInRequest, bo authzReqDTO.setResponseType("code"); OAuthAuthzReqMessageContext authzReqMsgCtx = new OAuthAuthzReqMessageContext(authzReqDTO); authzReqMsgCtx.setApprovedScope(new String[]{OAuthConstants.Scope.OPENID}); + authzReqMsgCtx.setApprovedAuthorizationDetails(new AuthorizationDetails(testAuthorizationDetails)); when(oAuth2Service.validateScopesBeforeConsent(any(OAuth2AuthorizeReqDTO.class))).thenReturn( authzReqMsgCtx); when(mockAuthorizationHandlerManager.validateScopesBeforeConsent(any(OAuth2AuthorizeReqDTO.class))) @@ -815,6 +841,19 @@ public void testAuthorizeForAuthenticationResponse(boolean isResultInRequest, bo when(oAuth2ScopeService.hasUserProvidedConsentForAllRequestedScopes( anyString(), isNull(), anyInt(), anyList())).thenReturn(true); + when(authorizationDetailsServiceMock.isUserAlreadyConsentedForAuthorizationDetails( + any(AuthenticatedUser.class), any(OAuth2Parameters.class))).thenReturn(true); + OAuth2AuthzEndpoint.setAuthorizationDetailsService(authorizationDetailsServiceMock); + + when(authorizationDetailsValidatorMock + .getValidatedAuthorizationDetails(any(OAuthAuthzReqMessageContext.class))) + .thenReturn(new AuthorizationDetails(testAuthorizationDetails)); + + when(oAuth2ServiceComponentHolderMock.getAuthorizationDetailsValidator()) + .thenReturn(authorizationDetailsValidatorMock); + serviceComponentHolder.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(oAuth2ServiceComponentHolderMock); + mockServiceURLBuilder(serviceURLBuilder); setSupportedResponseModes(); Response response = oAuth2AuthzEndpoint.authorize(httpServletRequest, httpServletResponse); @@ -968,7 +1007,7 @@ public void testUserConsentResponse(String consent, String redirectUrl, Set(), APP_NAME, responseMode, APP_REDIRECT_URL); + setOAuth2Parameters(new HashSet<>(), APP_NAME, responseMode, APP_REDIRECT_URL, null); oAuth2Params.setResponseType(responseType); oAuth2Params.setState(state); oAuth2Params.setClientId(CLIENT_ID_VALUE); @@ -1656,6 +1695,8 @@ public void testHandleUserConsent(boolean isRespDTONull, String consent, boolean OAuthAuthzReqMessageContext authzReqMsgCtx = new OAuthAuthzReqMessageContext(authorizeReqDTO); when(consentCacheEntry.getAuthzReqMsgCtx()).thenReturn(authzReqMsgCtx); + OAuth2AuthzEndpoint.setAuthorizationDetailsService(authorizationDetailsServiceMock); + Response response; try { setSupportedResponseModes(); @@ -1750,7 +1791,8 @@ public void testDoUserAuthz(String prompt, String idTokenHint, boolean hasUserAp mockHttpRequest(requestParams, requestAttributes, HttpMethod.POST); - OAuth2Parameters oAuth2Params = setOAuth2Parameters(new HashSet<>(), APP_NAME, null, APP_REDIRECT_URL); + OAuth2Parameters oAuth2Params = + setOAuth2Parameters(new HashSet<>(), APP_NAME, null, APP_REDIRECT_URL, null); oAuth2Params.setClientId(CLIENT_ID_VALUE); oAuth2Params.setPrompt(prompt); oAuth2Params.setIDTokenHint(idTokenHint); @@ -1910,7 +1952,7 @@ public void testManageOIDCSessionState(Object cookieObject, Object sessionStateO OAuth2Parameters oAuth2Params = setOAuth2Parameters(new HashSet<>(Arrays.asList(OAuthConstants.Scope.OPENID)), - APP_NAME, responseMode, APP_REDIRECT_URL); + APP_NAME, responseMode, APP_REDIRECT_URL, null); oAuth2Params.setClientId(CLIENT_ID_VALUE); oAuth2Params.setPrompt(OAuthConstants.Prompt.LOGIN); @@ -1943,8 +1985,8 @@ public void testManageOIDCSessionState(Object cookieObject, Object sessionStateO , anyString())) .thenReturn("sessionStateValue"); oidcSessionManagementUtil.when( - () -> OIDCSessionManagementUtil.addSessionStateToURL(anyString(), anyString(), - isNull())).thenCallRealMethod(); + () -> OIDCSessionManagementUtil.addSessionStateToURL(anyString(), anyString(), + isNull())).thenCallRealMethod(); sessionDataCache.when(SessionDataCache::getInstance).thenReturn(mockSessionDataCache); SessionDataCacheKey loginDataCacheKey = new SessionDataCacheKey(SESSION_DATA_KEY_VALUE); @@ -2675,11 +2717,11 @@ private void mockEndpointUtil(boolean isConsentMgtEnabled, MockedStatic EndpointUtil.getUserConsentURL(any(OAuth2Parameters.class), - anyString(), anyString(), any(OAuthMessage.class), anyString())).thenReturn(USER_CONSENT_URL); + anyString(), anyString(), any(OAuthMessage.class), anyString())).thenReturn(USER_CONSENT_URL); endpointUtil.when(EndpointUtil::getRequestObjectService).thenReturn(requestObjectService); endpointUtil.when(() -> EndpointUtil.getLoginPageURL(anyString(), anyString(), anyBoolean(), - anyBoolean(), anySet(), anyMap(), any())).thenReturn(LOGIN_PAGE_URL); + anyBoolean(), anySet(), anyMap(), any())).thenReturn(LOGIN_PAGE_URL); EndpointUtil.setOAuthAdminService(oAuthAdminService); EndpointUtil.setOAuth2ScopeService(oAuth2ScopeService); @@ -2724,13 +2766,14 @@ private AuthenticationResult setAuthenticationResult(boolean isAuthenticated, Ma } private OAuth2Parameters setOAuth2Parameters(Set scopes, String appName, String responseMode, - String redirectUri) { + String redirectUri, Set authorizationDetails) { OAuth2Parameters oAuth2Parameters = new OAuth2Parameters(); oAuth2Parameters.setScopes(scopes); oAuth2Parameters.setResponseMode(responseMode); oAuth2Parameters.setRedirectURI(redirectUri); oAuth2Parameters.setApplicationName(appName); + oAuth2Parameters.setAuthorizationDetails(new AuthorizationDetails(authorizationDetails)); return oAuth2Parameters; } diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtilTest.java b/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtilTest.java index 188c2943859..58af26b8132 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtilTest.java +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/test/java/org/wso2/carbon/identity/oauth/endpoint/util/EndpointUtilTest.java @@ -38,6 +38,7 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Listeners; import org.testng.annotations.Test; +import org.testng.collections.Sets; import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.base.ServerConfiguration; import org.wso2.carbon.context.PrivilegedCarbonContext; @@ -71,8 +72,12 @@ import org.wso2.carbon.identity.oauth2.OAuth2Service; import org.wso2.carbon.identity.oauth2.OAuth2TokenValidationService; import org.wso2.carbon.identity.oauth2.bean.Scope; +import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; import org.wso2.carbon.identity.oauth2.model.OAuth2ScopeConsentResponse; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import org.wso2.carbon.identity.openidconnect.RequestObjectService; import org.wso2.carbon.identity.webfinger.DefaultWebFingerProcessor; @@ -159,9 +164,15 @@ public class EndpointUtilTest { @Mock OAuth2ScopeService oAuth2ScopeService; + @Mock + private AuthorizationDetailsService authorizationDetailsServiceMock; + @Mock FileBasedConfigurationBuilder mockFileBasedConfigurationBuilder; + @Mock + private OAuth2ServiceComponentHolder oAuth2ServiceComponentHolderMock; + private static final String COMMONAUTH_URL = "https://localhost:9443/commonauth"; private static final String OIDC_CONSENT_PAGE_URL = "https://localhost:9443/authenticationendpoint/oauth2_consent.do"; @@ -196,6 +207,14 @@ public class EndpointUtilTest { private String clientId; private AuthenticatedUser user; private OAuth2ScopeConsentResponse oAuth2ScopeConsentResponse; + private final AuthorizationDetails testAuthorizationDetails; + + public EndpointUtilTest() { + + final AuthorizationDetail testAuthorizationDetail = new AuthorizationDetail(); + testAuthorizationDetail.setType("test_type"); + this.testAuthorizationDetails = new AuthorizationDetails(Sets.newHashSet(testAuthorizationDetail)); + } @BeforeMethod public void setUp() { @@ -245,6 +264,7 @@ public Object[][] provideDataForUserConsentURL() { params.setClientId("testClientId"); params.setTenantDomain("testTenantDomain"); params.setScopes(new HashSet(Arrays.asList("scope1", "scope2", "internal_login"))); + params.setAuthorizationDetails(testAuthorizationDetails); OAuth2Parameters paramsOIDC = new OAuth2Parameters(); paramsOIDC.setApplicationName("TestApplication"); @@ -291,7 +311,9 @@ public void testGetUserConsentURL(Object oAuth2ParamObject, boolean isOIDC, bool MockedStatic oAuthURL = mockStatic(OAuth2Util.OAuthURL.class); MockedStatic identityTenantUtil = mockStatic(IdentityTenantUtil.class); MockedStatic frameworkUtils = mockStatic(FrameworkUtils.class); - MockedStatic sessionDataCache = mockStatic(SessionDataCache.class);) { + MockedStatic sessionDataCache = mockStatic(SessionDataCache.class); + MockedStatic serviceComponentHolder = + mockStatic(OAuth2ServiceComponentHolder.class, Mockito.CALLS_REAL_METHODS)) { EndpointUtil.setOauthServerConfiguration(mockedOAuthServerConfiguration); lenient().when(mockedOAuthServerConfiguration.isDropUnregisteredScopes()).thenReturn(false); @@ -344,6 +366,13 @@ public void testGetUserConsentURL(Object oAuth2ParamObject, boolean isOIDC, bool lenient().when(mockedOAuthAdminService.getRegisteredOIDCScope(anyString())) .thenReturn(Arrays.asList("openid", "email", "profile", "groups")); + lenient().when(authorizationDetailsServiceMock.getConsentRequiredAuthorizationDetails(user, parameters)) + .thenReturn(testAuthorizationDetails); + lenient().when(oAuth2ServiceComponentHolderMock.getAuthorizationDetailsService()) + .thenReturn(authorizationDetailsServiceMock); + serviceComponentHolder.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(oAuth2ServiceComponentHolderMock); + String consentUrl; try { consentUrl = EndpointUtil.getUserConsentURL(parameters, username, sessionDataKey, isOIDC); 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..5d491ddac4b --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/pom.xml @@ -0,0 +1,172 @@ + + + + + + org.wso2.carbon.identity.inbound.auth.oauth2 + identity-inbound-auth-oauth + ../../pom.xml + 7.0.215-SNAPSHOT + + + 4.0.0 + org.wso2.carbon.identity.oauth.rar + jar + WSO2 Carbon - Rich Authorization Requests + http://wso2.org + + + UTF-8 + + + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.core + provided + + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.api.resource.mgt + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + + io.vertx + vertx-json-schema + + + + + org.testng + testng + test + + + + org.mockito + mockito-testng + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + com.h2database + h2 + test + + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + + High + 2048 + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.plugin.version} + + + src/test/resources/testng.xml + + + target/jacoco.exec + + true + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.version} + + + + **/*Constants.class + **/dto/** + **/exception/** + **/model/** + + + + + default-prepare-agent + + prepare-agent + + + + default-report + verify + + report + + + + default-check + + check + + + + + BUNDLE + + + COMPLEXITY + COVEREDRATIO + 0.80 + + + + + + + + + + + + + diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsSchemaValidator.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsSchemaValidator.java new file mode 100644 index 00000000000..82755287197 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsSchemaValidator.java @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2025, 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 io.vertx.core.Vertx; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.JsonObject; +import io.vertx.json.schema.Draft; +import io.vertx.json.schema.JsonSchema; +import io.vertx.json.schema.JsonSchemaOptions; +import io.vertx.json.schema.JsonSchemaValidationException; +import io.vertx.json.schema.OutputFormat; +import io.vertx.json.schema.OutputUnit; +import io.vertx.json.schema.SchemaRepository; +import io.vertx.json.schema.Validator; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; + +import java.util.Map; + +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.SCHEMA_VALIDATION_FAILED_ERR_MSG_FORMAT; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.TYPE_VALIDATION_FAILED_ERR_MSG_FORMAT; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_MSG; + +/** + * The {@code AuthorizationDetailsSchemaValidator} is responsible for validating authorization details + * against a provided JSON schema. + *

+ * This class supports both validation of custom schemas provided as input and validation of default schemas + * based on the DRAFT202012 standard. + *

+ * Typical usage: + *

+ *     AuthorizationDetailsSchemaValidator validator = AuthorizationDetailsSchemaValidator.getInstance();
+ *     boolean isValid = validator.isSchemaCompliant(schemaString, authorizationDetail);
+ * 
+ * + *

Refer to + * json-schema for detailed information on the JSON documents structure.

+ * + * @see AuthorizationDetail + * @see JsonSchema + */ +public class AuthorizationDetailsSchemaValidator { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsSchemaValidator.class); + + private static final String ADDITIONAL_PROPERTIES = "additionalProperties"; + private static final String BASE_URI = "https://wso2.com/identity-server/schemas"; + + private static volatile AuthorizationDetailsSchemaValidator instance; + private final JsonSchemaOptions jsonSchemaOptions; + private final SchemaRepository schemaRepository; + + private AuthorizationDetailsSchemaValidator() { + + this.jsonSchemaOptions = new JsonSchemaOptions() + .setBaseUri(BASE_URI) + .setDraft(Draft.DRAFT202012) + .setOutputFormat(OutputFormat.Basic); + + this.schemaRepository = SchemaRepository.create(this.jsonSchemaOptions) + .preloadMetaSchema(Vertx.vertx().fileSystem()); + } + + public static AuthorizationDetailsSchemaValidator getInstance() { + + if (instance == null) { + synchronized (AuthorizationDetailsSchemaValidator.class) { + if (instance == null) { + instance = new AuthorizationDetailsSchemaValidator(); + } + } + } + return instance; + } + + /** + * Validates whether the given schema is compliant with the JSON schema DRAFT202012 standard. + * + * @param schema the JSON schema as a string. + * @return true if the schema is valid, false if the schema is invalid or empty. + * @throws AuthorizationDetailsProcessingException if the validation fails or an error occurs during validation. + */ + public boolean isValidSchema(final String schema) throws AuthorizationDetailsProcessingException { + + if (StringUtils.isEmpty(schema)) { + log.debug("Schema validation failed. Schema cannot be null"); + return false; + } + + final OutputUnit outputUnit = this.buildOutputUnit(null, this.parseJsonObject(schema)); + try { + // Validates the schema itself against the DRAFT202012 schema standard + outputUnit.checkValidity(); + } catch (JsonSchemaValidationException e) { + if (log.isDebugEnabled()) { + log.debug(String.format("Validation failed against DRAFT202012 schema for input: %s. Caused by, ", + schema), e); + } + throw new AuthorizationDetailsProcessingException(String.format(SCHEMA_VALIDATION_FAILED_ERR_MSG_FORMAT, + buildSchemaValidationErrorMessage(outputUnit, e)), e); + } + return true; + } + + private OutputUnit buildOutputUnit(final JsonObject jsonSchema, final JsonObject jsonInput) { + + // Validate the jsonSchema if present, otherwise validate the schema itself against json-schema DRAFT202012 + final Validator validator = (jsonSchema != null) + ? this.schemaRepository.validator(JsonSchema.of(jsonSchema), this.jsonSchemaOptions) + : this.schemaRepository.validator(this.jsonSchemaOptions.getDraft().getIdentifier()); + + return validator.validate(jsonInput); + } + + /** + * Converts a JSON string into a {@link JsonObject}. If the input is invalid, throws an exception. + * + * @param jsonString The input JSON string to be converted. + * @return A {@link JsonObject} created from the input string. + * @throws AuthorizationDetailsProcessingException if the input string is not valid JSON. + */ + private JsonObject parseJsonObject(final String jsonString) throws AuthorizationDetailsProcessingException { + + try { + return new JsonObject(jsonString); + } catch (DecodeException e) { + if (log.isDebugEnabled()) { + log.debug(String.format("Failed to parse the JSON input: '%s'. Caused by, ", jsonString), e); + } + throw new AuthorizationDetailsProcessingException( + String.format("%s. Invalid JSON input received.", VALIDATION_FAILED_ERR_MSG), e); + } + } + + private String buildSchemaValidationErrorMessage(final OutputUnit outputUnit, + final JsonSchemaValidationException ex) { + + // Extract the last validation error if available, otherwise use exception message. + if (outputUnit == null || CollectionUtils.isEmpty(outputUnit.getErrors())) { + return ex.getMessage(); + } + final OutputUnit lastError = outputUnit.getErrors().get(outputUnit.getErrors().size() - 1); + return lastError.getInstanceLocation() + StringUtils.SPACE + lastError.getError(); + } + + /** + * Validates whether the given authorization detail complies with the provided JSON schema. + * + * @param schema the JSON schema as a string. + * @param authorizationDetail the authorization detail to be validated. + * @return true if the authorization detail is schema compliant, false if schema or authorizationDetail is invalid. + * @throws AuthorizationDetailsProcessingException if the validation fails or an error occurs during validation. + */ + public boolean isSchemaCompliant(final String schema, final AuthorizationDetail authorizationDetail) + throws AuthorizationDetailsProcessingException { + + if (StringUtils.isEmpty(schema) || authorizationDetail == null) { + log.debug("Schema validation failed. Inputs cannot be null"); + return false; + } + + return this.isSchemaCompliant(this.parseJsonObject(schema), authorizationDetail); + } + + public boolean isSchemaCompliant(final JsonObject schema, final AuthorizationDetail authorizationDetail) + throws AuthorizationDetailsProcessingException { + + if (schema == null || authorizationDetail == null) { + log.debug("Schema validation failed. Inputs cannot be null"); + return false; + } + + final OutputUnit outputUnit = + this.buildOutputUnit(schema, this.parseJsonObject(authorizationDetail.toJsonString())); + + try { + // Validates the authorization detail against the schema + outputUnit.checkValidity(); + } catch (JsonSchemaValidationException e) { + if (log.isDebugEnabled()) { + log.debug(String.format("Schema validation failed for authorization details type: %s. Caused by, ", + authorizationDetail.getType()), e); + } + throw new AuthorizationDetailsProcessingException(String.format(TYPE_VALIDATION_FAILED_ERR_MSG_FORMAT, + authorizationDetail.getType(), this.buildSchemaValidationErrorMessage(outputUnit, e)), e); + } + return true; + } + + public boolean isSchemaCompliant(final Map schema, final AuthorizationDetail authorizationDetail) + throws AuthorizationDetailsProcessingException { + + if (MapUtils.isEmpty(schema) || authorizationDetail == null) { + log.debug("Schema validation failed. Inputs cannot be null"); + return false; + } + + final JsonObject jsonSchema = new JsonObject(schema); + jsonSchema.put(ADDITIONAL_PROPERTIES, false); // Ensure no unknown fields are allowed + + return this.isSchemaCompliant(jsonSchema, authorizationDetail); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAO.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAO.java new file mode 100644 index 00000000000..de104648af2 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAO.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2025, 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.dao; + +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsCodeDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsTokenDTO; + +import java.sql.SQLException; +import java.util.Set; + +/** + * Provides methods to interact with the database to manage rich authorization requests. + * + *

{@link AuthorizationDetailsDAO} provides methods to add, update, retrieve, and delete authorization details + * associated with user consent and access tokens. + */ +public interface AuthorizationDetailsDAO { + + /** + * Adds user consented authorization details to the database. + * + * @param authorizationDetailsConsentDTOs A set 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(Set authorizationDetailsConsentDTOs) + throws SQLException; + + /** + * Updates user consented authorization details in the database. + * + * @param authorizationDetailsConsentDTOs A set 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[] updateUserConsentedAuthorizationDetails(Set authorizationDetailsConsentDTOs) + throws SQLException; + + /** + * Retrieves user consented authorization details from the database. + * + * @param consentId The ID of the consent. + * @param tenantId The tenant ID. + * @return A set of user consented authorization details DTOs. + * @throws SQLException If a database access error occurs. + */ + Set getUserConsentedAuthorizationDetails(String consentId, int tenantId) + throws SQLException; + + /** + * Deletes user consented authorization details from the database. + * + * @param consentId The ID of the consent. + * @param tenantId The tenant ID. + * @return The number of rows affected by the delete operation. + * @throws SQLException If a database access error occurs. + */ + int deleteUserConsentedAuthorizationDetails(String consentId, int tenantId) throws SQLException; + + /** + * Adds access token authorization details to the database. + * + * @param authorizationDetailsTokenDTOs A set of access token authorization details DTOs. + * {@link AuthorizationDetailsTokenDTO} + * @return An array of integers indicating the number of rows affected for each batch operation. + * Positive values indicate success, negative values indicate failure. + * @throws SQLException If a database access error occurs. + */ + int[] addAccessTokenAuthorizationDetails(Set authorizationDetailsTokenDTOs) + throws SQLException; + + /** + * Retrieves access token authorization details from the database. + * + * @param accessTokenId The ID of the access token. + * @param tenantId The tenant ID. + * @return A set of access token authorization details DTOs. + * @throws SQLException If a database access error occurs. + */ + Set getAccessTokenAuthorizationDetails(String accessTokenId, int tenantId) + throws SQLException; + + /** + * Deletes access token authorization details from the database. + * + * @param accessTokenId The ID of the access token. + * @param tenantId The tenant ID. + * @return The number of rows affected by the delete operation. + * @throws SQLException If a database access error occurs. + */ + int deleteAccessTokenAuthorizationDetails(String accessTokenId, int tenantId) throws SQLException; + + /** + * Adds authorization details against a given OAuth2 code. + * + * @param authorizationDetailsCodeDTOs A list of code authorization details DTOs to store. + * @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(Set authorizationDetailsCodeDTOs) + throws SQLException; + + /** + * Retrieves authorization code authorization details from the database. + * + * @param authorizationCode The value of the authorization code. + * @param tenantId The tenant ID. + * @return A set of authorization code authorization details DTOs. + * @throws SQLException If a database access error occurs. + */ + Set getOAuth2CodeAuthorizationDetails(String authorizationCode, int tenantId) + throws SQLException; + + /** + * Retrieves the consent ID associated with a specific user ID and application ID. + * + * @param userId The user ID. + * @param appId The application ID. + * @param tenantId The tenant ID. + * @return The consent ID as a string. + * @throws SQLException If a database access error occurs. + */ + // TODO: Move this method to the consent module + String getConsentIdByUserIdAndAppId(String userId, String appId, int tenantId) throws SQLException; +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAOImpl.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAOImpl.java new file mode 100644 index 00000000000..a5f46a3b83a --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAOImpl.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2025, 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.dao; + +import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsCodeDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsTokenDTO; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; + +/** + * Implements the {@link AuthorizationDetailsDAO} interface to manage rich authorization requests. + * + *

{@link AuthorizationDetailsDAO} provides methods to add, update, retrieve, and delete authorization details + * associated with user consent and access tokens. + */ +public class AuthorizationDetailsDAOImpl implements AuthorizationDetailsDAO { + + /** + * {@inheritDoc} + */ + @Override + public int[] addUserConsentedAuthorizationDetails(final Set consentDTOs) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + PreparedStatement ps = + connection.prepareStatement(SQLQueries.ADD_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS)) { + + for (AuthorizationDetailsConsentDTO consentDTO : consentDTOs) { + ps.setString(1, consentDTO.getConsentId()); + ps.setString(2, consentDTO.getAuthorizationDetail().toJsonString()); + ps.setBoolean(3, consentDTO.isConsentActive()); + ps.setString(4, consentDTO.getAuthorizationDetail().getType()); + ps.setInt(5, consentDTO.getTenantId()); + ps.setInt(6, consentDTO.getTenantId()); + ps.addBatch(); + } + return ps.executeBatch(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int[] updateUserConsentedAuthorizationDetails(final Set consentDTOs) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + PreparedStatement ps = + connection.prepareStatement(SQLQueries.UPDATE_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS)) { + + for (AuthorizationDetailsConsentDTO consentDTO : consentDTOs) { + ps.setString(1, consentDTO.getAuthorizationDetail().toJsonString()); + ps.setBoolean(2, consentDTO.isConsentActive()); + ps.setString(3, consentDTO.getConsentId()); + ps.setString(4, consentDTO.getAuthorizationDetail().getType()); + ps.setInt(5, consentDTO.getTenantId()); + ps.setInt(6, consentDTO.getTenantId()); + ps.addBatch(); + } + return ps.executeBatch(); + } + } + + /** + * {@inheritDoc} + */ + @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 String typeId = rs.getString(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; + } + } + } + + /** + * {@inheritDoc} + */ + @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(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int[] addAccessTokenAuthorizationDetails(final Set tokenDTOs) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.ADD_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS)) { + + for (AuthorizationDetailsTokenDTO tokenDTO : tokenDTOs) { + ps.setString(1, tokenDTO.getAccessTokenId()); + ps.setString(2, tokenDTO.getAuthorizationDetail().toJsonString()); + ps.setString(3, tokenDTO.getAuthorizationDetail().getType()); + ps.setInt(4, tokenDTO.getTenantId()); + ps.setInt(5, tokenDTO.getTenantId()); + ps.addBatch(); + } + return ps.executeBatch(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Set getAccessTokenAuthorizationDetails(final String accessTokenId, + final int tenantId) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.GET_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS)) { + + ps.setString(1, accessTokenId); + ps.setInt(2, tenantId); + try (ResultSet rs = ps.executeQuery()) { + + final Set authorizationDetailsTokenDTO = new HashSet<>(); + while (rs.next()) { + final String id = rs.getString(1); + final String typeId = rs.getString(2); + final String authorizationDetail = rs.getString(3); + + authorizationDetailsTokenDTO.add( + new AuthorizationDetailsTokenDTO(id, accessTokenId, typeId, authorizationDetail, tenantId)); + } + return authorizationDetailsTokenDTO; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public int deleteAccessTokenAuthorizationDetails(final String accessTokenId, final int tenantId) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.DELETE_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS)) { + + ps.setString(1, accessTokenId); + ps.setInt(2, tenantId); + return ps.executeUpdate(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int[] addOAuth2CodeAuthorizationDetails(final Set authorizationDetailsCodeDTOs) + throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.ADD_OAUTH2_CODE_AUTHORIZATION_DETAILS)) { + + for (AuthorizationDetailsCodeDTO authorizationDetailsCodeDTO : authorizationDetailsCodeDTOs) { + ps.setString(1, authorizationDetailsCodeDTO.getAuthorizationCodeId()); + ps.setString(2, authorizationDetailsCodeDTO.getAuthorizationDetail().toJsonString()); + ps.setString(3, authorizationDetailsCodeDTO.getAuthorizationDetail().getType()); + ps.setInt(4, authorizationDetailsCodeDTO.getTenantId()); + ps.setInt(5, authorizationDetailsCodeDTO.getTenantId()); + ps.addBatch(); + } + return ps.executeBatch(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Set getOAuth2CodeAuthorizationDetails(final String authorizationCode, + final int tenantId) throws SQLException { + + try (final Connection connection = IdentityDatabaseUtil.getDBConnection(false); + final PreparedStatement ps = + connection.prepareStatement(SQLQueries.GET_OAUTH2_CODE_AUTHORIZATION_DETAILS_BY_CODE)) { + + ps.setString(1, authorizationCode); + ps.setInt(2, tenantId); + try (ResultSet rs = ps.executeQuery()) { + + final Set authorizationDetailsCodeDTOs = new HashSet<>(); + while (rs.next()) { + final String codeId = rs.getString(1); + final String typeId = rs.getString(2); + final String authorizationDetail = rs.getString(3); + + authorizationDetailsCodeDTOs.add(new AuthorizationDetailsCodeDTO( + codeId, typeId, authorizationDetail, tenantId)); + } + return authorizationDetailsCodeDTOs; + } + } + } + + /** + * {@inheritDoc} + */ + @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/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/SQLQueries.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/SQLQueries.java new file mode 100644 index 00000000000..d540e9f4b15 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dao/SQLQueries.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, 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.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 + } + + private static final String SELECT_AUTHORIZATION_DETAILS_ID_BY_TYPE = + "SELECT ID FROM AUTHORIZATION_DETAILS_TYPES WHERE TYPE = ? AND TENANT_ID = ?"; + + public static final String ADD_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS = + "INSERT INTO IDN_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS " + + "(CONSENT_ID, AUTHORIZATION_DETAILS, CONSENT, TYPE_ID, TENANT_ID) " + + "VALUES (?, ?, ?, (" + SELECT_AUTHORIZATION_DETAILS_ID_BY_TYPE + "), ?)"; + + 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 TYPE_ID=(" + SELECT_AUTHORIZATION_DETAILS_ID_BY_TYPE + ") 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 ADD_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS = + "INSERT INTO IDN_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS " + + "(TOKEN_ID, AUTHORIZATION_DETAILS, TYPE_ID, TENANT_ID) " + + "VALUES (?, ?, (" + SELECT_AUTHORIZATION_DETAILS_ID_BY_TYPE + "), ?)"; + + public static final String DELETE_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS = + "DELETE FROM IDN_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS WHERE TOKEN_ID=? AND TENANT_ID=?"; + + public static final String GET_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS = + "SELECT ID, TYPE_ID, AUTHORIZATION_DETAILS FROM IDN_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS " + + "WHERE TOKEN_ID=? AND TENANT_ID=?"; + + public static final String ADD_OAUTH2_CODE_AUTHORIZATION_DETAILS = + "INSERT INTO IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS" + + "(CODE_ID, AUTHORIZATION_DETAILS, TYPE_ID, TENANT_ID) " + + "VALUES (?, ?, (" + SELECT_AUTHORIZATION_DETAILS_ID_BY_TYPE + "), ?)"; + + public static final String GET_OAUTH2_CODE_AUTHORIZATION_DETAILS_BY_CODE = + "SELECT IOAC.CODE_ID, IOACAD.TYPE_ID, IOACAD.AUTHORIZATION_DETAILS " + + "FROM IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS IOACAD " + + "INNER JOIN IDN_OAUTH2_AUTHORIZATION_CODE IOAC ON IOACAD.CODE_ID = IOAC.CODE_ID " + + "WHERE IOAC.AUTHORIZATION_CODE=? AND IOACAD.TENANT_ID=?"; + + 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/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsCodeDTO.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsCodeDTO.java new file mode 100644 index 00000000000..19aeaff5bce --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsCodeDTO.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, 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.dto; + +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; + +/** + * Data Transfer Object (DTO) for representing authorization details along with authorization code. + * This class extends {@link AuthorizationDetailsDTO} to include additional fields for authorization code ID. + */ +public class AuthorizationDetailsCodeDTO extends AuthorizationDetailsDTO { + + final String codeId; + + /** + * Constructs an {@link AuthorizationDetailsCodeDTO} with all required fields. + * + * @param codeId the authorization code ID associated with the authorization detail. + * @param typeId the type ID of the authorization detail. + * @param authorizationDetail the {@link AuthorizationDetail} object. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsCodeDTO(final String codeId, final String typeId, + final String authorizationDetail, final int tenantId) { + + super(null, typeId, authorizationDetail, tenantId); + this.codeId = codeId; + } + + /** + * Constructs an {@link AuthorizationDetailsCodeDTO} with essential fields. + * + * @param codeId the authorization code ID associated with the authorization detail. + * @param authorizationDetail the {@link AuthorizationDetail} object. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsCodeDTO(final String codeId, final AuthorizationDetail authorizationDetail, + final int tenantId) { + + super(authorizationDetail, tenantId); + this.codeId = codeId; + } + + /** + * Gets the authorization code ID associated with the authorization detail. + * + * @return the authorization code ID. + */ + public String getAuthorizationCodeId() { + return this.codeId; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsConsentDTO.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsConsentDTO.java new file mode 100644 index 00000000000..72e270b9950 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsConsentDTO.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025, 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.dto; + +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; + +/** + * Data Transfer Object (DTO) for representing authorization details along with consent information. + * This class extends {@link AuthorizationDetailsDTO} to include additional fields for consent ID and consent status. + */ +public class AuthorizationDetailsConsentDTO extends AuthorizationDetailsDTO { + + final String consentId; + final boolean isConsentActive; + + /** + * Constructs an {@link AuthorizationDetailsConsentDTO} with all required fields. + * + * @param id the ID of the authorization detail DTO. + * @param consentId the consent ID associated with the authorization detail. + * @param typeId the type ID of the authorization detail. + * @param authorizationDetailJson the JSON string of the authorization detail. + * @param isConsentActive the consent status. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsConsentDTO(final String id, final String consentId, final String typeId, + final String authorizationDetailJson, + final boolean isConsentActive, final int tenantId) { + + super(id, typeId, authorizationDetailJson, tenantId); + this.consentId = consentId; + this.isConsentActive = isConsentActive; + } + + /** + * Constructs an {@link AuthorizationDetailsConsentDTO} with essential fields. + * + * @param consentId the consent ID associated with the authorization detail. + * @param authorizationDetail the {@link AuthorizationDetail} object. + * @param isConsentActive the consent status. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsConsentDTO(final String consentId, + final AuthorizationDetail authorizationDetail, + final boolean isConsentActive, final int tenantId) { + + super(authorizationDetail, tenantId); + this.consentId = consentId; + this.isConsentActive = isConsentActive; + } + + /** + * Checks if the consent is active. + * + * @return {@code true} if consent is active, {@code false} otherwise. + */ + public boolean isConsentActive() { + return isConsentActive; + } + + /** + * Gets the consent ID associated with the authorization detail. + * + * @return the consent ID. + */ + public String getConsentId() { + return consentId; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsDTO.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsDTO.java new file mode 100644 index 00000000000..a855db404e1 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsDTO.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025, 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.dto; + +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; + +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsCommonUtils.fromJSON; + +/** + * Data Transfer Object (DTO) for representing authorization details. + *

This class encapsulates the details of authorization, including the ID, type ID, + * authorization detail object, and tenant ID. + */ +public class AuthorizationDetailsDTO { + + final String id; + final String typeId; + final AuthorizationDetail authorizationDetail; + final int tenantId; + + /** + * Constructs an AuthorizationDetailsDTO with all fields. + * + * @param id the ID of the authorization detail DTO. + * @param typeId the type ID of the authorization detail. + * @param authorizationDetail the authorization detail object. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsDTO(final String id, final String typeId, final AuthorizationDetail authorizationDetail, + final int tenantId) { + + this.id = id; + this.typeId = typeId; + this.authorizationDetail = authorizationDetail; + this.tenantId = tenantId; + } + + /** + * Constructs an AuthorizationDetailsDTO from authorization detail JSON string. + * + * @param id the ID of the authorization detail DTO. + * @param typeId the type ID of the authorization detail. + * @param authorizationDetailJson the JSON string of the authorization detail. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsDTO(final String id, final String typeId, final String authorizationDetailJson, + final int tenantId) { + + this(id, typeId, fromJSON(authorizationDetailJson, AuthorizationDetail.class), tenantId); + } + + /** + * Constructs an AuthorizationDetailsDTO with an authorization detail object and tenant ID. + * + * @param authorizationDetail the authorization detail object. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsDTO(final AuthorizationDetail authorizationDetail, final int tenantId) { + + this(null, null, authorizationDetail, tenantId); + } + + /** + * Gets the ID of the authorization detail. + * + * @return the ID of the authorization detail. + */ + public String getId() { + return this.id; + } + + /** + * Gets the type ID of the authorization detail. + * + * @return the type ID of the authorization detail. + */ + public String getTypeId() { + return this.typeId; + } + + /** + * Gets the authorization detail object. + * + * @return the authorization detail object. + */ + public AuthorizationDetail getAuthorizationDetail() { + return this.authorizationDetail; + } + + /** + * Gets the tenant ID. + * + * @return the tenant ID. + */ + public int getTenantId() { + return this.tenantId; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsTokenDTO.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsTokenDTO.java new file mode 100644 index 00000000000..217310e208c --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/dto/AuthorizationDetailsTokenDTO.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, 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.dto; + +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; + +/** + * Data Transfer Object (DTO) for representing authorization details along with access token information. + * This class extends {@link AuthorizationDetailsDTO} to include additional fields for access token ID. + */ +public class AuthorizationDetailsTokenDTO extends AuthorizationDetailsDTO { + + final String accessTokenId; + + /** + * Constructs an {@link AuthorizationDetailsTokenDTO} with all required fields. + * + * @param id the ID of the authorization detail DTO. + * @param accessTokenId the access token ID associated with the authorization detail. + * @param typeId the type ID of the authorization detail. + * @param authorizationDetail the {@link AuthorizationDetail} object. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsTokenDTO(final String id, final String accessTokenId, final String typeId, + final String authorizationDetail, final int tenantId) { + + super(id, typeId, authorizationDetail, tenantId); + this.accessTokenId = accessTokenId; + } + + /** + * Constructs an {@link AuthorizationDetailsTokenDTO} with essential fields. + * + * @param accessTokenId the access token ID associated with the authorization detail. + * @param authorizationDetail the {@link AuthorizationDetail} object. + * @param tenantId the tenant ID. + */ + public AuthorizationDetailsTokenDTO(final String accessTokenId, final AuthorizationDetail authorizationDetail, + final int tenantId) { + + super(authorizationDetail, tenantId); + this.accessTokenId = accessTokenId; + } + + /** + * Gets the access token ID associated with the authorization detail. + * + * @return the access token ID. + */ + public String getAccessTokenId() { + return this.accessTokenId; + } +} 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..711280d1d0c --- /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,55 @@ +/* + * Copyright (c) 2025, 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.exception; + +import org.wso2.carbon.identity.base.IdentityException; + +/** + * Exception class to represent failures related to Rich Authorization Requests in OAuth 2.0 clients. + * + *

This exception is thrown when there are errors in processing authorization details during the OAuth 2.0 + * authorization flow. It extends the {@link IdentityException} class, providing more specific + * context for authorization-related issues.

+ */ +public class AuthorizationDetailsProcessingException extends IdentityException { + + private static final long serialVersionUID = -206212512259482200L; + + /** + * Constructs a new exception with the specified detail message. + * + * @param message The detail message. It provides information about the cause of the exception. + */ + public AuthorizationDetailsProcessingException(final String message) { + + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + * @param message The detail message. It provides information about the cause of the exception. + * @param cause The cause of the exception. It can be used to retrieve the stack trace or other information + * about the root cause of the exception. + */ + public AuthorizationDetailsProcessingException(final String message, final Throwable cause) { + + super(message, cause); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetail.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetail.java new file mode 100644 index 00000000000..6ebbc0f3b73 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetail.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2025, 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 com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang.StringUtils; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsCommonUtils; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.

+ */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AuthorizationDetail implements Serializable { + + private static final long serialVersionUID = -3928636285264078857L; + + private String type; + private List locations; + private List actions; + @JsonProperty("datatypes") + private List dataTypes; + private String identifier; + private List privileges; + private Map details; + + @JsonProperty("_id") + private String id; + @JsonProperty("_description") + private String description; + + /** + * Gets the unique ID of the authorization detail. + * + * @return the ID of the authorization detail. + */ + public String getId() { + + return this.id; + } + + /** + * Sets a unique temporary ID for a given authorization detail instance. + */ + 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) { + this.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 getDescription() { + return this.description; + } + + /** + * 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 description 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 setDescription(final String description) { + + this.description = description; + } + + /** + * 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 getDescriptionOrDefault(Function defaultFunction) { + + return StringUtils.isNotEmpty(this.getDescription()) ? this.getDescription() : 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); + } + + /** + * Converts the current authorization detail instance to a {@link Map}. + * + * @return The {@code Map} representation of the authorization detail. + */ + public Map toMap() { + + return AuthorizationDetailsCommonUtils.toMap(this); + } + + @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 + + '}'; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetails.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetails.java new file mode 100644 index 00000000000..657e62ef6db --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetails.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2025, 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.apache.commons.lang.StringUtils; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsCommonUtils; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +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)); + } + + /** + * Returns a set of the {@code authorization_details}. + * + * @return A set of {@link AuthorizationDetail}. + */ + public Set getDetails() { + + return this.authorizationDetails; + } + + /** + * Converts a stream of AuthorizationDetail objects into a {@link Set} of {@link Map}. + * + *

Each AuthorizationDetail object is transformed into a Map representation using + * the {@link AuthorizationDetail#toMap} method. + * + * @return a Set of Maps representing the AuthorizationDetail objects. + */ + public Set> toSet() { + + return this.stream().map(AuthorizationDetail::toMap).collect(Collectors.toSet()); + } + + /** + * 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()); + } + + /** + * 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.getDescriptionOrDefault(AuthorizationDetail::getType)) + .collect(Collectors.joining(AuthorizationDetailsConstants.PARAM_SEPARATOR)); + } + + /** + * Converts the current set of authorization details to a {@link Stream}. + * + * @return The Stream representation of the {@code authorization_details}. + */ + public Stream stream() { + + return this.getDetails().stream(); + } +} 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..28906d8d42b --- /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) 2025, 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/AuthorizationDetailsCommonUtils.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsCommonUtils.java new file mode 100644 index 00000000000..b05221bfac3 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsCommonUtils.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025, 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 com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +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.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.EMPTY_JSON_ARRAY; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.EMPTY_JSON_OBJECT; + +/** + * Utility class for handling OAuth2 Rich Authorization Requests. + */ +public class AuthorizationDetailsCommonUtils { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsCommonUtils.class); + + private static volatile ObjectMapper objectMapper; + private static final TypeReference> TYPE_MAP = new TypeReference>() { }; + + 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 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 AuthorizationDetails + */ + public static Set fromJSONArray(final String authorizationDetailsJson, + final Class clazz) { + + try { + if (StringUtils.isNotEmpty(authorizationDetailsJson)) { + return getDefaultObjectMapper().readValue(authorizationDetailsJson, + getDefaultObjectMapper().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 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 AuthorizationDetail + */ + public static T fromJSON(final String authorizationDetailJson, + final Class clazz) { + + try { + if (StringUtils.isNotEmpty(authorizationDetailJson)) { + return getDefaultObjectMapper().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 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 AuthorizationDetail + * @see AuthorizationDetails + */ + public static String toJSON(final Set authorizationDetails) { + + try { + if (authorizationDetails != null) { + return getDefaultObjectMapper().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 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 AuthorizationDetail + * @see AuthorizationDetails + */ + public static String toJSON(final T authorizationDetail) { + + try { + if (authorizationDetail != null) { + return getDefaultObjectMapper().writeValueAsString(authorizationDetail); + } + } catch (JsonProcessingException e) { + log.debug("Error occurred while parsing AuthorizationDetail to String. Caused by, ", e); + } + return EMPTY_JSON_OBJECT; + } + + /** + * Converts a single {@code AuthorizationDetail} object into a {@link Map}. + *

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

+ * + * @param authorizationDetail the {@code AuthorizationDetail} object to convert + * @param the type parameter extending {@code AuthorizationDetail} + * @return a {@code Map} representation of the authorization detail, + * or an empty {@code HashMap} if null or an error occurs + * @see AuthorizationDetail + * @see AuthorizationDetails + */ + public static Map toMap(final T authorizationDetail) { + + return (authorizationDetail == null) ? Collections.emptyMap() + : getDefaultObjectMapper().convertValue(authorizationDetail, TYPE_MAP); + } + + /** + * Returns a configured default {@link ObjectMapper} instance. + * + *

This singleton ObjectMapper is configured to exclude properties with null values from the JSON output. + * + * @return a configured {@link ObjectMapper} instance. + */ + public static ObjectMapper getDefaultObjectMapper() { + if (objectMapper == null) { + synchronized (AuthorizationDetailsCommonUtils.class) { + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + } + } + return objectMapper; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsConstants.java b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsConstants.java new file mode 100644 index 00000000000..fb3749ae5a2 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsConstants.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, 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; + +/** + * Stores constants related to OAuth2 Rich Authorization Requests. + */ +public final class AuthorizationDetailsConstants { + + 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_FORMAT = "%s is an unknown authorization details type value"; + public static final String TYPE_VALIDATION_FAILED_ERR_MSG_FORMAT = + "The payload of the authorization details type '%s' contains errors: %s"; + public static final String SCHEMA_VALIDATION_FAILED_ERR_MSG_FORMAT = + "The schema of the authorization details type contains errors: %s"; + public static final String VALIDATION_FAILED_ERR_MSG = "Authorization details validation failed"; + public static final String VALIDATION_FAILED_ERR_CODE = "invalid_authorization_details"; + public static final String EMPTY_JSON_OBJECT = "{}"; + public static final String EMPTY_JSON_ARRAY = "[]"; + + private AuthorizationDetailsConstants() { + // Private constructor to prevent instantiation + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsSchemaValidatorTest.java b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsSchemaValidatorTest.java new file mode 100644 index 00000000000..740954a4aca --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsSchemaValidatorTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025, 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 com.fasterxml.jackson.core.JsonProcessingException; +import io.vertx.core.json.JsonObject; +import org.apache.commons.lang3.StringUtils; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.util.TestDAOUtils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_SCHEMA; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_TYPE; + +/** + * Test class for {@link AuthorizationDetailsSchemaValidator}. + */ +public class AuthorizationDetailsSchemaValidatorTest { + + private AuthorizationDetailsSchemaValidator uut; + + @BeforeClass + public void setUp() throws JsonProcessingException { + + this.uut = AuthorizationDetailsSchemaValidator.getInstance(); + } + + @Test + public void shouldReturnTrue_whenAuthorizationDetailIsSchemaCompliant() + throws AuthorizationDetailsProcessingException { + + AuthorizationDetail testAuthorizationDetail = new TestDAOUtils.TestAuthorizationDetail(); + testAuthorizationDetail.setType(TEST_TYPE); + + assertTrue(this.uut.isSchemaCompliant(TEST_SCHEMA, testAuthorizationDetail)); + assertTrue(this.uut.isSchemaCompliant(this.getTestSchema(), testAuthorizationDetail)); + } + + @Test + public void shouldReturnFalse_whenSchemaIsEmpty() throws AuthorizationDetailsProcessingException { + + assertFalse(this.uut.isSchemaCompliant(StringUtils.EMPTY, new TestDAOUtils.TestAuthorizationDetail())); + assertFalse(this.uut.isSchemaCompliant(TEST_SCHEMA, null)); + assertFalse(this.uut.isSchemaCompliant((JsonObject) null, new TestDAOUtils.TestAuthorizationDetail())); + assertFalse(this.uut.isSchemaCompliant(new JsonObject(), null)); + assertFalse(this.uut.isSchemaCompliant((Map) null, new TestDAOUtils.TestAuthorizationDetail())); + assertFalse(this.uut.isSchemaCompliant(this.getTestSchema(), null)); + } + + @Test(expectedExceptions = {AuthorizationDetailsProcessingException.class}) + public void shouldThrowAuthorizationDetailsProcessingException_whenJsonSchemaIsInvalid() + throws AuthorizationDetailsProcessingException { + + this.uut.isSchemaCompliant("{", new TestDAOUtils.TestAuthorizationDetail()); + } + + @Test(expectedExceptions = {AuthorizationDetailsProcessingException.class}) + public void shouldThrowAuthorizationDetailsProcessingException_whenAuthorizationDetailIsNotSchemaCompliant() + throws AuthorizationDetailsProcessingException { + + AuthorizationDetail testAuthorizationDetail = new TestDAOUtils.TestAuthorizationDetail(); + testAuthorizationDetail.setType(TEST_TYPE); + testAuthorizationDetail.setActions(Arrays.asList("initiate", "cancel")); + + assertTrue(this.uut.isSchemaCompliant(TEST_SCHEMA, testAuthorizationDetail)); + } + + @Test(expectedExceptions = {AuthorizationDetailsProcessingException.class}) + public void shouldThrowAuthorizationDetailsProcessingException_whenSchemaIsInvalid1() + throws AuthorizationDetailsProcessingException { + + final String invalidSchema = "{\"type\":\"object\",\"required\":[\"type\"]," + + "\"properties\":{\"type\":{\"type\":\"string\"},\"creditorName\":\"string\"}}"; + + assertTrue(this.uut.isValidSchema(TEST_SCHEMA)); + assertFalse(this.uut.isValidSchema(StringUtils.EMPTY)); + assertFalse(this.uut.isValidSchema(invalidSchema)); + } + + @Test(expectedExceptions = {AuthorizationDetailsProcessingException.class}) + public void shouldThrowAuthorizationDetailsProcessingException_whenSchemaIsInvalid2() + throws AuthorizationDetailsProcessingException { + + final String invalidSchema = "{\"type\":\"object\",\"required\":[\"type\"]," + + "\"properties\":[{\"type\":{\"type\":\"string\"}}]}"; + + assertFalse(this.uut.isValidSchema(invalidSchema)); + } + + private Map getTestSchema() { + final Map items = new HashMap<>(); + items.put("type", "string"); + items.put("enum", Collections.singletonList("initiate")); + + final Map actions = new HashMap<>(); + actions.put("type", "array"); + actions.put("items", items); + + final Map type = new HashMap<>(); + type.put("type", "string"); + type.put("enum", Collections.singletonList("test_type_v1")); + + final Map properties = new HashMap<>(); + properties.put("type", type); + properties.put("actions", actions); + + final Map schema = new HashMap<>(); + schema.put("type", "object"); + schema.put("required", Collections.singletonList("type")); + schema.put("properties", properties); + return schema; + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAOImplTest.java b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAOImplTest.java new file mode 100644 index 00000000000..da5782a96b7 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/dao/AuthorizationDetailsDAOImplTest.java @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2025, 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.dao; + +import org.apache.commons.lang3.StringUtils; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.api.resource.mgt.util.AuthorizationDetailsTypesUtil; +import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsCodeDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsTokenDTO; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.util.TestDAOUtils; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Set; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_AUTHORIZATION_CODE; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_CODE_ID; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_CONSENT_ID; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_DB_NAME; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_TENANT_ID; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_TOKEN_ID; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_TYPE; +import static org.wso2.carbon.identity.oauth2.rar.util.TestDAOUtils.closeMockedStatic; + +/** + * Test class for {@link AuthorizationDetailsDAO}. + */ +public class AuthorizationDetailsDAOImplTest { + + private MockedStatic identityDatabaseUtilMock; + private MockedStatic authorizationDetailsTypesUtilMock; + private AuthorizationDetailsDAO uut; + + @BeforeClass + public void setUp() throws SQLException { + this.uut = new AuthorizationDetailsDAOImpl(); + TestDAOUtils.initializeDataSource(TEST_DB_NAME, TestDAOUtils.getFilePath("h2.sql")); + this.identityDatabaseUtilMock = Mockito.mockStatic(IdentityDatabaseUtil.class); + this.authorizationDetailsTypesUtilMock = Mockito.mockStatic(AuthorizationDetailsTypesUtil.class); + } + + @AfterClass + public void tearDown() throws SQLException { + + closeMockedStatic(this.identityDatabaseUtilMock); + closeMockedStatic(this.authorizationDetailsTypesUtilMock); + } + + @BeforeMethod + public void setUpBeforeMethod() throws SQLException { + + this.mockIdentityDatabaseUtil(); + this.mockAuthorizationDetailsTypesUtil(true); + } + + @Test + public void testAddUserConsentedAuthorizationDetails() throws SQLException { + + assertEquals(0, this.uut.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TEST_TENANT_ID).size()); + + this.mockIdentityDatabaseUtil(); + + AuthorizationDetail testAuthorizationDetail = new AuthorizationDetail(); + testAuthorizationDetail.setType(TEST_TYPE); + + AuthorizationDetailsConsentDTO consentDTO = + new AuthorizationDetailsConsentDTO(TEST_CONSENT_ID, testAuthorizationDetail, true, TEST_TENANT_ID); + int[] result = uut.addUserConsentedAuthorizationDetails(Collections.singleton(consentDTO)); + + assertEquals(1, result.length); + } + + @Test(priority = 1) + public void testGetUserConsentedAuthorizationDetails() throws SQLException { + + Set consentDTOs = + this.uut.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TEST_TENANT_ID); + + assertEquals(1, consentDTOs.size()); + consentDTOs.forEach(dto -> { + assertEquals(TEST_CONSENT_ID, dto.getConsentId()); + assertNotNull(dto.getAuthorizationDetail()); + assertEquals(TEST_TYPE, dto.getAuthorizationDetail().getType()); + }); + } + + @Test(priority = 2) + public void testUpdateUserConsentedAuthorizationDetails() throws SQLException { + + final String identifier = UUID.randomUUID().toString(); + Set existingConsentDTOs = + this.uut.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TEST_TENANT_ID); + + this.mockIdentityDatabaseUtil(); + + AuthorizationDetailsConsentDTO existingDTO = existingConsentDTOs.iterator().next(); + Assert.assertTrue(StringUtils.isEmpty(existingDTO.getAuthorizationDetail().getIdentifier())); + + AuthorizationDetail authorizationDetailToUpdate = existingDTO.getAuthorizationDetail(); + authorizationDetailToUpdate.setIdentifier(identifier); + + AuthorizationDetailsConsentDTO consentDTO = new AuthorizationDetailsConsentDTO(existingDTO.getConsentId(), + authorizationDetailToUpdate, existingDTO.isConsentActive(), existingDTO.getTenantId()); + + int[] result = uut.updateUserConsentedAuthorizationDetails(Collections.singleton(consentDTO)); + assertEquals(1, result.length); + + this.mockIdentityDatabaseUtil(); + + Set updatedConsentDTOs = + this.uut.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TEST_TENANT_ID); + AuthorizationDetailsConsentDTO updatedDto = updatedConsentDTOs.iterator().next(); + + assertEquals(existingConsentDTOs.size(), updatedConsentDTOs.size()); + assertEquals(existingDTO.getAuthorizationDetail().getType(), updatedDto.getAuthorizationDetail().getType()); + assertEquals(identifier, updatedDto.getAuthorizationDetail().getIdentifier()); + } + + @Test(dependsOnMethods = "testUpdateUserConsentedAuthorizationDetails") + public void testDeleteUserConsentedAuthorizationDetails() throws SQLException { + + assertEquals(1, uut.deleteUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TEST_TENANT_ID)); + + this.mockIdentityDatabaseUtil(); + + assertEquals(0, this.uut.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TEST_TENANT_ID).size()); + } + + @Test + public void testAddAccessTokenAuthorizationDetails() throws SQLException { + assertEquals(0, this.uut.getAccessTokenAuthorizationDetails(TEST_TOKEN_ID, TEST_TENANT_ID).size()); + + this.mockIdentityDatabaseUtil(); + + AuthorizationDetail testAuthorizationDetail = new AuthorizationDetail(); + testAuthorizationDetail.setType(TEST_TYPE); + + AuthorizationDetailsTokenDTO tokenDTO = + new AuthorizationDetailsTokenDTO(TEST_TOKEN_ID, testAuthorizationDetail, TEST_TENANT_ID); + + int[] result = uut.addAccessTokenAuthorizationDetails(Collections.singleton(tokenDTO)); + + assertEquals(1, result.length); + } + + @Test(priority = 1) + public void testGetAccessTokenAuthorizationDetails() throws SQLException { + Set tokenDTOs = + this.uut.getAccessTokenAuthorizationDetails(TEST_TOKEN_ID, TEST_TENANT_ID); + + assertEquals(1, tokenDTOs.size()); + tokenDTOs.forEach(dto -> { + assertEquals(TEST_TOKEN_ID, dto.getAccessTokenId()); + assertNotNull(dto.getAuthorizationDetail()); + assertEquals(TEST_TYPE, dto.getAuthorizationDetail().getType()); + }); + } + + @Test(priority = 2) + public void testDeleteAccessTokenAuthorizationDetails() throws SQLException { + assertEquals(1, uut.deleteAccessTokenAuthorizationDetails(TEST_TOKEN_ID, TEST_TENANT_ID)); + + this.mockIdentityDatabaseUtil(); + + assertEquals(0, this.uut.getAccessTokenAuthorizationDetails(TEST_TOKEN_ID, TEST_TENANT_ID).size()); + } + + @Test + public void testAddOAuth2CodeAuthorizationDetails() throws SQLException { + assertEquals(0, this.uut.getOAuth2CodeAuthorizationDetails(TEST_CODE_ID, TEST_TENANT_ID).size()); + + this.mockIdentityDatabaseUtil(); + + AuthorizationDetail testAuthorizationDetail = new AuthorizationDetail(); + testAuthorizationDetail.setType(TEST_TYPE); + + AuthorizationDetailsCodeDTO codeDTO = + new AuthorizationDetailsCodeDTO(TEST_CODE_ID, testAuthorizationDetail, TEST_TENANT_ID); + + int[] result = uut.addOAuth2CodeAuthorizationDetails(Collections.singleton(codeDTO)); + + assertEquals(1, result.length); + } + + @Test(priority = 1) + public void testGetOAuth2CodeAuthorizationDetails() throws SQLException { + Set codeDTOs = + this.uut.getOAuth2CodeAuthorizationDetails(TEST_AUTHORIZATION_CODE, TEST_TENANT_ID); + + assertEquals(1, codeDTOs.size()); + codeDTOs.forEach(dto -> { + assertEquals(TEST_CODE_ID, dto.getAuthorizationCodeId()); + assertNotNull(dto.getAuthorizationDetail()); + assertEquals(TEST_TYPE, dto.getAuthorizationDetail().getType()); + }); + } + + @Test(priority = 3, expectedExceptions = {SQLException.class}) + public void shouldThrowSQLException_whenAddingConsentedAuthorizationDetailsFails() throws SQLException { + + try (Connection connectionMock = Mockito.spy(Connection.class)) { + + Mockito.when(connectionMock.prepareStatement(anyString())).thenThrow(SQLException.class); + identityDatabaseUtilMock + .when(() -> IdentityDatabaseUtil.getDBConnection(any(Boolean.class))) + .thenReturn(connectionMock); + uut.addUserConsentedAuthorizationDetails(Collections.emptySet()); + } + } + + @Test(priority = 3, expectedExceptions = {SQLException.class}) + public void shouldThrowSQLException_whenGettingConsentedAuthorizationDetailsFails() throws SQLException { + + try (Connection connectionMock = Mockito.spy(Connection.class)) { + + Mockito.when(connectionMock.prepareStatement(anyString())).thenThrow(SQLException.class); + identityDatabaseUtilMock + .when(() -> IdentityDatabaseUtil.getDBConnection(any(Boolean.class))) + .thenReturn(connectionMock); + uut.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TEST_TENANT_ID); + } + } + + @Test + public void testGetConsentIdByUserIdAndAppId() throws SQLException { + + assertNotNull(this.uut.getConsentIdByUserIdAndAppId("valid_user_id", "valid_app_id", TEST_TENANT_ID)); + } + + @Test + public void shouldReturnNull_whenUserIdOrAppIdInvalid() throws SQLException { + + assertNull(this.uut.getConsentIdByUserIdAndAppId("invalid_user_id", "invalid_app_id", TEST_TENANT_ID)); + } + + private void mockAuthorizationDetailsTypesUtil(boolean isRichAuthorizationRequestsEnabled) { + + this.authorizationDetailsTypesUtilMock + .when(AuthorizationDetailsTypesUtil::isRichAuthorizationRequestsDisabled) + .thenReturn(!isRichAuthorizationRequestsEnabled); + } + + private void mockIdentityDatabaseUtil() throws SQLException { + + this.identityDatabaseUtilMock + .when(() -> IdentityDatabaseUtil.getDBConnection(any(Boolean.class))) + .thenReturn(TestDAOUtils.getConnection(TEST_DB_NAME)); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsCommonUtilsTest.java b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsCommonUtilsTest.java new file mode 100644 index 00000000000..e06d97c88f4 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsCommonUtilsTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025, 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.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.testng.collections.Sets; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.EMPTY_JSON_ARRAY; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.EMPTY_JSON_OBJECT; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_NAME; +import static org.wso2.carbon.identity.oauth2.rar.util.TestConstants.TEST_TYPE; + +/** + * Test class for {@link AuthorizationDetailsCommonUtils}. + */ +public class AuthorizationDetailsCommonUtilsTest { + + @DataProvider(name = "AuthorizationDetailsCommonUtilsTestDataProvider") + public Object[][] provideAuthorizationDetailsCommonUtilsTestData(Method testMethod) { + + switch (testMethod.getName()) { + case "shouldReturnNull_whenJSONIsInvalid": + case "shouldReturnCorrectSize_whenJSONArrayIsValid": + return new Object[][]{ + {null, 0}, + {"", 0}, + {" ", 0}, + {"invalid JSON", 0}, + {"[]", 0}, + {"[{}]", 1}, + {"[{},{}]", 2} + }; + case "shouldReturnCorrectType_whenJSONIsValid": + return new Object[][]{ + {AuthorizationDetail.class}, + {TestDAOUtils.TestAuthorizationDetail.class} + }; + } + return null; + } + + @Test(dataProvider = "AuthorizationDetailsCommonUtilsTestDataProvider") + public void shouldReturnCorrectSize_whenJSONArrayIsValid(String inputJson, int expectedSize) { + + Set actualAuthorizationDetails = + AuthorizationDetailsCommonUtils.fromJSONArray(inputJson, AuthorizationDetail.class); + + assertEquals(expectedSize, actualAuthorizationDetails.size()); + } + + @Test(dataProvider = "AuthorizationDetailsCommonUtilsTestDataProvider") + public void shouldReturnNull_whenJSONIsInvalid(String inputJson, int expectedSize) { + + assertNull(AuthorizationDetailsCommonUtils.fromJSON(inputJson, AuthorizationDetail.class)); + } + + @Test(dataProvider = "AuthorizationDetailsCommonUtilsTestDataProvider") + public void shouldReturnCorrectType_whenJSONIsValid(Class expectedClazz) { + + final String inputJson = "{\"type\": \"" + TEST_TYPE + "\"}"; + AuthorizationDetail actualAuthorizationDetail = + AuthorizationDetailsCommonUtils.fromJSON(inputJson, expectedClazz); + + assertNotNull(actualAuthorizationDetail); + assertEquals(TEST_TYPE, actualAuthorizationDetail.getType()); + } + + @Test + public void shouldReturnCorrectJson_whenAuthorizationDetailsAreValid() { + + TestDAOUtils.TestAuthorizationDetail inputAuthorizationDetail = new TestDAOUtils.TestAuthorizationDetail(); + inputAuthorizationDetail.setType(TEST_TYPE); + inputAuthorizationDetail.setName(TEST_NAME); + + final String authorizationDetails = + AuthorizationDetailsCommonUtils.toJSON(Sets.newHashSet(inputAuthorizationDetail)); + + assertTrue(authorizationDetails.contains(TEST_TYPE)); + assertTrue(authorizationDetails.contains(TEST_NAME)); + assertEquals(EMPTY_JSON_ARRAY, AuthorizationDetailsCommonUtils.toJSON((Set) null)); + } + + @Test + public void shouldReturnCorrectJson_whenAuthorizationDetailIsValid() { + + TestDAOUtils.TestAuthorizationDetail inputAuthorizationDetail = new TestDAOUtils.TestAuthorizationDetail(); + inputAuthorizationDetail.setType(TEST_TYPE); + inputAuthorizationDetail.setName(TEST_NAME); + + final String authorizationDetail = AuthorizationDetailsCommonUtils.toJSON(inputAuthorizationDetail); + + assertTrue(authorizationDetail.contains(TEST_TYPE)); + assertTrue(authorizationDetail.contains(TEST_NAME)); + assertEquals(EMPTY_JSON_OBJECT, + AuthorizationDetailsCommonUtils.toJSON((TestDAOUtils.TestAuthorizationDetail) null)); + assertEquals(EMPTY_JSON_OBJECT, + AuthorizationDetailsCommonUtils.toJSON(new TestDAOUtils.TestAuthorizationDetail())); + } + + @Test + public void shouldReturnMap_whenAuthorizationDetailIsValid() { + + TestDAOUtils.TestAuthorizationDetail inputAuthorizationDetail = new TestDAOUtils.TestAuthorizationDetail(); + inputAuthorizationDetail.setType(TEST_TYPE); + inputAuthorizationDetail.setName(TEST_NAME); + Map map = AuthorizationDetailsCommonUtils.toMap(inputAuthorizationDetail); + + assertTrue(map.containsKey("type")); + assertTrue(map.containsKey("name")); + assertEquals(TEST_TYPE, String.valueOf(map.get("type"))); + assertEquals(TEST_NAME, String.valueOf(map.get("name"))); + assertEquals(2, map.keySet().size()); + + assertFalse(AuthorizationDetailsCommonUtils.toMap(null).containsKey(TEST_TYPE)); + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/TestConstants.java b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/TestConstants.java new file mode 100644 index 00000000000..fa3a9d21f80 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/TestConstants.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025, 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; + +/** + * Rich Authorization Requests Test Constants. + */ +public class TestConstants { + + public static final String TEST_AUTHORIZATION_CODE = "b1b833f0-f605-4f5c-add6-38ea8ce1b969"; + public static final String TEST_CODE_ID = "81197bc6-63f3-4c0f-90dd-1588076ab50f"; + public static final String TEST_CONSENT_ID = "52481ccd-0927-4d17-8cfc-5110fc4aa009"; + public static final String TEST_DB_NAME = "TEST_IAM_RAR_DATABASE"; + public static final int TEST_TENANT_ID = 1234; + public static final String TEST_TOKEN_ID = "e1fea951-a3b5-4347-bd73-b18b3feecd54"; + public static final String TEST_TYPE = "test_type_v1"; + public static final String TEST_NAME = "test_name_v1"; + public static final String TEST_SCHEMA = "{\"type\":\"object\",\"required\":[\"type\"],\"properties\":" + + "{\"type\":{\"type\":\"string\",\"enum\":[\"test_type_v1\"]},\"actions\":{\"type\":\"array\"," + + "\"items\":{\"type\":\"string\",\"enum\":[\"initiate\"]}}}}"; + + private TestConstants() { + // Private constructor to prevent instantiation + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/TestDAOUtils.java b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/TestDAOUtils.java new file mode 100644 index 00000000000..f89d9b8f10d --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/test/java/org/wso2/carbon/identity/oauth2/rar/util/TestDAOUtils.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025, 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.dbcp.BasicDataSource; +import org.apache.commons.lang.StringUtils; +import org.mockito.MockedStatic; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; + +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +/** + * Test DB Utils. + */ +public class TestDAOUtils { + + private static final Map dataSourceMap = new HashMap<>(); + + public static void initializeDataSource(String databaseName, String scriptPath) throws SQLException { + + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUsername("username"); + dataSource.setPassword("password"); + dataSource.setUrl("jdbc:h2:mem:" + databaseName); + + try (Connection connection = dataSource.getConnection()) { + connection.createStatement().executeUpdate("RUNSCRIPT FROM '" + scriptPath + "'"); + } + dataSourceMap.put(databaseName, dataSource); + } + + public static Connection getConnection(String database) throws SQLException { + + if (dataSourceMap.get(database) != null) { + return dataSourceMap.get(database).getConnection(); + } + throw new RuntimeException("Invalid datasource."); + } + + public static String getFilePath(String fileName) { + + if (StringUtils.isNotBlank(fileName)) { + return Paths.get(System.getProperty("user.dir"), "src", "test", "resources", "dbScripts", fileName) + .toString(); + } + return null; + } + + public static void closeMockedStatic(final MockedStatic mockedStatic) { + + if (mockedStatic != null && !mockedStatic.isClosed()) { + mockedStatic.close(); + } + } + + /** + * Test authorization detail class which extends AuthorizationDetail + */ + public static class TestAuthorizationDetail extends AuthorizationDetail { + + private String name; + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + } +} diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/test/resources/dbScripts/h2.sql b/components/org.wso2.carbon.identity.oauth.rar/src/test/resources/dbScripts/h2.sql new file mode 100644 index 00000000000..83f076e4bc9 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/test/resources/dbScripts/h2.sql @@ -0,0 +1,71 @@ +CREATE TABLE IF NOT EXISTS AUTHORIZATION_DETAILS_TYPES( + ID INTEGER NOT NULL AUTO_INCREMENT, + TYPE VARCHAR(255) NOT NULL, + CURSOR_KEY INTEGER DEFAULT 1, + NAME VARCHAR(255), + DESCRIPTION VARCHAR (255), + JSON_SCHEMA CLOB NOT NULL, + TENANT_ID INTEGER DEFAULT -1 +); + +CREATE TABLE IF NOT EXISTS IDN_OAUTH2_USER_CONSENTED_AUTHORIZATION_DETAILS ( + ID INTEGER NOT NULL AUTO_INCREMENT, + CONSENT_ID VARCHAR(255) NOT NULL, + TYPE_ID INTEGER NOT NULL, + AUTHORIZATION_DETAILS CLOB NOT NULL, + CONSENT BOOLEAN NOT NULL DEFAULT 1, + TENANT_ID INTEGER NOT NULL DEFAULT -1 +); + +CREATE TABLE IF NOT EXISTS IDN_OAUTH2_ACCESS_TOKEN_AUTHORIZATION_DETAILS ( + ID INTEGER NOT NULL AUTO_INCREMENT, + TYPE_ID INTEGER NOT NULL, + AUTHORIZATION_DETAILS CLOB NOT NULL, + TOKEN_ID VARCHAR (255), + TENANT_ID INTEGER DEFAULT -1 +); + +CREATE TABLE IF NOT EXISTS IDN_OAUTH2_AUTHZ_CODE_AUTHORIZATION_DETAILS ( + ID INTEGER NOT NULL AUTO_INCREMENT, + TYPE_ID CHAR(36) NOT NULL, + AUTHORIZATION_DETAILS CLOB NOT NULL, + CODE_ID VARCHAR (255), + TENANT_ID INTEGER +); + +CREATE TABLE IF NOT EXISTS IDN_OAUTH2_AUTHORIZATION_CODE ( + CODE_ID VARCHAR (255), + AUTHORIZATION_CODE VARCHAR (2048), + CONSUMER_KEY_ID INTEGER, + CALLBACK_URL VARCHAR (2048), + SCOPE VARCHAR(2048), + AUTHZ_USER VARCHAR (100), + TENANT_ID INTEGER, + USER_DOMAIN VARCHAR(50), + TIME_CREATED TIMESTAMP, + VALIDITY_PERIOD BIGINT, + STATE VARCHAR (25) DEFAULT 'ACTIVE', + TOKEN_ID VARCHAR(255), + SUBJECT_IDENTIFIER VARCHAR(255), + PKCE_CODE_CHALLENGE VARCHAR (255), + PKCE_CODE_CHALLENGE_METHOD VARCHAR(128), + AUTHORIZATION_CODE_HASH VARCHAR (512), + IDP_ID INTEGER DEFAULT -1 NOT NULL +); + +CREATE TABLE IF NOT EXISTS IDN_OAUTH2_USER_CONSENT ( + ID INTEGER NOT NULL AUTO_INCREMENT, + USER_ID VARCHAR(255) NOT NULL, + APP_ID CHAR(36) NOT NULL, + TENANT_ID INTEGER NOT NULL DEFAULT -1, + CONSENT_ID VARCHAR(255) NOT NULL +); + +INSERT INTO AUTHORIZATION_DETAILS_TYPES (TYPE, NAME, DESCRIPTION, JSON_SCHEMA, TENANT_ID) +VALUES ('test_type_v1', 'Test Type', 'Test Type V1', '{}', 1234); + +INSERT INTO IDN_OAUTH2_AUTHORIZATION_CODE (CODE_ID, AUTHORIZATION_CODE, IDP_ID) +VALUES ('81197bc6-63f3-4c0f-90dd-1588076ab50f', 'b1b833f0-f605-4f5c-add6-38ea8ce1b969', 1); + +INSERT INTO IDN_OAUTH2_USER_CONSENT (USER_ID, APP_ID, TENANT_ID, CONSENT_ID) +VALUES ('valid_user_id', 'valid_app_id', 1234, '52481ccd-0927-4d17-8cfc-5110fc4aa009'); diff --git a/components/org.wso2.carbon.identity.oauth.rar/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.oauth.rar/src/test/resources/testng.xml new file mode 100644 index 00000000000..9a683e2a9c4 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth.rar/src/test/resources/testng.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/org.wso2.carbon.identity.oauth/pom.xml b/components/org.wso2.carbon.identity.oauth/pom.xml index 19db1dcdfda..c61d6a4c0dc 100644 --- a/components/org.wso2.carbon.identity.oauth/pom.xml +++ b/components/org.wso2.carbon.identity.oauth/pom.xml @@ -262,6 +262,10 @@ org.wso2.orbit.javax.xml.bind jaxb-api + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar + org.testng 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..8b4e6775007 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.model.AuthorizationDetails; import java.io.Serializable; import java.util.Properties; @@ -56,6 +57,10 @@ public class OAuthAuthzReqMessageContext implements Serializable { private Properties properties = new Properties(); + private AuthorizationDetails approvedAuthorizationDetails; + + private AuthorizationDetails requestedAuthorizationDetails; + public OAuthAuthzReqMessageContext(OAuth2AuthorizeReqDTO authorizationReqDTO) { this.authorizationReqDTO = authorizationReqDTO; @@ -212,4 +217,48 @@ public void setSubjectTokenFlow(boolean subjectTokenFlow) { isSubjectTokenFlow = subjectTokenFlow; } + + /** + * Retrieves the user approved authorization details. + * + * @return the {@link AuthorizationDetails} instance representing the approved authorization information. + * If no authorization details are available, it will return {@code null}. + */ + public AuthorizationDetails getApprovedAuthorizationDetails() { + + return this.approvedAuthorizationDetails; + } + + /** + * Sets the approved authorization details. + * This method updates the approved authorization details with the provided {@link AuthorizationDetails} instance. + * + * @param approvedAuthorizationDetails the approved {@link AuthorizationDetails} to set. + */ + public void setApprovedAuthorizationDetails(final AuthorizationDetails approvedAuthorizationDetails) { + + this.approvedAuthorizationDetails = approvedAuthorizationDetails; + } + + /** + * Retrieves the requested authorization details. + * + * @return the {@link AuthorizationDetails} instance representing the authorization information came in the request. + * If no authorization details are available, it will return {@code null}. + */ + public AuthorizationDetails getRequestedAuthorizationDetails() { + + return this.requestedAuthorizationDetails; + } + + /** + * Sets the requested authorization details. + * This method updates the requested authorization details with the provided {@link AuthorizationDetails} instance. + * + * @param requestedAuthorizationDetails the requested {@link AuthorizationDetails} to set. + */ + public void setRequestedAuthorizationDetails(final AuthorizationDetails requestedAuthorizationDetails) { + + this.requestedAuthorizationDetails = requestedAuthorizationDetails; + } } 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..89c099d9a20 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 @@ -225,6 +225,7 @@ public OAuth2AuthorizeRespDTO initResponse(OAuthAuthzReqMessageContext oauthAuth OAuth2AuthorizeReqDTO authorizationReqDTO = oauthAuthzMsgCtx.getAuthorizationReqDTO(); respDTO.setCallbackURI(authorizationReqDTO.getCallbackUrl()); respDTO.setScope(oauthAuthzMsgCtx.getApprovedScope()); + respDTO.setAuthorizationDetails(oauthAuthzMsgCtx.getApprovedAuthorizationDetails()); return respDTO; } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/util/ResponseTypeHandlerUtil.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/util/ResponseTypeHandlerUtil.java index 08696c48c9d..1091906da03 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/util/ResponseTypeHandlerUtil.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/authz/handlers/util/ResponseTypeHandlerUtil.java @@ -299,6 +299,9 @@ public static AuthzCodeDO generateAuthorizationCode(OAuthAuthzReqMessageContext OAuthTokenPersistenceFactory.getInstance().getAuthorizationCodeDAO() .insertAuthorizationCode(authorizationCode, authorizationReqDTO.getConsumerKey(), appTenant, authorizationReqDTO.getCallbackUrl(), authzCodeDO); + + OAuth2ServiceComponentHolder.getInstance().getAuthorizationDetailsService() + .storeAuthorizationCodeAuthorizationDetails(authzCodeDO, oauthAuthzMsgCtx); } else { OAuthTokenPersistenceFactory.getInstance().getAuthorizationCodeDAO() .insertAuthorizationCode(authorizationCode, authorizationReqDTO.getConsumerKey(), @@ -629,6 +632,9 @@ private static AccessTokenDO generateNewAccessToken(OAuthAuthzReqMessageContext // Persist the access token in database persistAccessTokenInDB(oauthAuthzMsgCtx, existingTokenBean, newTokenBean); deactivateCurrentAuthorizationCode(newTokenBean.getAuthorizationCode(), newTokenBean.getTokenId()); + // Persist access token authorization details in database + OAuth2ServiceComponentHolder.getInstance().getAuthorizationDetailsService() + .storeAccessTokenAuthorizationDetails(newTokenBean, oauthAuthzMsgCtx); //update cache with newly added token if (isHashDisabled && cacheEnabled) { addTokenToCache(getOAuthCacheKey(consumerKey, scope, authorizedUserId, authenticatedIDP), 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..e38b6ad272d 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.dao.AuthorizationDetailsDAO; +import org.wso2.carbon.identity.oauth2.rar.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.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/dto/OAuth2AccessTokenReqDTO.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AccessTokenReqDTO.java index 193a61fd304..d6f98c73b37 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AccessTokenReqDTO.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AccessTokenReqDTO.java @@ -22,6 +22,7 @@ import org.wso2.carbon.identity.oauth2.model.AccessTokenExtendedAttributes; import org.wso2.carbon.identity.oauth2.model.HttpRequestHeader; import org.wso2.carbon.identity.oauth2.model.RequestParameter; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; import java.util.ArrayList; import java.util.Collections; @@ -62,6 +63,8 @@ public class OAuth2AccessTokenReqDTO { private AccessTokenExtendedAttributes accessTokenExtendedAttributes; + private AuthorizationDetails authorizationDetails; + public String getClientId() { return clientId; } @@ -252,4 +255,26 @@ public HttpServletResponseWrapper getHttpServletResponseWrapper() { public void setHttpServletResponseWrapper(HttpServletResponseWrapper httpServletResponseWrapper) { this.httpServletResponseWrapper = httpServletResponseWrapper; } + + /** + * Retrieves the authorization details requested in the token request. + * + * @return the {@link AuthorizationDetails} instance representing the rich authorization requests. + * If no authorization details are requested by the client, the method 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/dto/OAuth2AuthorizeReqDTO.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AuthorizeReqDTO.java index 94cc22c362c..f1ed3504fd6 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AuthorizeReqDTO.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AuthorizeReqDTO.java @@ -21,6 +21,7 @@ import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.application.common.model.ClaimMapping; import org.wso2.carbon.identity.oauth2.model.HttpRequestHeader; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; import org.wso2.carbon.identity.openidconnect.model.RequestObject; import java.util.LinkedHashSet; @@ -64,6 +65,7 @@ public class OAuth2AuthorizeReqDTO { private String state; private String requestedSubjectId; private Map mappedRemoteClaims; + private AuthorizationDetails authorizationDetails; public String getRequestedSubjectId() { @@ -317,4 +319,26 @@ public void setMappedRemoteClaims( this.mappedRemoteClaims = mappedRemoteClaims; } + + /** + * Retrieves the authorization details requested by the client. + * + * @return the {@link AuthorizationDetails} instance representing the {@code authorization_details} requested + * by the client. If no authorization details are available, it will return {@code null}. + */ + public AuthorizationDetails getAuthorizationDetails() { + + return this.authorizationDetails; + } + + /** + * Sets the authorization details requested by the client. + * 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/dto/OAuth2AuthorizeRespDTO.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AuthorizeRespDTO.java index 6cb706807c4..8c248bfc4b8 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AuthorizeRespDTO.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/dto/OAuth2AuthorizeRespDTO.java @@ -18,6 +18,8 @@ package org.wso2.carbon.identity.oauth2.dto; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; + import java.util.Properties; /** @@ -39,6 +41,7 @@ public class OAuth2AuthorizeRespDTO { private String pkceCodeChallenge; private String pkceCodeChallengeMethod; private String oidcSessionId; + private AuthorizationDetails authorizationDetails; private String subjectToken; public String getAuthorizationCode() { @@ -200,4 +203,26 @@ public void setSubjectToken(String subjectToken) { this.subjectToken = subjectToken; } + + /** + * Retrieves the validated authorization details to be included in the authorize response. + * + * @return the {@link AuthorizationDetails} instance representing the validated 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 sets {@link AuthorizationDetails} that can potentially be included in the authorization response. + * + * @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/internal/OAuth2ServiceComponent.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/internal/OAuth2ServiceComponent.java index ef4ebcd2777..c219826497a 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/internal/OAuth2ServiceComponent.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/internal/OAuth2ServiceComponent.java @@ -33,6 +33,7 @@ import org.osgi.service.component.annotations.ReferencePolicy; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.identity.api.resource.mgt.APIResourceManager; +import org.wso2.carbon.identity.api.resource.mgt.AuthorizationDetailsTypeManager; import org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticationService; import org.wso2.carbon.identity.application.authentication.framework.AuthenticationDataPublisher; import org.wso2.carbon.identity.application.authentication.framework.AuthenticationMethodNameTranslator; @@ -58,6 +59,7 @@ import org.wso2.carbon.identity.oauth.tokenprocessor.OAuth2RevocationProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.RefreshTokenGrantProcessor; import org.wso2.carbon.identity.oauth.tokenprocessor.TokenProvider; +import org.wso2.carbon.identity.oauth2.IntrospectionDataProvider; import org.wso2.carbon.identity.oauth2.OAuth2ScopeService; import org.wso2.carbon.identity.oauth2.OAuth2Service; import org.wso2.carbon.identity.oauth2.OAuth2TokenValidationService; @@ -85,6 +87,11 @@ import org.wso2.carbon.identity.oauth2.keyidprovider.KeyIDProvider; import org.wso2.carbon.identity.oauth2.listener.TenantCreationEventListener; import org.wso2.carbon.identity.oauth2.model.ResourceAccessControlKey; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessor; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessorFactory; +import org.wso2.carbon.identity.oauth2.rar.token.AccessTokenResponseRARHandler; +import org.wso2.carbon.identity.oauth2.rar.token.IntrospectionRARDataProvider; +import org.wso2.carbon.identity.oauth2.rar.token.JWTAccessTokenRARClaimProvider; import org.wso2.carbon.identity.oauth2.scopeservice.APIResourceBasedScopeMetadataService; import org.wso2.carbon.identity.oauth2.scopeservice.ScopeMetadataService; import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinder; @@ -405,6 +412,11 @@ protected void activate(ComponentContext context) { bundleContext.registerService(ImpersonationConfigMgtService.class, new ImpersonationConfigMgtServiceImpl(), null); + bundleContext.registerService(AccessTokenResponseHandler.class, new AccessTokenResponseRARHandler(), null); + bundleContext.registerService(JWTAccessTokenClaimProvider.class, + new JWTAccessTokenRARClaimProvider(), null); + bundleContext.registerService(IntrospectionDataProvider.class, new IntrospectionRARDataProvider(), null); + // Note : DO NOT add any activation related code below this point, // to make sure the server doesn't start up if any activation failures occur } catch (Throwable e) { @@ -1662,4 +1674,71 @@ protected void unregisterClaimMetadataManagementService( OAuth2ServiceComponentHolder.getInstance().setClaimMetadataManagementService(null); } + + /** + * Registers the {@link AuthorizationDetailsTypeManager} service. + * + * @param typeManager The {@code AuthorizationDetailsTypeManager} instance. + */ + @Reference( + name = "org.wso2.carbon.identity.api.resource.mgt.AuthorizationDetailsTypeManager", + service = AuthorizationDetailsTypeManager.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unregisterAuthorizationDetailsTypeManager" + ) + protected void registerAuthorizationDetailsTypeManager(AuthorizationDetailsTypeManager typeManager) { + + if (log.isDebugEnabled()) { + log.debug("Registering the AuthorizationDetailsTypeManager service."); + } + OAuth2ServiceComponentHolder.getInstance().setAuthorizationDetailsTypeManager(typeManager); + } + + + /** + * Unset the {@link AuthorizationDetailsTypeManager} service. + * + * @param typeManager The {@code AuthorizationDetailsTypeManager} instance. + */ + protected void unregisterAuthorizationDetailsTypeManager(AuthorizationDetailsTypeManager typeManager) { + + if (log.isDebugEnabled()) { + log.debug("Unregistering the AuthorizationDetailsTypeManager service."); + } + OAuth2ServiceComponentHolder.getInstance().setAuthorizationDetailsTypeManager(null); + } + + /** + * Registers the {@link AuthorizationDetailsProcessor} service. + * + * @param processor The {@code AuthorizationDetailsProcessor} instance. + */ + @Reference( + name = "org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessor", + service = AuthorizationDetailsProcessor.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.DYNAMIC, + unbind = "unregisterAuthorizationDetailsProcessor" + ) + protected void registerAuthorizationDetailsProcessor(AuthorizationDetailsProcessor processor) { + + if (log.isDebugEnabled()) { + log.debug("Registering the AuthorizationDetailsProcessor service."); + } + AuthorizationDetailsProcessorFactory.getInstance().setAuthorizationDetailsProcessors(processor); + } + + /** + * Unset the {@link AuthorizationDetailsProcessor} service. + * + * @param processor The {@code AuthorizationDetailsProcessor} instance. + */ + protected void unregisterAuthorizationDetailsProcessor(AuthorizationDetailsProcessor processor) { + + if (log.isDebugEnabled()) { + log.debug("Unregistering the AuthorizationDetailsProcessor service."); + } + AuthorizationDetailsProcessorFactory.getInstance().setAuthorizationDetailsProcessors(null); + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/internal/OAuth2ServiceComponentHolder.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/internal/OAuth2ServiceComponentHolder.java index fe7896869c6..ada9ba84808 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/internal/OAuth2ServiceComponentHolder.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/internal/OAuth2ServiceComponentHolder.java @@ -19,6 +19,7 @@ package org.wso2.carbon.identity.oauth2.internal; import org.wso2.carbon.identity.api.resource.mgt.APIResourceManager; +import org.wso2.carbon.identity.api.resource.mgt.AuthorizationDetailsTypeManager; import org.wso2.carbon.identity.application.authentication.framework.AuthenticationDataPublisher; import org.wso2.carbon.identity.application.authentication.framework.AuthenticationMethodNameTranslator; import org.wso2.carbon.identity.application.authentication.framework.UserSessionManagementService; @@ -46,6 +47,9 @@ import org.wso2.carbon.identity.oauth2.impersonation.services.ImpersonationMgtService; import org.wso2.carbon.identity.oauth2.impersonation.validators.ImpersonationValidator; import org.wso2.carbon.identity.oauth2.keyidprovider.KeyIDProvider; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.validator.AuthorizationDetailsValidator; +import org.wso2.carbon.identity.oauth2.rar.validator.DefaultAuthorizationDetailsValidator; import org.wso2.carbon.identity.oauth2.responsemode.provider.ResponseModeProvider; import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinder; import org.wso2.carbon.identity.oauth2.token.handlers.claims.JWTAccessTokenClaimProvider; @@ -127,6 +131,10 @@ public class OAuth2ServiceComponentHolder { private static AccountLockService accountLockService; private ClaimMetadataManagementService claimMetadataManagementService; + private AuthorizationDetailsService authorizationDetailsService; + private AuthorizationDetailsValidator authorizationDetailsValidator; + private AuthorizationDetailsTypeManager authorizationDetailsTypeManager; + private OAuth2ServiceComponentHolder() { } @@ -932,4 +940,50 @@ public ClaimMetadataManagementService getClaimMetadataManagementService() { return claimMetadataManagementService; } + + /** + * Get an {@link AuthorizationDetailsService} instance. + * + * @return A {@link AuthorizationDetailsService} singleton instance. + */ + public AuthorizationDetailsService getAuthorizationDetailsService() { + + if (this.authorizationDetailsService == null) { + this.authorizationDetailsService = new AuthorizationDetailsService(); + } + return this.authorizationDetailsService; + } + + /** + * Get an {@link AuthorizationDetailsValidator} instance. + * + * @return A {@link AuthorizationDetailsValidator} singleton instance. + */ + public AuthorizationDetailsValidator getAuthorizationDetailsValidator() { + + if (this.authorizationDetailsValidator == null) { + this.authorizationDetailsValidator = new DefaultAuthorizationDetailsValidator(); + } + return this.authorizationDetailsValidator; + } + + /** + * Get an {@link AuthorizationDetailsTypeManager} instance. + * + * @return A {@link AuthorizationDetailsTypeManager} singleton instance. + */ + public AuthorizationDetailsTypeManager getAuthorizationDetailsTypeManager() { + + return this.authorizationDetailsTypeManager; + } + + /** + * set an {@link AuthorizationDetailsTypeManager} instance. + * + * @param authorizationDetailsTypeManager An {@link AuthorizationDetailsTypeManager} instance. + */ + public void setAuthorizationDetailsTypeManager(AuthorizationDetailsTypeManager authorizationDetailsTypeManager) { + + this.authorizationDetailsTypeManager = authorizationDetailsTypeManager; + } } 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..520e9a4f31e 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.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/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsService.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsService.java new file mode 100644 index 00000000000..31a04e5b98a --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsService.java @@ -0,0 +1,686 @@ +/* + * Copyright (c) 2025, 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.collections.CollectionUtils; +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.api.resource.mgt.util.AuthorizationDetailsTypesUtil; +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.dao.OAuthTokenPersistenceFactory; +import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessor; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessorFactory; +import org.wso2.carbon.identity.oauth2.rar.dao.AuthorizationDetailsDAO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsCodeDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsTokenDTO; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; + +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils.getAuthorizationDetailsTypesMap; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils.isEmpty; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils.isRichAuthorizationRequest; + +/** + * AuthorizationDetailsService 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 AuthorizationDetailsService { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsService.class); + private final AuthorizationDetailsDAO authorizationDetailsDAO; + private final AuthorizationDetailsProcessorFactory authorizationDetailsProcessorFactory; + private final boolean isRichAuthorizationRequestsDisabled; + + /** + * Default constructor that initializes the service with the default {@link AuthorizationDetailsDAO} and + * {@link AuthorizationDetailsProcessorFactory}. + *

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

+ */ + public AuthorizationDetailsService() { + + this( + AuthorizationDetailsProcessorFactory.getInstance(), + OAuthTokenPersistenceFactory.getInstance().getAuthorizationDetailsDAO(), + AuthorizationDetailsTypesUtil.isRichAuthorizationRequestsEnabled() + ); + } + + /** + * Constructor that initializes the service with a given {@link AuthorizationDetailsDAO}. + * + * @param authorizationDetailsProcessorFactory Factory instance for providing authorization details. + * @param authorizationDetailsDAO The {@link AuthorizationDetailsDAO} instance to be used for + * handling authorization details persistence. Must not be {@code null}. + */ + public AuthorizationDetailsService(final AuthorizationDetailsProcessorFactory authorizationDetailsProcessorFactory, + final AuthorizationDetailsDAO authorizationDetailsDAO, + final boolean isRichAuthorizationRequestsEnabled) { + + this.authorizationDetailsDAO = + Objects.requireNonNull(authorizationDetailsDAO, "AuthorizationDetailsDAO must not be null"); + this.authorizationDetailsProcessorFactory = Objects.requireNonNull(authorizationDetailsProcessorFactory, + "AuthorizationDetailsProviderFactory must not be null"); + this.isRichAuthorizationRequestsDisabled = !isRichAuthorizationRequestsEnabled; + } + + /** + * Persists or updates user-consented authorization details. If previously stored authorization details are found, + * they are updated with the new information. + * + * @param authenticatedUser The authenticated user. + * @param clientId The client ID. + * @param oAuth2Parameters Requested OAuth2 parameters. + * @param userConsentedAuthorizationDetails User consented authorization details. + * @throws OAuthSystemException if an error occurs while storing user consented authorization details. + */ + public void storeOrUpdateUserConsentedAuthorizationDetails( + final AuthenticatedUser authenticatedUser, final String clientId, final OAuth2Parameters oAuth2Parameters, + final AuthorizationDetails userConsentedAuthorizationDetails) + throws OAuthSystemException { + + if (this.isRichAuthorizationRequestsDisabled || !isRichAuthorizationRequest(oAuth2Parameters)) { + log.debug("Request is not a rich authorization request. Skipping storage of authorization details."); + return; + } + + try { + final int tenantId = OAuth2Util.getTenantId(oAuth2Parameters.getTenantDomain()); + final Optional optConsentId = this.getConsentId(authenticatedUser, clientId, tenantId); + + if (!optConsentId.isPresent()) { + if (log.isDebugEnabled()) { + log.debug(String.format("Unable to find a consent for userId %s and appId %s", + authenticatedUser.getLoggableMaskedUserId(), clientId)); + } + return; + } + + final String consentId = optConsentId.get(); + final Set authorizationDetailsToBeUpdated = new HashSet<>(); + final Set authorizationDetailsToBeAdded = new HashSet<>(); + + final AuthorizationDetails trimmedAuthorizationDetails = AuthorizationDetailsUtils + .getTrimmedAuthorizationDetails(userConsentedAuthorizationDetails); + final Map> consentedAuthorizationDetailsByType = + getAuthorizationDetailsTypesMap(this.getUserConsentedAuthorizationDetails(consentId, tenantId)); + + // Determine new authorization details to add or update based on the existing user consent + trimmedAuthorizationDetails.stream().forEach(authorizationDetail -> { + final AuthorizationDetailsConsentDTO authorizationDetailsConsentDTO = + new AuthorizationDetailsConsentDTO(consentId, authorizationDetail, true, tenantId); + + if (isUserConsentedAuthorizationDetailsType(authorizationDetail, consentedAuthorizationDetailsByType)) { + authorizationDetailsToBeUpdated.add(authorizationDetailsConsentDTO); + } else { + authorizationDetailsToBeAdded.add(authorizationDetailsConsentDTO); + } + }); + + if (CollectionUtils.isNotEmpty(authorizationDetailsToBeUpdated)) { + this.authorizationDetailsDAO.updateUserConsentedAuthorizationDetails(authorizationDetailsToBeUpdated); + if (log.isDebugEnabled()) { + log.debug("User consented authorization details updated. consentId: " + consentId); + } + } + + if (CollectionUtils.isNotEmpty(authorizationDetailsToBeAdded)) { + this.authorizationDetailsDAO.addUserConsentedAuthorizationDetails(authorizationDetailsToBeAdded); + if (log.isDebugEnabled()) { + log.debug("User consented authorization details stored. consentId: " + consentId); + } + } + } 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); + } + } + + /** + * Deletes user-consented authorization details. + * + * @param authenticatedUser The authenticated user. + * @param clientId The client ID. + * @param oAuth2Parameters Requested OAuth2 parameters. + * @throws OAuthSystemException if an error occurs while deleting authorization details. + */ + public void deleteUserConsentedAuthorizationDetails(final AuthenticatedUser authenticatedUser, + final String clientId, final OAuth2Parameters oAuth2Parameters) + throws OAuthSystemException { + + if (this.isRichAuthorizationRequestsDisabled || !isRichAuthorizationRequest(oAuth2Parameters)) { + log.debug("Request is not a rich authorization request. Skipping deletion of authorization details."); + return; + } + + try { + final int tenantId = OAuth2Util.getTenantId(oAuth2Parameters.getTenantDomain()); + final Optional consentId = this.getConsentId(authenticatedUser, clientId, tenantId); + + if (consentId.isPresent()) { + + this.authorizationDetailsDAO.deleteUserConsentedAuthorizationDetails(consentId.get(), tenantId); + + if (log.isDebugEnabled()) { + log.debug("User consented authorization details deleted successfully. consentId: " + + consentId.get()); + } + } + } 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); + } + } + + /** + * Replaces the user consented authorization details. + * + * @param authenticatedUser The authenticated user. + * @param clientId The client ID. + * @param oAuth2Parameters Requested OAuth2 parameters. + * @param userConsentedAuthorizationDetails User consented authorization details. + * @throws OAuthSystemException if an error occurs while storing or replacing authorization details. + */ + public void replaceUserConsentedAuthorizationDetails( + final AuthenticatedUser authenticatedUser, final String clientId, final OAuth2Parameters oAuth2Parameters, + final AuthorizationDetails userConsentedAuthorizationDetails) throws OAuthSystemException { + + this.deleteUserConsentedAuthorizationDetails(authenticatedUser, clientId, oAuth2Parameters); + this.storeOrUpdateUserConsentedAuthorizationDetails(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 {@code true} if user has given consent to all the requested authorization details, + * {@code false} otherwise. + */ + public boolean isUserAlreadyConsentedForAuthorizationDetails(final AuthenticatedUser authenticatedUser, + final OAuth2Parameters oAuth2Parameters) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled || !isRichAuthorizationRequest(oAuth2Parameters)) { + return true; + } + + return isEmpty(this.getConsentRequiredAuthorizationDetails(authenticatedUser, oAuth2Parameters)); + } + + public AuthorizationDetails getConsentRequiredAuthorizationDetails(final AuthenticatedUser authenticatedUser, + final OAuth2Parameters oAuth2Parameters) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled || !isRichAuthorizationRequest(oAuth2Parameters)) { + log.debug("Request is not a rich authorization request. Skipping the authorization details retrieval."); + return new AuthorizationDetails(); + } + + Map> consentedAuthorizationDetailsByType = getAuthorizationDetailsTypesMap( + this.getUserConsentedAuthorizationDetails(authenticatedUser, oAuth2Parameters)); + + final Set consentRequiredAuthorizationDetails = new HashSet<>(); + oAuth2Parameters.getAuthorizationDetails().stream() + .filter(requestedDetail -> + !this.isUserConsentedAuthorizationDetail(requestedDetail, consentedAuthorizationDetailsByType)) + .forEach(consentRequiredAuthorizationDetails::add); + + return new AuthorizationDetails(consentRequiredAuthorizationDetails); + } + + /** + * Checks if the user has already consented to the requested authorization detail. + * + *

This method validates if the requested authorization detail is part of the consented authorization details. + * It uses the appropriate provider to compare the requested detail with the existing consented details.

+ * + * @param requestedAuthorizationDetail the authorization detail to be checked + * @param consentedAuthorizationDetailsByType a map of consented authorization details grouped by type + * @return {@code true} if the user has consented to the requested authorization detail, {@code false} otherwise + */ + private boolean isUserConsentedAuthorizationDetail( + final AuthorizationDetail requestedAuthorizationDetail, + final Map> consentedAuthorizationDetailsByType) { + + if (!this.isUserConsentedAuthorizationDetailsType(requestedAuthorizationDetail, + consentedAuthorizationDetailsByType)) { + return false; + } + + final String requestedType = requestedAuthorizationDetail.getType(); + final Optional optProcessor = + this.authorizationDetailsProcessorFactory.getAuthorizationDetailsProcessorByType(requestedType); + + if (optProcessor.isPresent()) { + + if (log.isDebugEnabled()) { + log.debug("Validating equality of requested and existing authorization details " + + "using processor class: " + optProcessor.get().getClass().getSimpleName()); + } + + final AuthorizationDetails existingAuthorizationDetails = + new AuthorizationDetails(consentedAuthorizationDetailsByType.get(requestedType)); + boolean isEqualOrSubset = optProcessor.get() + .isEqualOrSubset(requestedAuthorizationDetail, existingAuthorizationDetails); + + if (log.isDebugEnabled()) { + log.debug(String.format("Verifying if the user has already consented to the requested " + + "authorization details type: '%s'. Result: %b", requestedType, isEqualOrSubset)); + } + return isEqualOrSubset; + } + if (log.isDebugEnabled()) { + log.debug(String.format("No AuthorizationDetailsProcessor implementation found for type: %s. " + + "Proceeding with user consent.", requestedType)); + } + return false; + } + + private boolean isUserConsentedAuthorizationDetailsType( + final AuthorizationDetail requestedAuthorizationDetail, + final Map> consentedAuthorizationDetailsByType) { + + if (consentedAuthorizationDetailsByType.containsKey(requestedAuthorizationDetail.getType())) { + return true; + } + if (log.isDebugEnabled()) { + log.debug(String.format("User hasn't consented for the requested authorization details type '%s'", + requestedAuthorizationDetail.getType())); + } + return false; + } + + /** + * Retrieves the user consented authorization details for a given user and OAuth2 parameters. + * + * @param authenticatedUser The authenticated user. + * @param oAuth2Parameters The OAuth2 parameters. + * @return The user consented authorization details. + * @throws IdentityOAuth2Exception If an error occurs while retrieving the details. + */ + public AuthorizationDetails getUserConsentedAuthorizationDetails(final AuthenticatedUser authenticatedUser, + final OAuth2Parameters oAuth2Parameters) + throws IdentityOAuth2Exception { + + final int tenantId = OAuth2Util.getTenantId(oAuth2Parameters.getTenantDomain()); + return this.getUserConsentedAuthorizationDetails(authenticatedUser, oAuth2Parameters.getClientId(), tenantId); + } + + /** + * Retrieves the user consented authorization details for a given user, client, and tenant. + * + * @param authenticatedUser The authenticated user. + * @param clientId The client ID. + * @param tenantId The tenant ID. + * @return The user consented authorization details, or {@code null} if no consent is found. + * @throws IdentityOAuth2Exception If an error occurs while retrieving the details. + */ + public AuthorizationDetails getUserConsentedAuthorizationDetails( + final AuthenticatedUser authenticatedUser, final String clientId, final int tenantId) + throws IdentityOAuth2Exception { + + final Optional consentId = this.getConsentId(authenticatedUser, clientId, tenantId); + if (consentId.isPresent()) { + return this.getUserConsentedAuthorizationDetails(consentId.get(), tenantId); + } + return null; + } + + public AuthorizationDetails getUserConsentedAuthorizationDetails(final String consentId, final int tenantId) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled) { + log.debug("Rich authorization requests is disabled. Skip retrieving consented authorization details."); + return new AuthorizationDetails(); + } + + try { + final Set consentedAuthorizationDetails = new HashSet<>(); + this.authorizationDetailsDAO.getUserConsentedAuthorizationDetails(consentId, tenantId) + .stream() + .filter(AuthorizationDetailsConsentDTO::isConsentActive) + .map(AuthorizationDetailsConsentDTO::getAuthorizationDetail) + .forEach(consentedAuthorizationDetails::add); + return new AuthorizationDetails(consentedAuthorizationDetails); + } catch (SQLException e) { + log.error("Error occurred while retrieving user consented authorization details. Caused by, ", e); + throw new IdentityOAuth2Exception("Unable to retrieve user consented authorization details", e); + } + } + + /** + * Retrieves the consent ID for the given user, client, and tenant. + * + * @param authenticatedUser The authenticated user. + * @param clientId The client ID. + * @param tenantId The tenant ID. + * @return An {@link Optional} containing the consent ID if present. + * @throws IdentityOAuth2Exception if an error occurs related to OAuth2 identity. + */ + private Optional getConsentId(final AuthenticatedUser authenticatedUser, final String clientId, + final int tenantId) + throws IdentityOAuth2Exception { + + final String userId = AuthorizationDetailsUtils.getIdFromAuthenticatedUser(authenticatedUser); + final String appId = AuthorizationDetailsUtils.getApplicationResourceIdFromClientId(clientId); + + return this.getConsentIdByUserIdAndAppId(userId, appId, tenantId); + } + + /** + * Retrieves the consent ID by user ID and application ID. + * + * @param userId The user ID. + * @param appId The application ID. + * @param tenantId The tenant ID. + * @return An {@link Optional} containing the consent ID if present. + * @throws IdentityOAuth2Exception if an error occurs while retrieving the consent ID. + */ + public Optional getConsentIdByUserIdAndAppId(final String userId, final String appId, final int tenantId) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled) { + log.debug("Rich authorization requests is disabled. Skip retrieving consents."); + return Optional.empty(); + } + try { + return Optional + .ofNullable(this.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 IdentityOAuth2Exception("Error occurred while retrieving user consent", e); + } + } + + /** + * Retrieves the authorization details associated with a given access token. + * + * @param accessTokenId The access token ID. + * @param tenantId The tenant ID. + * @return The access token authorization details. + * @throws IdentityOAuth2Exception If an error occurs while retrieving the details. + */ + public AuthorizationDetails getAccessTokenAuthorizationDetails(final String accessTokenId, final int tenantId) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled) { + log.debug("Rich authorization requests is disabled. Skip retrieving token authorization details."); + return new AuthorizationDetails(); + } + try { + final Set authorizationDetailsTokenDTOs = + this.authorizationDetailsDAO.getAccessTokenAuthorizationDetails(accessTokenId, tenantId); + + final Set accessTokenAuthorizationDetails = new HashSet<>(); + authorizationDetailsTokenDTOs + .stream() + .map(AuthorizationDetailsTokenDTO::getAuthorizationDetail) + .forEach(accessTokenAuthorizationDetails::add); + + return new AuthorizationDetails(accessTokenAuthorizationDetails); + } catch (SQLException e) { + log.error("Error occurred while retrieving access token authorization details. Caused by, ", e); + throw new IdentityOAuth2Exception("Unable to retrieve access token authorization details", e); + } + } + + /** + * Stores the authorization details for a given access token and OAuth authorization request context. + * + * @param accessTokenDO The access token data object. + * @param oAuthAuthzReqMessageContext The OAuth authorization request message context. + * @throws IdentityOAuth2Exception If an error occurs while storing the details. + */ + public void storeAccessTokenAuthorizationDetails(final AccessTokenDO accessTokenDO, + final OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext) + throws IdentityOAuth2Exception { + + if (!isRichAuthorizationRequest(oAuthAuthzReqMessageContext.getApprovedAuthorizationDetails())) { + log.debug("Request is not a rich authorization request. Skipping storage of token authorization details."); + return; + } + + this.storeAccessTokenAuthorizationDetails(accessTokenDO, + oAuthAuthzReqMessageContext.getApprovedAuthorizationDetails()); + } + + /** + * Stores the authorization details for a given access token and authorization details. + * + * @param accessTokenDO The access token data object. + * @param authorizationDetails The authorization details. + * @throws IdentityOAuth2Exception If an error occurs while storing the details. + */ + public void storeAccessTokenAuthorizationDetails(final AccessTokenDO accessTokenDO, + final AuthorizationDetails authorizationDetails) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled || AuthorizationDetailsUtils.isEmpty(authorizationDetails)) { + log.debug("Request is not a rich authorization request. Skipping storage of token authorization details."); + return; + } + try { + final AuthorizationDetails trimmedAuthorizationDetails = AuthorizationDetailsUtils + .getTrimmedAuthorizationDetails(authorizationDetails); + + final Set authorizationDetailsTokenDTOs = AuthorizationDetailsUtils + .getAccessTokenAuthorizationDetailsDTOs(accessTokenDO, trimmedAuthorizationDetails); + + // Storing the authorization details. + this.authorizationDetailsDAO.addAccessTokenAuthorizationDetails(authorizationDetailsTokenDTOs); + + if (log.isDebugEnabled()) { + log.debug("Successfully stored access token authorization details for tokenId: " + + accessTokenDO.getTokenId()); + } + } catch (SQLException e) { + log.error("Error occurred while storing access token authorization details. Caused by, ", e); + throw new IdentityOAuth2Exception("Error occurred while storing access token authorization details", e); + } + } + + /** + * Stores or replaces the authorization details for a new access token and + * optionally deletes the old token's details. + * + * @param newAccessTokenDO The new access token data object. + * @param oldAccessTokenDO The old access token data object. + * @param oAuthTokenReqMessageContext The OAuth token request message context. + * @throws IdentityOAuth2Exception If an error occurs while storing or replacing the details. + */ + public void storeOrReplaceAccessTokenAuthorizationDetails( + final AccessTokenDO newAccessTokenDO, final AccessTokenDO oldAccessTokenDO, + final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) throws IdentityOAuth2Exception { + + if (!isRichAuthorizationRequest(oAuthTokenReqMessageContext)) { + log.debug("Request is not a rich authorization request. Skipping storage of token authorization details."); + return; + } + + if (Objects.nonNull(oldAccessTokenDO)) { + this.deleteAccessTokenAuthorizationDetails(oldAccessTokenDO.getTokenId(), oldAccessTokenDO.getTenantID()); + } + + this.storeAccessTokenAuthorizationDetails(newAccessTokenDO, + oAuthTokenReqMessageContext.getAuthorizationDetails()); + } + + /** + * Deletes the authorization details associated with a given access token. + * + * @param accessTokenId The access token ID. + * @param tenantId The tenant ID. + * @throws IdentityOAuth2Exception If an error occurs while deleting the details. + */ + public void deleteAccessTokenAuthorizationDetails(final String accessTokenId, final int tenantId) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled) { + log.debug("Rich authorization requests is disabled. Skip persisting token authorization details."); + return; + } + try { + int result = this.authorizationDetailsDAO.deleteAccessTokenAuthorizationDetails(accessTokenId, tenantId); + if (result > 0 && log.isDebugEnabled()) { + log.debug("Access token authorization details deleted successfully. accessTokenId: " + accessTokenId); + } + } catch (SQLException e) { + log.error("Error occurred while deleting access token authorization details. Caused by, ", e); + throw new IdentityOAuth2Exception("Error occurred while deleting access token authorization details", e); + } + } + + /** + * Replaces the authorization details for an old access token with the details of a new access token. + * + * @param oldAccessTokenId The old access token ID. + * @param newAccessTokenDO The new access token data object. + * @param oAuthTokenReqMessageContext The OAuth token request message context. + * @throws IdentityOAuth2Exception If an error occurs while replacing the details. + */ + public void replaceAccessTokenAuthorizationDetails(final String oldAccessTokenId, + final AccessTokenDO newAccessTokenDO, + final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) + throws IdentityOAuth2Exception { + + if (!isRichAuthorizationRequest(oAuthTokenReqMessageContext)) { + log.debug("Request is not a rich authorization request. Skipping replacement of authorization details."); + return; + } + this.deleteAccessTokenAuthorizationDetails(oldAccessTokenId, newAccessTokenDO.getTenantID()); + this.storeAccessTokenAuthorizationDetails(newAccessTokenDO, oAuthTokenReqMessageContext); + } + + /** + * Stores the authorization details for a given access token and OAuth token request context. + * + * @param accessTokenDO The access token data object. + * @param oAuthTokenReqMessageContext The OAuth token request message context. + * @throws IdentityOAuth2Exception If an error occurs while storing the details. + */ + public void storeAccessTokenAuthorizationDetails(final AccessTokenDO accessTokenDO, + final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) + throws IdentityOAuth2Exception { + + if (!isRichAuthorizationRequest(oAuthTokenReqMessageContext)) { + log.debug("Request is not a rich authorization request. Skipping storage of token authorization details."); + return; + } + + this.storeAccessTokenAuthorizationDetails(accessTokenDO, oAuthTokenReqMessageContext.getAuthorizationDetails()); + } + + /** + * Stores the authorization details for a given authorization code and OAuth authorization request context. + * + * @param authzCodeDO The authorization code data object. + * @param oAuthAuthzReqMessageContext The OAuth authorization request message context. + * @throws IdentityOAuth2Exception If an error occurs while storing the details. + */ + public void storeAuthorizationCodeAuthorizationDetails( + final AuthzCodeDO authzCodeDO, final OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled || !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()); + + final Set authorizationDetailsCodeDTOs = + AuthorizationDetailsUtils.getCodeAuthorizationDetailsDTOs(authzCodeDO, + oAuthAuthzReqMessageContext.getApprovedAuthorizationDetails(), tenantId); + + // Storing the authorization details. + this.authorizationDetailsDAO.addOAuth2CodeAuthorizationDetails(authorizationDetailsCodeDTOs); + + if (log.isDebugEnabled()) { + log.debug("Successfully stored authorization code authorization details for code ID: " + + authzCodeDO.getAuthzCodeId()); + } + } catch (SQLException e) { + log.error("Error occurred while storing authorization code authorization details. Caused by, ", e); + throw new IdentityOAuth2Exception("Error occurred while storing authz code authorization details", e); + } + } + + /** + * Retrieves the authorization details associated with a given authorization code Id. + * + * @param code The authorization code. + * @param tenantId The tenant ID. + * @return The authorization code authorization details. + * @throws IdentityOAuth2Exception If an error occurs while retrieving the details. + */ + public AuthorizationDetails getAuthorizationCodeAuthorizationDetails(final String code, final int tenantId) + throws IdentityOAuth2Exception { + + if (this.isRichAuthorizationRequestsDisabled) { + log.debug("Rich authorization requests is disabled. Skip retrieving code authorization details."); + return new AuthorizationDetails(); + } + try { + final Set authorizationDetailsCodeDTOs = + this.authorizationDetailsDAO.getOAuth2CodeAuthorizationDetails(code, tenantId); + + final Set codeAuthorizationDetails = new HashSet<>(); + authorizationDetailsCodeDTOs + .stream() + .map(AuthorizationDetailsCodeDTO::getAuthorizationDetail) + .forEach(codeAuthorizationDetails::add); + + return new AuthorizationDetails(codeAuthorizationDetails); + } catch (SQLException e) { + log.error("Error occurred while retrieving authz code authorization details. Caused by, ", e); + throw new IdentityOAuth2Exception("Unable to retrieve authz code authorization details", e); + } + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProcessor.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProcessor.java new file mode 100644 index 00000000000..c31888d2e3b --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProcessor.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2025, 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.IdentityOAuth2ServerException; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetailsContext; +import org.wso2.carbon.identity.oauth2.rar.model.ValidationResult; + +/** + * The {@code AuthorizationDetailsProcessor} interface defines a contract for implementing + * different types of authorization detail providers in an OSGI setup. + *

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

+ */ +public interface AuthorizationDetailsProcessor { + + /** + * 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 due to a request error and the + * authorization flow needs to be interrupted. + * @throws IdentityOAuth2ServerException if the validation fails due to a server error and the + * authorization flow needs to be interrupted. + * @see AuthorizationDetailsContext + * @see ValidationResult + */ + ValidationResult validate(AuthorizationDetailsContext authorizationDetailsContext) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException; + + /** + * 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(); + + /** + * Checks if the requested authorization detail is equal to or a subset of the existing authorization details. + * + *

This method verifies if the provided {@code requestedAuthorizationDetail} is either exactly the same as or + * a subset of the {@code existingAuthorizationDetails} that have been previously accepted by the resource owner. + * + * @param requestedAuthorizationDetail The {@link AuthorizationDetail} being requested by the client. + * @param existingAuthorizationDetails The set of {@link AuthorizationDetail} that have been previously accepted + * by the resource owner. + * @return {@code true} if the requested authorization detail is equal to or a subset of the existing + * authorization details, {@code false} otherwise. + */ + 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/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProcessorFactory.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProcessorFactory.java new file mode 100644 index 00000000000..163b853012d --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/core/AuthorizationDetailsProcessorFactory.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025, 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.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.identity.api.resource.mgt.APIResourceMgtException; +import org.wso2.carbon.identity.application.common.model.AuthorizationDetailsType; +import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A factory class to manage and provide instances of {@link AuthorizationDetailsProcessor} 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 AuthorizationDetailsProcessor} implementations. + *

Example usage: + *

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

+ * + * @see AuthorizationDetailsProcessor AuthorizationDetailsService + * @see + * Request Parameter "authorization_details" + */ +public class AuthorizationDetailsProcessorFactory { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsProcessorFactory.class); + private static volatile AuthorizationDetailsProcessorFactory instance; + private final Map authorizationDetailsProcessors; + + /** + * 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 AuthorizationDetailsProcessorFactory() { + + this.authorizationDetailsProcessors = new HashMap<>(); + } + + /** + * Provides the singleton instance of {@code AuthorizationDetailsProviderFactory}. + * + * @return Singleton instance of {@code AuthorizationDetailsProviderFactory}. + */ + public static AuthorizationDetailsProcessorFactory getInstance() { + + if (instance == null) { + synchronized (AuthorizationDetailsProcessorFactory.class) { + if (instance == null) { + instance = new AuthorizationDetailsProcessorFactory(); + } + } + } + return instance; + } + + /** + * Returns the {@link AuthorizationDetailsProcessor} provider for the given type. + * + * @param type A supported authorization details type. + * @return {@link Optional} containing the {@link AuthorizationDetailsProcessor} if present, otherwise empty. + * @see AuthorizationDetailsProcessor#getType() getAuthorizationDetailsType + */ + public Optional getAuthorizationDetailsProcessorByType(final String type) { + + return Optional.ofNullable(this.authorizationDetailsProcessors.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 AuthorizationDetailsProcessor AuthorizationDetailsService + */ + public boolean isSupportedAuthorizationDetailsType(final String type) { + + return this.getSupportedAuthorizationDetailTypes().contains(type); + } + + /** + * Returns an {@link Collections#unmodifiableSet} containing all supported authorization details types. + *

A type is considered "supported" if it has been registered by invoking the + * POST: /api/server/v1/api-resources endpoint.

+ * + * @return An unmodifiable set of supported authorization details types. + */ + public Set getSupportedAuthorizationDetailTypes() { + + final String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + try { + + final List authorizationDetailsTypes = OAuth2ServiceComponentHolder.getInstance() + .getAuthorizationDetailsTypeManager().getAuthorizationDetailsTypes(StringUtils.EMPTY, tenantDomain); + + if (authorizationDetailsTypes != null) { + return authorizationDetailsTypes + .stream() + .map(AuthorizationDetailsType::getType) + .collect(Collectors.toUnmodifiableSet()); + } + } catch (APIResourceMgtException e) { + if (log.isDebugEnabled()) { + log.debug(String.format("Error occurred while retrieving supported authorization details types " + + "for tenant: %s. Caused by, ", tenantDomain), e); + } + } + return Collections.emptySet(); + } + + /** + * Caches the provided {@link AuthorizationDetailsProcessor} instance by associating it with its corresponding + * authorization details type. This allows efficient retrieval and reuse of processors based on their type. + *

The type of the authorization details processor is obtained using + * {@link AuthorizationDetailsProcessor#getType()}

+ * + * @param authorizationDetailsProcessor Processor instance to be cached, keyed by its authorization details type. + */ + public void setAuthorizationDetailsProcessors(final AuthorizationDetailsProcessor authorizationDetailsProcessor) { + + if (authorizationDetailsProcessor != null && StringUtils.isNotBlank(authorizationDetailsProcessor.getType())) { + final String type = authorizationDetailsProcessor.getType(); + if (log.isDebugEnabled()) { + log.debug(String.format("Registering AuthorizationDetailsProcessor %s against type %s", + authorizationDetailsProcessor.getClass().getSimpleName(), type)); + } + this.authorizationDetailsProcessors.put(type, authorizationDetailsProcessor); + } + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetailsContext.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetailsContext.java new file mode 100644 index 00000000000..b783eaabe9f --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/model/AuthorizationDetailsContext.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2025, 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.application.common.model.AuthorizationDetailsType; +import org.wso2.carbon.identity.oauth.dao.OAuthAppDO; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; + +import java.util.Objects; + +import javax.servlet.http.HttpServletRequestWrapper; + +import static org.wso2.carbon.identity.oauth2.authz.AuthorizationHandlerManager.OAUTH_APP_PROPERTY; + +/** + * 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 AuthenticatedUser authenticatedUser; + private final AuthorizationDetail authorizationDetail; + private final AuthorizationDetailsType authorizationDetailsType; + private final HttpServletRequestWrapper httpServletRequestWrapper; + private final OAuthAppDO oAuthAppDO; + private final String[] scopes; + + /** + * Constructs a new {@code AuthorizationDetailsContext}. + * + * @param authorizationDetail the specific {@link AuthorizationDetail} to be validated. + * @param oAuthAuthzReqMessageContext the {@link OAuthAuthzReqMessageContext} instance which represent + * the authorization request context. + * @throws NullPointerException if any of the arguments are {@code null}. + */ + public AuthorizationDetailsContext(final AuthorizationDetail authorizationDetail, + final AuthorizationDetailsType authorizationDetailsType, + final OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext) { + + this(oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getUser(), + authorizationDetail, + authorizationDetailsType, + oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getHttpServletRequestWrapper(), + (OAuthAppDO) oAuthAuthzReqMessageContext.getProperty(OAUTH_APP_PROPERTY), + oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getScopes()); + } + + /** + * Constructs a new {@code AuthorizationDetailsContext}. + *

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

+ * + * @param authenticatedUser the {@link AuthenticatedUser}. + * @param authorizationDetail the specific {@link AuthorizationDetail} to be validated. + * @param httpServletRequestWrapper the {@link HttpServletRequestWrapper} instance containing request details. + * @param oAuthAppDO the {@link OAuthAppDO} containing application details. + * @param scopes the array of scopes requested. + * @throws NullPointerException if any of the arguments are {@code null}. + */ + public AuthorizationDetailsContext(final AuthenticatedUser authenticatedUser, + final AuthorizationDetail authorizationDetail, + final AuthorizationDetailsType authorizationDetailsType, + final HttpServletRequestWrapper httpServletRequestWrapper, + final OAuthAppDO oAuthAppDO, + final String[] scopes) { + + this.authenticatedUser = Objects.requireNonNull(authenticatedUser, "authenticatedUser cannot be null"); + this.authorizationDetail = Objects.requireNonNull(authorizationDetail, "authorizationDetail cannot be null"); + this.authorizationDetailsType = + Objects.requireNonNull(authorizationDetailsType, "authorizationDetailsType cannot be null"); + this.httpServletRequestWrapper = + Objects.requireNonNull(httpServletRequestWrapper, "httpServletRequestWrapper cannot be null"); + this.oAuthAppDO = Objects.requireNonNull(oAuthAppDO, "oAuthAppDO cannot be null"); + this.scopes = Objects.requireNonNull(scopes, "scopes cannot be null"); + } + + /** + * Constructs a new {@code AuthorizationDetailsContext}. + * + * @param authorizationDetail the specific {@link AuthorizationDetail} to be validated. + * @param oAuthTokenReqMessageContext the {@link OAuthTokenReqMessageContext} instance which represent + * the token request context. + * @throws NullPointerException if any of the arguments are {@code null}. + */ + public AuthorizationDetailsContext(final AuthorizationDetail authorizationDetail, + final AuthorizationDetailsType authorizationDetailsType, + final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) { + + this(oAuthTokenReqMessageContext.getAuthorizedUser(), + authorizationDetail, + authorizationDetailsType, + oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO().getHttpServletRequestWrapper(), + (OAuthAppDO) oAuthTokenReqMessageContext.getProperty(OAUTH_APP_PROPERTY), + oAuthTokenReqMessageContext.getScope()); + } + + /** + * Returns the {@code AuthorizationDetail} instance. + * + * @return the {@link AuthorizationDetail} instance. + */ + public AuthorizationDetail getAuthorizationDetail() { + return this.authorizationDetail; + } + + /** + * Returns the {@code AuthorizationDetailsType} instance. + * + * @return the {@link AuthorizationDetailsType} instance. + */ + public AuthorizationDetailsType getAuthorizationDetailsType() { + return this.authorizationDetailsType; + } + + /** + * 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; + } + + /** + * Returns the HTTP servlet request user. + * + * @return the {@link HttpServletRequestWrapper} instance containing HTTP request details. + */ + public HttpServletRequestWrapper getHttpServletRequestWrapper() { + return this.httpServletRequestWrapper; + } + + /** + * Returns the valid scopes requested by the client. + * + * @return the {@link String} array of scopes. + */ + public String[] getScopes() { + return this.scopes; + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/AccessTokenResponseRARHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/AccessTokenResponseRARHandler.java new file mode 100644 index 00000000000..bf53b79138e --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/AccessTokenResponseRARHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025, 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.token; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; +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.Map; + +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils.isRichAuthorizationRequest; + +/** + * Class responsible for modifying the access token response to include user-consented authorization details. + * + *

This class enhances the access token response by appending user-consented authorization details. + * It is invoked by the {@link org.wso2.carbon.identity.oauth2.token.AccessTokenIssuer#issue} method during + * the OAuth 2.0 token issuance process.

+ */ +public class AccessTokenResponseRARHandler implements AccessTokenResponseHandler { + + private static final Log log = LogFactory.getLog(AccessTokenResponseRARHandler.class); + + /** + * Returns Rich Authorization Request attributes to be added to the access token response. + * + * @param oAuthTokenReqMessageContext {@link OAuthTokenReqMessageContext} token request message context. + * @return Map of additional attributes to be added to the token response. + * @throws IdentityOAuth2Exception Error while constructing additional token response attributes. + */ + @Override + public Map getAdditionalTokenResponseAttributes( + final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) throws IdentityOAuth2Exception { + + Map additionalAttributes = new HashMap<>(); + if (isRichAuthorizationRequest(oAuthTokenReqMessageContext.getAuthorizationDetails())) { + + if (log.isDebugEnabled()) { + log.debug("Adding authorization details into the token response: " + oAuthTokenReqMessageContext + .getAuthorizationDetails().toReadableText()); + } + additionalAttributes.put(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS, + oAuthTokenReqMessageContext.getAuthorizationDetails().toSet()); + } + return additionalAttributes; + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/IntrospectionRARDataProvider.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/IntrospectionRARDataProvider.java new file mode 100644 index 00000000000..afc515d3adc --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/IntrospectionRARDataProvider.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025, 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.token; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.oauth.common.OAuthConstants; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.IntrospectionDataProvider; +import org.wso2.carbon.identity.oauth2.dto.OAuth2IntrospectionResponseDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationRequestDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationResponseDTO; +import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; +import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; +import org.wso2.carbon.identity.oauth2.rar.validator.AuthorizationDetailsValidator; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; +import org.wso2.carbon.identity.oauth2.validators.OAuth2TokenValidationMessageContext; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.AUTHORIZATION_DETAILS; +import static org.wso2.carbon.identity.oauth2.validators.RefreshTokenValidator.TOKEN_TYPE_NAME; + +/** + * Class responsible for modifying the introspection response to include user-consented authorization details. + * + *

This class enhances the introspection response by appending user-consented authorization details. + * It is invoked by the /introspect endpoint of the oauth.endpoint webapp during the token introspection process.

+ */ +public class IntrospectionRARDataProvider implements IntrospectionDataProvider { + + private static final Log log = LogFactory.getLog(IntrospectionRARDataProvider.class); + private final AuthorizationDetailsValidator authorizationDetailsValidator; + + public IntrospectionRARDataProvider() { + + this(OAuth2ServiceComponentHolder.getInstance().getAuthorizationDetailsValidator()); + } + + public IntrospectionRARDataProvider(final AuthorizationDetailsValidator authorizationDetailsValidator) { + + this.authorizationDetailsValidator = authorizationDetailsValidator; + } + + /** + * Provides additional Rich Authorization Requests data for OAuth token introspection. + * + * @param tokenValidationRequestDTO Token validation request DTO. + * @param introspectionResponseDTO Token introspection response DTO. + * @return Map of additional data to be added to the introspection response. + * @throws IdentityOAuth2Exception If an error occurs while setting additional introspection data. + */ + @Override + public Map getIntrospectionData( + final OAuth2TokenValidationRequestDTO tokenValidationRequestDTO, + final OAuth2IntrospectionResponseDTO introspectionResponseDTO) throws IdentityOAuth2Exception { + + try { + final OAuth2TokenValidationMessageContext tokenValidationMessageContext = + generateOAuth2TokenValidationMessageContext(tokenValidationRequestDTO, introspectionResponseDTO); + final Map introspectionData = new HashMap<>(); + + if (Objects.nonNull(tokenValidationMessageContext)) { + + final AuthorizationDetails validatedAuthorizationDetails = this.authorizationDetailsValidator + .getValidatedAuthorizationDetails(tokenValidationMessageContext); + + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(validatedAuthorizationDetails)) { + introspectionData.put(AUTHORIZATION_DETAILS, validatedAuthorizationDetails.toSet()); + } + } + return introspectionData; + } catch (AuthorizationDetailsProcessingException e) { + log.error("Authorization details validation failed. Caused by, ", e); + throw new IdentityOAuth2Exception("Authorization details validation failed", e); + } + } + + /** + * Generates an OAuth2TokenValidationMessageContext based on the token validation request and + * introspection response. + * + * @param tokenValidationRequestDTO The OAuth2 token validation request DTO. + * @param introspectionResponseDTO The OAuth2 introspection response DTO. + * @return The generated OAuth2TokenValidationMessageContext. + * @throws IdentityOAuth2Exception If an error occurs during the generation of the context. + */ + private OAuth2TokenValidationMessageContext generateOAuth2TokenValidationMessageContext( + final OAuth2TokenValidationRequestDTO tokenValidationRequestDTO, + final OAuth2IntrospectionResponseDTO introspectionResponseDTO) throws IdentityOAuth2Exception { + + // Check if the introspection response contains a validation message context + if (introspectionResponseDTO.getProperties().containsKey(OAuth2Util.OAUTH2_VALIDATION_MESSAGE_CONTEXT)) { + log.debug("Introspection response contains a validation message context."); + + final Object oAuth2TokenValidationMessageContext = introspectionResponseDTO.getProperties() + .get(OAuth2Util.OAUTH2_VALIDATION_MESSAGE_CONTEXT); + + if (oAuth2TokenValidationMessageContext instanceof OAuth2TokenValidationMessageContext) { + return (OAuth2TokenValidationMessageContext) oAuth2TokenValidationMessageContext; + } + } else { + // Create a new validation message context + final OAuth2TokenValidationMessageContext oAuth2TokenValidationMessageContext = + new OAuth2TokenValidationMessageContext(tokenValidationRequestDTO, + generateOAuth2TokenValidationResponseDTO(introspectionResponseDTO)); + + oAuth2TokenValidationMessageContext.addProperty(OAuthConstants.ACCESS_TOKEN_DO, + this.getVerifiedToken(tokenValidationRequestDTO, introspectionResponseDTO)); + + return oAuth2TokenValidationMessageContext; + } + + log.debug("OAuth2TokenValidationMessageContext could not be generated. returning null"); + return null; + } + + private OAuth2TokenValidationResponseDTO generateOAuth2TokenValidationResponseDTO( + final OAuth2IntrospectionResponseDTO oAuth2IntrospectionResponseDTO) { + + final OAuth2TokenValidationResponseDTO tokenValidationResponseDTO = new OAuth2TokenValidationResponseDTO(); + tokenValidationResponseDTO.setValid(oAuth2IntrospectionResponseDTO.isActive()); + tokenValidationResponseDTO.setErrorMsg(oAuth2IntrospectionResponseDTO.getError()); + tokenValidationResponseDTO.setScope(OAuth2Util.buildScopeArray(oAuth2IntrospectionResponseDTO.getScope())); + tokenValidationResponseDTO.setExpiryTime(oAuth2IntrospectionResponseDTO.getExp()); + + return tokenValidationResponseDTO; + } + + private AccessTokenDO getVerifiedToken(final OAuth2TokenValidationRequestDTO tokenValidationRequestDTO, + final OAuth2IntrospectionResponseDTO introspectionResponseDTO) + throws IdentityOAuth2Exception { + + if (StringUtils.equals(TOKEN_TYPE_NAME, introspectionResponseDTO.getTokenType())) { + return OAuth2ServiceComponentHolder.getInstance().getTokenProvider() + .getVerifiedRefreshToken(tokenValidationRequestDTO.getAccessToken().getIdentifier()); + } else { + return OAuth2ServiceComponentHolder.getInstance().getTokenProvider() + .getVerifiedAccessToken(tokenValidationRequestDTO.getAccessToken().getIdentifier(), false); + } + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/JWTAccessTokenRARClaimProvider.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/JWTAccessTokenRARClaimProvider.java new file mode 100644 index 00000000000..202549b8f35 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/token/JWTAccessTokenRARClaimProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, 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.token; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.token.handlers.claims.JWTAccessTokenClaimProvider; + +import java.util.HashMap; +import java.util.Map; + +/** + * Provides additional claims related to Rich Authorization Requests to be included in JWT Access Tokens. + * This implementation supports both the OAuth2 authorization and token flows. + */ +public class JWTAccessTokenRARClaimProvider implements JWTAccessTokenClaimProvider { + + private static final Log log = LogFactory.getLog(JWTAccessTokenRARClaimProvider.class); + + /** + * Returns a map of additional claims related to Rich Authorization Requests to be included in + * JWT Access Tokens issued in the OAuth2 authorize flow. + * + * @param oAuthAuthzReqMessageContext The OAuth authorization request message context. + * @return A map of additional claims. + * @throws IdentityOAuth2Exception If an error occurs during claim retrieval. + */ + @Override + public Map getAdditionalClaims(final OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext) + throws IdentityOAuth2Exception { + + final Map additionalClaims = new HashMap<>(); + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(oAuthAuthzReqMessageContext)) { + if (log.isDebugEnabled()) { + log.debug("Adding authorization details into JWT token response in authorization flow: " + + oAuthAuthzReqMessageContext.getRequestedAuthorizationDetails().toReadableText()); + } + additionalClaims.put(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS, + oAuthAuthzReqMessageContext.getApprovedAuthorizationDetails().toSet()); + } + return additionalClaims; + } + + /** + * Returns a map of additional claims related to Rich Authorization Requests to be included in + * JWT Access Tokens issued in the OAuth2 token flow. + * + * @param oAuthTokenReqMessageContext The OAuth token request message context. + * @return A map of additional claims. + * @throws IdentityOAuth2Exception If an error occurs during claim retrieval. + */ + @Override + public Map getAdditionalClaims(final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) + throws IdentityOAuth2Exception { + + final Map additionalClaims = new HashMap<>(); + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(oAuthTokenReqMessageContext)) { + if (log.isDebugEnabled()) { + log.debug("Adding authorization details into JWT token response in token flow: " + + oAuthTokenReqMessageContext.getAuthorizationDetails().toReadableText()); + } + additionalClaims.put(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS, + oAuthTokenReqMessageContext.getAuthorizationDetails().toSet()); + } + return additionalClaims; + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsUtils.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsUtils.java new file mode 100644 index 00000000000..aedc2d19809 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/util/AuthorizationDetailsUtils.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2025, 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.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest; +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.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeReqDTO; +import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO; +import org.wso2.carbon.identity.oauth2.model.CarbonOAuthTokenRequest; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsCodeDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsTokenDTO; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; + +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.mapping; +import static java.util.stream.Collectors.toSet; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.AUTHORIZATION_DETAILS_ID_PREFIX; + +/** + * Utility class for handling and validating authorization details in OAuth2 requests. + */ +public class AuthorizationDetailsUtils { + + private static final Log log = LogFactory.getLog(AuthorizationDetailsUtils.class); + + /** + * 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.getRequestedAuthorizationDetails()); + } + + /** + * 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 !isEmpty(authorizationDetails); + } + + /** + * Determines if the provided {@link AuthorizationDetails} object is empty or not. + *

+ * 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 isEmpty(final AuthorizationDetails authorizationDetails) { + + return authorizationDetails == null || authorizationDetails.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)); + } + + /** + * Determines if the given {@link CarbonOAuthTokenRequest} object contains {@code authorization_details}. + * + * @param carbonOAuthTokenRequest The OAuth Token Request to check. + * @return {@code true} if the OAuth token request contains a non-blank authorization details parameter, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final CarbonOAuthTokenRequest carbonOAuthTokenRequest) { + + return StringUtils + .isNotBlank(carbonOAuthTokenRequest.getParam(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS)); + } + + /** + * Determines if the given {@link OAuthTokenReqMessageContext} object or the + * {@link OAuthTokenReqMessageContext#getOauth2AccessTokenReqDTO} contains {@link AuthorizationDetails}. + * + * @param oAuthTokenReqMessageContext The requested oAuthTokenReqMessageContext to check. + * @return {@code true} if the oAuthTokenReqMessageContext contains non-empty authorization details set, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) { + + return isRichAuthorizationRequest(oAuthTokenReqMessageContext.getAuthorizationDetails()) || + isRichAuthorizationRequest(oAuthTokenReqMessageContext + .getOauth2AccessTokenReqDTO().getAuthorizationDetails()); + } + + /** + * Retrieves the application resource ID from the client ID. + * + * @param clientId The client ID. + * @return The application resource ID. + * @throws IdentityOAuth2Exception if an error occurs while retrieving the application resource ID. + */ + public static 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); + } + + /** + * Retrieves the user ID from the authenticated user. + * + * @param authenticatedUser The authenticated user. + * @return The user ID. + * @throws IdentityOAuth2Exception if an error occurs while retrieving the user ID. + */ + public static String getIdFromAuthenticatedUser(final AuthenticatedUser authenticatedUser) + throws IdentityOAuth2Exception { + + try { + return authenticatedUser.getUserId(); + } catch (UserIdNotFoundException e) { + log.error("Error occurred while extracting userId from authenticated user. Caused by, ", e); + throw new IdentityOAuth2Exception( + "User id is not found for user: " + authenticatedUser.getLoggableMaskedUserId(), e); + } + } + + /** + * Generates a set of {@link AuthorizationDetailsConsentDTO} from the provided consent ID, + * authorization details, and tenant ID. + * + * @param consentId The consent ID. + * @param userConsentedAuthorizationDetails The user-consented authorization details. + * @param tenantId The tenant ID. + * @return A list of {@link AuthorizationDetailsConsentDTO}. + */ + public static Set getAuthorizationDetailsConsentDTOs( + final String consentId, final AuthorizationDetails userConsentedAuthorizationDetails, final int tenantId) { + + return userConsentedAuthorizationDetails.stream() + .map(detail -> new AuthorizationDetailsConsentDTO(consentId, detail, true, tenantId)) + .collect(toSet()); + } + + /** + * Generates a set of {@link AuthorizationDetailsTokenDTO} from the provided access token and + * authorization details. + * + * @param accessTokenDO The access token data object. + * @param authorizationDetails The user-consented authorization details. + * @return A list of {@link AuthorizationDetailsTokenDTO}. + */ + public static Set getAccessTokenAuthorizationDetailsDTOs( + final AccessTokenDO accessTokenDO, final AuthorizationDetails authorizationDetails) { + + return authorizationDetails + .stream() + .map(authorizationDetail -> new AuthorizationDetailsTokenDTO( + accessTokenDO.getTokenId(), authorizationDetail, accessTokenDO.getTenantID())) + .collect(toSet()); + } + + /** + * Generates a set of {@link AuthorizationDetailsCodeDTO} from the provided access token and + * authorization details. + * + * @param authzCodeDO The authorization code data object. + * @param authorizationDetails The user-consented authorization details. + * @return A list of {@link AuthorizationDetailsTokenDTO}. + */ + public static Set getCodeAuthorizationDetailsDTOs( + final AuthzCodeDO authzCodeDO, final AuthorizationDetails authorizationDetails, final int tenantId) { + + return authorizationDetails + .stream() + .map(authorizationDetail -> + new AuthorizationDetailsCodeDTO(authzCodeDO.getAuthzCodeId(), authorizationDetail, tenantId)) + .collect(toSet()); + } + + /** + * Extracts the user-consented authorization details from the request parameters and OAuth2 parameters. + * + * @param httpServletRequest The HTTP servlet request containing the authorization details. + * @param oAuth2Parameters The OAuth2 parameters that include the authorization details. + * @return The {@link AuthorizationDetails} containing the user-consented authorization details. + */ + public static AuthorizationDetails extractAuthorizationDetailsFromRequest( + final HttpServletRequest httpServletRequest, final OAuth2Parameters oAuth2Parameters) { + + if (!AuthorizationDetailsUtils.isRichAuthorizationRequest(oAuth2Parameters)) { + log.debug("Request is not a rich authorization request. Returning empty authorization details."); + return new AuthorizationDetails(); + } + + // Extract consented authorization detail IDs from the parameter map + final Set consentedAuthorizationDetailIDs = httpServletRequest.getParameterMap().keySet().stream() + .filter(parameterName -> parameterName.startsWith(AUTHORIZATION_DETAILS_ID_PREFIX)) + .map(parameterName -> parameterName.substring(AUTHORIZATION_DETAILS_ID_PREFIX.length())) + .collect(toSet()); + + // Filter and collect the consented authorization details + final AuthorizationDetails consentedAuthorizationDetails = new AuthorizationDetails(oAuth2Parameters + .getAuthorizationDetails() + .stream() + .filter(authorizationDetail -> consentedAuthorizationDetailIDs.contains(authorizationDetail.getId())) + .collect(toSet())); + + log.debug("User consented authorization details extracted successfully."); + + oAuth2Parameters.setAuthorizationDetails(consentedAuthorizationDetails); + return consentedAuthorizationDetails; + } + + /** + * 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()); + } + + /** + * Transforms the given {@link AuthorizationDetails} by creating a new set of {@link AuthorizationDetail} objects + * with only the displayable fields ({@code type}, {@code id}, {@code description}) copied over. + * + * @param authorizationDetails The original AuthorizationDetails to be transformed. + * @return A new {@link AuthorizationDetails} object containing the displayable authorization details. + */ + public static AuthorizationDetails getDisplayableAuthorizationDetails( + final AuthorizationDetails authorizationDetails) { + + final Set displayableAuthorizationDetails = authorizationDetails.stream() + .map(protectedAuthorizationDetail -> { + final AuthorizationDetail authorizationDetail = new AuthorizationDetail(); + authorizationDetail.setId(protectedAuthorizationDetail.getId()); + authorizationDetail.setType(protectedAuthorizationDetail.getType()); + authorizationDetail.setDescription(protectedAuthorizationDetail.getDescription()); + return authorizationDetail; + }).collect(toSet()); + + return new AuthorizationDetails(displayableAuthorizationDetails); + } + + /** + * Trims the given {@link AuthorizationDetails} by setting the temporary {@code id} and {@code consentDescription} + * fields to null for each {@link AuthorizationDetail}. + * + * @param authorizationDetails The original AuthorizationDetails to be trimmed. + * @return The same AuthorizationDetails object with trimmed fields. + */ + public static AuthorizationDetails getTrimmedAuthorizationDetails(final AuthorizationDetails authorizationDetails) { + + if (!isEmpty(authorizationDetails)) { + authorizationDetails.stream().forEach(authorizationDetail -> { + authorizationDetail.setId(null); + authorizationDetail.setDescription(null); + }); + } + return authorizationDetails; + } + + /** + * Generates unique IDs for each {@link AuthorizationDetail} within the given {@link AuthorizationDetails} object. + * + * @param authorizationDetails The AuthorizationDetails object containing a set of AuthorizationDetail objects. + * @return The AuthorizationDetails object with unique IDs assigned to each AuthorizationDetail. + */ + public static AuthorizationDetails assignUniqueIDsToAuthorizationDetails( + final AuthorizationDetails authorizationDetails) { + + authorizationDetails.stream().filter(Objects::nonNull) + .forEach(authorizationDetail -> authorizationDetail.setId(UUID.randomUUID().toString())); + return authorizationDetails; + } + + /** + * Encodes the given AuthorizationDetails object to a URL-encoded JSON string. + * + * @param authorizationDetails The AuthorizationDetails object to be encoded. + * @return A URL-encoded JSON string representing the authorization details. + */ + public static String getUrlEncodedAuthorizationDetails(final AuthorizationDetails authorizationDetails) { + + if (log.isDebugEnabled()) { + log.debug("Starts URL encoding authorization details: " + authorizationDetails.toJsonString()); + } + if (isRichAuthorizationRequest(authorizationDetails)) { + return URLEncoder.encode(authorizationDetails.toJsonString(), StandardCharsets.UTF_8); + } + return StringUtils.EMPTY; + } + + /** + * Decodes the given URL-encoded AuthorizationDetails JSON String. + * + * @param encodedAuthorizationDetails The encoded AuthorizationDetails String to be decoded. + * @return A URL-decoded JSON string representing the authorization details. + */ + public static String getUrlDecodedAuthorizationDetails(final String encodedAuthorizationDetails) { + + if (log.isDebugEnabled()) { + log.debug("Starts decoding URL encoded authorization details JSON: " + encodedAuthorizationDetails); + } + if (StringUtils.isNotEmpty(encodedAuthorizationDetails)) { + return URLDecoder.decode(encodedAuthorizationDetails, StandardCharsets.UTF_8); + } + return StringUtils.EMPTY; + } + + /** + * Determines if the given {@link OAuth2AuthorizeReqDTO} object contains {@link AuthorizationDetails}. + * + * @param oAuth2AuthorizeReqDTO The requested oAuth2AuthorizeReqDTO to check. + * @return {@code true} if the oAuth2AuthorizeReqDTO contains non-empty authorization details set, + * {@code false} otherwise. + */ + public static boolean isRichAuthorizationRequest(final OAuth2AuthorizeReqDTO oAuth2AuthorizeReqDTO) { + + return isRichAuthorizationRequest(oAuth2AuthorizeReqDTO.getAuthorizationDetails()); + } + + /** + * Converts a list of AuthorizationDetails into a map with the type as the key. + * + * @param authorizationDetails {@link AuthorizationDetails} instance to be converted. + * @return A map where the key is the type and the value is the corresponding AuthorizationDetails object. + */ + public static Map> getAuthorizationDetailsTypesMap( + final AuthorizationDetails authorizationDetails) { + + return authorizationDetails == null ? Collections.emptyMap() + : authorizationDetails.stream() + .collect(groupingBy(AuthorizationDetail::getType, mapping(identity(), toSet()))); + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/validator/AuthorizationDetailsValidator.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/validator/AuthorizationDetailsValidator.java new file mode 100644 index 00000000000..4aca4c8ff88 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/validator/AuthorizationDetailsValidator.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025, 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.validator; + +import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.validators.OAuth2TokenValidationMessageContext; + +/** + * Interface for validating {@link AuthorizationDetails} in different OAuth2 message contexts. + * + *

This interface provides methods to validate {@link AuthorizationDetails} across various OAuth2 message contexts, + * including authorization requests, token requests, and token validation requests. Implementations of this + * interface should handle the validation logic specific to the type of request and ensure that the returned + * AuthorizationDetails are accurate and compliant with the application's security policies.

+ */ +public interface AuthorizationDetailsValidator { + + /** + * Validates and returns the {@link AuthorizationDetails} for the given {@link OAuthAuthzReqMessageContext}. + *

+ * Validates the {@link AuthorizationDetails} during the authorization request phase. + * This is typically invoked when an authorization request is received and needs to be processed. + * + * @param oAuthAuthzReqMessageContext The OAuth authorization request message context. + * @return The validated {@link AuthorizationDetails}. + * @throws AuthorizationDetailsProcessingException If an error occurs during the processing of authorization details + * @throws IdentityOAuth2ServerException if the validation fails due to a server error. + */ + AuthorizationDetails getValidatedAuthorizationDetails(OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException; + + /** + * Validates and returns the {@link AuthorizationDetails} for the given {@link OAuthTokenReqMessageContext}. + *

+ * Validates the AuthorizationDetails during the token request phase. This is usually called when an authorization + * code is exchanged for an access token, or when a refresh token request is made. + * + * @param oAuthTokenReqMessageContext The OAuth token request message context. + * @return The validated {@link AuthorizationDetails}. + * @throws AuthorizationDetailsProcessingException If an error occurs during the processing of authorization details + * @throws IdentityOAuth2ServerException if the validation fails due to a server error. + */ + AuthorizationDetails getValidatedAuthorizationDetails(OAuthTokenReqMessageContext oAuthTokenReqMessageContext) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException; + + /** + * Validates and returns the {@link AuthorizationDetails} for the given {@link OAuth2TokenValidationMessageContext}. + *

+ * Validates the {@link AuthorizationDetails} during the token validation phase. This method is often used when an + * access token is being introspected to ensure its legitimacy and the associated AuthorizationDetails. + * + * @param oAuth2TokenValidationMessageContext The OAuth2 token validation message context. + * @return The validated {@link AuthorizationDetails}. + * @throws AuthorizationDetailsProcessingException If an error occurs during the processing of authorization details + * @throws IdentityOAuth2ServerException If an error occurs related to the OAuth2 server. + */ + AuthorizationDetails getValidatedAuthorizationDetails(OAuth2TokenValidationMessageContext + oAuth2TokenValidationMessageContext) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException; +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/validator/DefaultAuthorizationDetailsValidator.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/validator/DefaultAuthorizationDetailsValidator.java new file mode 100644 index 00000000000..6e3e3c0996c --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/rar/validator/DefaultAuthorizationDetailsValidator.java @@ -0,0 +1,417 @@ +/* + * Copyright (c) 2025, 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.validator; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.oltu.oauth2.common.message.types.GrantType; +import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; +import org.wso2.carbon.identity.application.common.model.AuthorizationDetailsType; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.oauth.common.OAuthConstants; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; +import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsSchemaValidator; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessor; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessorFactory; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetailsContext; +import org.wso2.carbon.identity.oauth2.rar.model.ValidationResult; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.validators.OAuth2TokenValidationMessageContext; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.TYPE_NOT_SUPPORTED_ERR_FORMAT; +import static org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_MSG; + +/** + * Default implementation class responsible for validating {@link AuthorizationDetails} in different + * OAuth2 message contexts. + */ +public class DefaultAuthorizationDetailsValidator implements AuthorizationDetailsValidator { + + private static final Log log = LogFactory.getLog(DefaultAuthorizationDetailsValidator.class); + private final AuthorizationDetailsProcessorFactory authorizationDetailsProcessorFactory; + private final AuthorizationDetailsService authorizationDetailsService; + private final AuthorizationDetailsSchemaValidator authorizationDetailsSchemaValidator; + + public DefaultAuthorizationDetailsValidator() { + this( + AuthorizationDetailsProcessorFactory.getInstance(), + OAuth2ServiceComponentHolder.getInstance().getAuthorizationDetailsService(), + AuthorizationDetailsSchemaValidator.getInstance() + ); + } + + public DefaultAuthorizationDetailsValidator( + final AuthorizationDetailsProcessorFactory authorizationDetailsProcessorFactory, + final AuthorizationDetailsService authorizationDetailsService, + final AuthorizationDetailsSchemaValidator authorizationDetailsSchemaValidator) { + + this.authorizationDetailsProcessorFactory = authorizationDetailsProcessorFactory; + this.authorizationDetailsService = authorizationDetailsService; + this.authorizationDetailsSchemaValidator = authorizationDetailsSchemaValidator; + } + + /** + * {@inheritDoc} + */ + @Override + public AuthorizationDetails getValidatedAuthorizationDetails(final OAuthAuthzReqMessageContext + oAuthAuthzReqMessageContext) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException { + + try { + + return this.getValidatedAuthorizationDetails( + oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getConsumerKey(), + oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getTenantDomain(), + oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getAuthorizationDetails(), + (detail, type) -> new AuthorizationDetailsContext(detail, type, oAuthAuthzReqMessageContext) + ); + } catch (IdentityOAuth2Exception e) { + log.error("Unable find the tenant ID of the domain: " + + oAuthAuthzReqMessageContext.getAuthorizationReqDTO().getTenantDomain() + " Caused by, ", e); + throw new AuthorizationDetailsProcessingException("Invalid tenant domain", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public AuthorizationDetails getValidatedAuthorizationDetails(final OAuthTokenReqMessageContext + oAuthTokenReqMessageContext) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException { + + final OAuth2AccessTokenReqDTO accessTokenReqDTO = oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO(); + + if (!AuthorizationDetailsUtils.isRichAuthorizationRequest(accessTokenReqDTO.getAuthorizationDetails())) { + if (log.isDebugEnabled()) { + log.debug("Client application does not request new authorization details. " + + "Returning previously validated authorization details."); + } + return oAuthTokenReqMessageContext.getAuthorizationDetails(); + } + + if (GrantType.AUTHORIZATION_CODE.toString().equals(accessTokenReqDTO.getGrantType())) { + if (log.isDebugEnabled()) { + log.debug("Skipping the authorization_details validation for authorization code flow " + + "as this validation has already happened in the authorize flow."); + } + return oAuthTokenReqMessageContext.getAuthorizationDetails(); + } + + final AuthorizationDetails validatedAuthorizationDetails = this.getValidatedAuthorizationDetails( + accessTokenReqDTO.getClientId(), + accessTokenReqDTO.getTenantDomain(), + accessTokenReqDTO.getAuthorizationDetails(), + (detail, type) -> new AuthorizationDetailsContext(detail, type, oAuthTokenReqMessageContext) + ); + + if (GrantType.REFRESH_TOKEN.toString().equals(accessTokenReqDTO.getGrantType())) { + return new AuthorizationDetails(this.filterConsentedAuthorizationDetails(validatedAuthorizationDetails, + oAuthTokenReqMessageContext.getAuthorizationDetails())); + } + + return validatedAuthorizationDetails; + } + + /** + * Validates whether the user has consented to the requested authorization details. + * + * @param requestedAuthorizationDetails The requested authorization details. + * @param consentedAuthorizationDetails The consented authorization details. + * @throws AuthorizationDetailsProcessingException If validation fails. + */ + private Set filterConsentedAuthorizationDetails( + final AuthorizationDetails requestedAuthorizationDetails, + final AuthorizationDetails consentedAuthorizationDetails) + throws AuthorizationDetailsProcessingException { + + final Set validAuthorizationDetails = new HashSet<>(); + if (AuthorizationDetailsUtils.isEmpty(requestedAuthorizationDetails)) { + log.debug("No authorization details requested. Using all consented authorization details."); + validAuthorizationDetails.addAll(consentedAuthorizationDetails.getDetails()); + return validAuthorizationDetails; + } + + if (AuthorizationDetailsUtils.isEmpty(consentedAuthorizationDetails)) { + log.debug("Invalid request. No consented authorization details found."); + throw new AuthorizationDetailsProcessingException(VALIDATION_FAILED_ERR_MSG); + } + + // Map consented authorization details by type for quick lookup + final Map> consentedAuthorizationDetailsByType = + AuthorizationDetailsUtils.getAuthorizationDetailsTypesMap(consentedAuthorizationDetails); + + for (AuthorizationDetail requestedAuthorizationDetail : requestedAuthorizationDetails.getDetails()) { + + final String requestedType = requestedAuthorizationDetail.getType(); + if (!consentedAuthorizationDetailsByType.containsKey(requestedType)) { + if (log.isDebugEnabled()) { + log.debug("User hasn't consented to the requested authorization details type: " + requestedType); + } + throw new AuthorizationDetailsProcessingException(VALIDATION_FAILED_ERR_MSG); + } + + final Optional optProcessor = + this.authorizationDetailsProcessorFactory.getAuthorizationDetailsProcessorByType(requestedType); + + if (optProcessor.isPresent()) { + if (log.isDebugEnabled()) { + log.debug("Validating equality of requested and existing authorization details using processor: " + + optProcessor.get().getClass().getSimpleName()); + } + final AuthorizationDetails existingAuthorizationDetails = + new AuthorizationDetails(consentedAuthorizationDetailsByType.get(requestedType)); + + // If the requested authorization details match the consented ones, add to the valid set + if (optProcessor.get().isEqualOrSubset(requestedAuthorizationDetail, existingAuthorizationDetails)) { + validAuthorizationDetails.add(requestedAuthorizationDetail); + } else { + if (log.isDebugEnabled()) { + log.debug("User hasn't consented to requested authorization details type: " + requestedType); + } + throw new AuthorizationDetailsProcessingException(VALIDATION_FAILED_ERR_MSG); + } + } else { + // Cannot process, returning all consented authorization details + if (CollectionUtils.isNotEmpty(consentedAuthorizationDetailsByType.get(requestedType))) { + validAuthorizationDetails.addAll(consentedAuthorizationDetailsByType.get(requestedType)); + } + consentedAuthorizationDetailsByType.put(requestedType, Collections.emptySet()); + } + } + return validAuthorizationDetails; + } + + /** + * {@inheritDoc} + */ + @Override + public AuthorizationDetails getValidatedAuthorizationDetails( + final OAuth2TokenValidationMessageContext oAuth2TokenValidationMessageContext) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException { + + try { + final AccessTokenDO accessTokenDO = (AccessTokenDO) oAuth2TokenValidationMessageContext + .getProperty(OAuthConstants.ACCESS_TOKEN_DO); + + final AuthorizationDetails accessTokenAuthorizationDetails = this.authorizationDetailsService + .getAccessTokenAuthorizationDetails(accessTokenDO.getTokenId(), accessTokenDO.getTenantID()); + + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(accessTokenAuthorizationDetails)) { + final Set authorizedAuthorizationDetails = + this.getValidatedAuthorizationDetails( + accessTokenDO.getConsumerKey(), + IdentityTenantUtil.getTenantDomain(accessTokenDO.getTenantID()), + accessTokenAuthorizationDetails); + return new AuthorizationDetails(authorizedAuthorizationDetails); + } + } catch (IdentityOAuth2Exception e) { + log.error("Error occurred while retrieving access token authorization details. Caused by, ", e); + throw new AuthorizationDetailsProcessingException("Unable to retrieve token authorization details", e); + } + return new AuthorizationDetails(); + } + + private Set getValidatedAuthorizationDetails( + final String clientId, final String tenantDomain, final AuthorizationDetails authorizationDetails) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException { + + return this.getSchemaCompliantAuthorizationDetails(authorizationDetails, + this.getAuthorizedAuthorizationDetailsTypes(clientId, tenantDomain)); + } + + /** + * Validates the authorization details for OAuthTokenReqMessageContext. + * + * @param clientId The client ID. + * @param tenantDomain The tenant domain. + * @param authorizationDetails The set of authorization details to validate. + * @param contextProvider A lambda function to create the AuthorizationDetailsContext. + * @return An {@link AuthorizationDetails} object containing the validated authorization details. + * @throws AuthorizationDetailsProcessingException if validation fails. + */ + private AuthorizationDetails getValidatedAuthorizationDetails( + final String clientId, final String tenantDomain, final AuthorizationDetails authorizationDetails, + BiFunction contextProvider) + throws AuthorizationDetailsProcessingException, IdentityOAuth2ServerException { + + final Map authorizedDetailsTypes = + this.getAuthorizedAuthorizationDetailsTypes(clientId, tenantDomain); + + final Set validatedAuthorizationDetails = new HashSet<>(); + for (final AuthorizationDetail authorizationDetail : + this.getSchemaCompliantAuthorizationDetails(authorizationDetails, authorizedDetailsTypes)) { + + final AuthorizationDetailsContext authorizationDetailsContext = contextProvider + .apply(authorizationDetail, authorizedDetailsTypes.get(authorizationDetail.getType())); + + if (this.isValidAuthorizationDetail(authorizationDetailsContext)) { + validatedAuthorizationDetails.add(this.getEnrichedAuthorizationDetail(authorizationDetailsContext)); + } + } + return new AuthorizationDetails(validatedAuthorizationDetails); + } + + /** + * 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 Map getAuthorizedAuthorizationDetailsTypes(final String clientId, + final String tenantDomain) + throws IdentityOAuth2ServerException { + + try { + final String appId = AuthorizationDetailsUtils.getApplicationResourceIdFromClientId(clientId); + final List authorizationDetailsTypes = OAuth2ServiceComponentHolder.getInstance() + .getAuthorizedAPIManagementService().getAuthorizedAuthorizationDetailsTypes(appId, tenantDomain); + + if (CollectionUtils.isNotEmpty(authorizationDetailsTypes)) { + return authorizationDetailsTypes.stream() + .collect(Collectors.toMap(AuthorizationDetailsType::getType, Function.identity())); + } + } catch (IdentityOAuth2Exception | IdentityApplicationManagementException e) { + log.error("Unable to retrieve authorized authorization details types. Caused by, ", e); + throw new IdentityOAuth2ServerException("Unable to retrieve authorized authorization details types", e); + } + return Collections.emptyMap(); + } + + private Set getSchemaCompliantAuthorizationDetails( + final AuthorizationDetails authorizationDetails, + final Map authorizedDetailsTypes) + throws AuthorizationDetailsProcessingException { + + final Set schemaCompliantAuthorizationDetails = new HashSet<>(); + for (final AuthorizationDetail authorizationDetail : authorizationDetails.getDetails()) { + + if (log.isDebugEnabled()) { + log.debug("Schema validation started for authorization details type: " + authorizationDetail.getType()); + } + + this.assertAuthorizationDetailTypeSupported(authorizationDetail.getType()); + + if (this.isSchemaCompliant(authorizationDetail.getType(), authorizationDetail, authorizedDetailsTypes)) { + schemaCompliantAuthorizationDetails.add(authorizationDetail); + } + } + return schemaCompliantAuthorizationDetails; + } + + /** + * 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, IdentityOAuth2ServerException { + + final String type = authorizationDetailsContext.getAuthorizationDetail().getType(); + final Optional optProcessor = + this.authorizationDetailsProcessorFactory.getAuthorizationDetailsProcessorByType(type); + + if (optProcessor.isPresent()) { + + final ValidationResult validationResult = optProcessor.get().validate(authorizationDetailsContext); + if (validationResult.isInvalid()) { + if (log.isDebugEnabled()) { + log.debug(String.format("Authorization details validation failed for type: %s. Caused by, %s", + type, validationResult.getReason())); + } + return false; + } + } else { + if (log.isDebugEnabled()) { + log.debug("An authorization details processor implementation is not found for type: " + type); + } + } + return true; + } + + /** + * 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.authorizationDetailsProcessorFactory + .getAuthorizationDetailsProcessorByType(authorizationDetailsContext.getAuthorizationDetail().getType()) + .map(authorizationDetailsProcessor -> authorizationDetailsProcessor.enrich(authorizationDetailsContext)) + // If provider is missing, return the original authorization detail instance + .orElse(authorizationDetailsContext.getAuthorizationDetail()); + } + + private void assertAuthorizationDetailTypeSupported(final String type) + throws AuthorizationDetailsProcessingException { + + if (!this.authorizationDetailsProcessorFactory.isSupportedAuthorizationDetailsType(type)) { + throw new AuthorizationDetailsProcessingException(String.format(TYPE_NOT_SUPPORTED_ERR_FORMAT, type)); + } + } + + private boolean isSchemaCompliant(final String type, final AuthorizationDetail authorizationDetail, + final Map authorizedDetailsTypes) + throws AuthorizationDetailsProcessingException { + + if (!authorizedDetailsTypes.containsKey(type)) { + if (log.isDebugEnabled()) { + log.debug("Request received for unauthorized authorization details type: " + type); + } + throw new AuthorizationDetailsProcessingException(VALIDATION_FAILED_ERR_MSG); + } + + if (this.authorizationDetailsSchemaValidator + .isSchemaCompliant(authorizedDetailsTypes.get(type).getSchema(), authorizationDetail)) { + return true; + } + + if (log.isDebugEnabled()) { + log.debug("Ignoring non-schema-compliant authorization details type: " + type); + } + return false; + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/responsemode/provider/SuccessResponseDTO.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/responsemode/provider/SuccessResponseDTO.java index 4c96445fe54..524e5ce2ab1 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/responsemode/provider/SuccessResponseDTO.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/responsemode/provider/SuccessResponseDTO.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.oauth2.responsemode.provider; import org.apache.commons.lang.StringUtils; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; import java.util.Set; @@ -34,6 +35,7 @@ public class SuccessResponseDTO { private String formPostBody; private String subjectToken; private Set scope = null; + private AuthorizationDetails authorizationDetails; public String getAuthorizationCode() { @@ -117,4 +119,25 @@ public void setSubjectToken(String subjectToken) { this.subjectToken = subjectToken; } + + /** + * Retrieves the authorization details to be included in the successful authorization response. + * + * @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 to be included in the successful authorization response. + * + * @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/responsemode/provider/impl/FragmentResponseModeProvider.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/responsemode/provider/impl/FragmentResponseModeProvider.java index 097530d103c..63288d00018 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/responsemode/provider/impl/FragmentResponseModeProvider.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/responsemode/provider/impl/FragmentResponseModeProvider.java @@ -22,6 +22,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.oauth.common.OAuthConstants; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; import org.wso2.carbon.identity.oauth2.responsemode.provider.AbstractResponseModeProvider; import org.wso2.carbon.identity.oauth2.responsemode.provider.AuthorizationResponseDTO; @@ -62,6 +65,8 @@ public String getAuthResponseRedirectUrl(AuthorizationResponseDTO authorizationR String scope = authorizationResponseDTO.getSuccessResponseDTO().getScope(); String authenticatedIdPs = authorizationResponseDTO.getAuthenticatedIDPs(); String subjectToken = authorizationResponseDTO.getSuccessResponseDTO().getSubjectToken(); + final AuthorizationDetails authorizationDetails = authorizationResponseDTO.getSuccessResponseDTO() + .getAuthorizationDetails(); List params = new ArrayList<>(); if (accessToken != null) { appendParam(params, OAuthConstants.ACCESS_TOKEN_RESPONSE_PARAM, accessToken); @@ -100,6 +105,11 @@ public String getAuthResponseRedirectUrl(AuthorizationResponseDTO authorizationR appendParam(params, OAuthConstants.SUBJECT_TOKEN, subjectToken); } + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(authorizationDetails)) { + params.add(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS + "=" + + AuthorizationDetailsUtils.getUrlEncodedAuthorizationDetails(authorizationDetails)); + } + redirectUrl += "#" + String.join("&", params); } else { diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java index 0158db681b1..b9667cbab63 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/AccessTokenIssuer.java @@ -65,6 +65,12 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; +import org.wso2.carbon.identity.oauth2.rar.validator.AuthorizationDetailsValidator; +import org.wso2.carbon.identity.oauth2.rar.validator.DefaultAuthorizationDetailsValidator; import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinder; import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinding; import org.wso2.carbon.identity.oauth2.token.handlers.grant.AuthorizationGrantHandler; @@ -119,6 +125,7 @@ public class AccessTokenIssuer { private Map authzGrantHandlers; public static final String OAUTH_APP_DO = "OAuthAppDO"; private static final String SERVICE_PROVIDERS_SUB_CLAIM = "ServiceProviders.UseUsernameAsSubClaim"; + private final AuthorizationDetailsValidator authorizationDetailsValidator; /** * Private constructor which will not allow to create objects of this class from outside @@ -126,6 +133,7 @@ public class AccessTokenIssuer { private AccessTokenIssuer() throws IdentityOAuth2Exception { authzGrantHandlers = OAuthServerConfiguration.getInstance().getSupportedGrantTypes(); + this.authorizationDetailsValidator = new DefaultAuthorizationDetailsValidator(); AppInfoCache appInfoCache = AppInfoCache.getInstance(); if (appInfoCache != null) { if (log.isDebugEnabled()) { @@ -449,6 +457,35 @@ private OAuth2AccessTokenRespDTO validateGrantAndIssueToken(OAuth2AccessTokenReq return tokenRespDTO; } + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(tokReqMsgCtx)) { + try { + final AuthorizationDetails validatedAuthorizationDetails = this.authorizationDetailsValidator + .getValidatedAuthorizationDetails(tokReqMsgCtx); + tokReqMsgCtx.setAuthorizationDetails(validatedAuthorizationDetails); + } catch (AuthorizationDetailsProcessingException e) { + if (log.isDebugEnabled()) { + log.debug("Invalid authorization details requested by client Id: " + tokenReqDTO.getClientId()); + } + + if (LoggerUtils.isDiagnosticLogsEnabled()) { + LoggerUtils.triggerDiagnosticLogEvent(new DiagnosticLog.DiagnosticLogBuilder( + OAuthConstants.LogConstants.OAUTH_INBOUND_SERVICE, + OAuthConstants.LogConstants.ActionIDs.VALIDATE_AUTHORIZATION_DETAILS) + .inputParam(LogConstants.InputKeys.CLIENT_ID, tokenReqDTO.getClientId()) + .inputParam(OAuthConstants.LogConstants.InputKeys.REQUESTED_AUTHORIZATION_DETAILS, + tokenReqDTO.getAuthorizationDetails().toSet()) + .resultMessage(AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_MSG) + .logDetailLevel(DiagnosticLog.LogDetailLevel.APPLICATION) + .resultStatus(DiagnosticLog.ResultStatus.FAILED)); + } + tokenRespDTO = handleError(AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_CODE, + AuthorizationDetailsConstants.VALIDATION_FAILED_ERR_MSG, tokenReqDTO); + setResponseHeaders(tokReqMsgCtx, tokenRespDTO); + triggerPostListeners(tokenReqDTO, tokenRespDTO, tokReqMsgCtx, isRefreshRequest); + return tokenRespDTO; + } + } + handleTokenBinding(tokenReqDTO, grantType, tokReqMsgCtx, oAuthAppDO); try { diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java index 5955e9b509d..5552c809f08 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/OAuthTokenReqMessageContext.java @@ -21,6 +21,7 @@ import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; import org.wso2.carbon.identity.oauth.common.OAuthConstants; import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinding; import java.util.List; @@ -64,6 +65,8 @@ public class OAuthTokenReqMessageContext { private Map additionalAccessTokenClaims; + private AuthorizationDetails authorizationDetails; + public OAuthTokenReqMessageContext(OAuth2AccessTokenReqDTO oauth2AccessTokenReqDTO) { this.oauth2AccessTokenReqDTO = oauth2AccessTokenReqDTO; @@ -231,4 +234,26 @@ public void setAdditionalAccessTokenClaims(Map additionalAccessT this.additionalAccessTokenClaims = additionalAccessTokenClaims; } + + /** + * Retrieves the user consented or authorized authorization details. + * + * @return the {@link AuthorizationDetails} instance representing the rich authorization requests. + * If no authorization details are available, it will return {@code null}. + */ + public AuthorizationDetails getAuthorizationDetails() { + + return this.authorizationDetails; + } + + /** + * Sets the validated 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/token/handlers/grant/AbstractAuthorizationGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandler.java index 4a628f0bef3..02aa8ddfa52 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AbstractAuthorizationGrantHandler.java @@ -59,6 +59,8 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; import org.wso2.carbon.identity.oauth2.token.OauthTokenIssuer; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; @@ -102,6 +104,7 @@ public abstract class AbstractAuthorizationGrantHandler implements Authorization protected static final String EXISTING_TOKEN_ISSUED = "existingTokenUsed"; protected static final int SECONDS_TO_MILISECONDS_FACTOR = 1000; private boolean isHashDisabled = OAuth2Util.isHashDisabled(); + protected AuthorizationDetailsService authorizationDetailsService; @Override public void init() throws IdentityOAuth2Exception { @@ -111,6 +114,7 @@ public void init() throws IdentityOAuth2Exception { cacheEnabled = true; oauthCache = OAuthCache.getInstance(); } + this.authorizationDetailsService = OAuth2ServiceComponentHolder.getInstance().getAuthorizationDetailsService(); } @Override @@ -456,6 +460,11 @@ private OAuth2AccessTokenRespDTO issueExistingAccessToken(OAuthTokenReqMessageCo existingTokenBean.getTokenId(), true); } + if (AuthorizationDetailsUtils.isRichAuthorizationRequest(tokReqMsgCtx.getAuthorizationDetails())) { + this.authorizationDetailsService.replaceAccessTokenAuthorizationDetails(existingTokenBean.getTokenId(), + existingTokenBean, tokReqMsgCtx); + } + setDetailsToMessageContext(tokReqMsgCtx, existingTokenBean); return createResponseWithTokenBean(existingTokenBean, expireTime, scope); } @@ -671,6 +680,8 @@ private void persistAccessTokenInDB(OAuthTokenReqMessageContext tokReqMsgCtx, Ac } storeAccessToken(tokenReq, getUserStoreDomain(tokReqMsgCtx.getAuthorizedUser()), newTokenBean, newAccessToken, existingTokenBean); + this.authorizationDetailsService + .storeOrReplaceAccessTokenAuthorizationDetails(newTokenBean, existingTokenBean, tokReqMsgCtx); } private void updateCacheIfEnabled(AccessTokenDO newTokenBean, String scope, OauthTokenIssuer oauthTokenIssuer) diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java index 21b3ae46d03..6c627cb759d 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/AuthorizationCodeGrantHandler.java @@ -49,6 +49,8 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenRespDTO; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; import org.wso2.carbon.identity.oauth2.model.AuthzCodeDO; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import org.wso2.carbon.utils.DiagnosticLog; @@ -126,12 +128,15 @@ public String buildSyncLockString(OAuthTokenReqMessageContext tokReqMsgCtx) { } private void setPropertiesForTokenGeneration(OAuthTokenReqMessageContext tokReqMsgCtx, - OAuth2AccessTokenReqDTO tokenReq, AuthzCodeDO authzCodeBean) { + OAuth2AccessTokenReqDTO tokenReq, AuthzCodeDO authzCodeBean) + throws IdentityOAuth2Exception { + tokReqMsgCtx.setAuthorizedUser(authzCodeBean.getAuthorizedUser()); tokReqMsgCtx.setScope(authzCodeBean.getScope()); // keep the pre processed authz code as a OAuthTokenReqMessageContext property to avoid // calculating it again when issuing the access token. tokReqMsgCtx.addProperty(AUTHZ_CODE, tokenReq.getAuthorizationCode()); + this.setRARPropertiesForTokenGeneration(tokReqMsgCtx); } private boolean validateCallbackUrlFromRequest(String callbackUrlFromRequest, @@ -669,4 +674,35 @@ private String resolveUserResidentOrganization(Map userAtt } return null; } + + /** + * Configures RAR properties for token generation in the OAuth 2.0 flow. + *

+ * It checks if authorization details were included in the authorization code request and whether the + * user has consented to these specific authorization details. Depending on user consent, it selects + * the appropriate authorization details to be included in the token response. + *

+ * + * @param oAuthTokenReqMessageContext Context of the OAuth token request message. + * @throws IdentityOAuth2Exception If an error occurs while retrieving authorization details. + */ + private void setRARPropertiesForTokenGeneration(final OAuthTokenReqMessageContext oAuthTokenReqMessageContext) + throws IdentityOAuth2Exception { + + final int tenantId = + OAuth2Util.getTenantId(oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO().getTenantDomain()); + + if (log.isDebugEnabled()) { + log.debug("Retrieving user consented authorization details for user: " + + oAuthTokenReqMessageContext.getAuthorizedUser().getLoggableMaskedUserId()); + } + + final AuthorizationDetails authorizationCodeAuthorizationDetails = super.authorizationDetailsService + .getAuthorizationCodeAuthorizationDetails( + oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO().getAuthorizationCode(), + tenantId); + + oAuthTokenReqMessageContext.setAuthorizationDetails(AuthorizationDetailsUtils + .getTrimmedAuthorizationDetails(authorizationCodeAuthorizationDetails)); + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java index 7a6762a1270..05e954235da 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandler.java @@ -61,6 +61,8 @@ import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsUtils; import org.wso2.carbon.identity.oauth2.token.AccessTokenIssuer; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; import org.wso2.carbon.identity.oauth2.token.OauthTokenIssuer; @@ -168,6 +170,8 @@ public OAuth2AccessTokenRespDTO issue(OAuthTokenReqMessageContext tokReqMsgCtx) // sets accessToken, refreshToken and validity data setTokenData(accessTokenBean, tokReqMsgCtx, validationBean, tokenReq, accessTokenBean.getIssuedTime()); persistNewToken(tokReqMsgCtx, accessTokenBean, tokenReq.getClientId()); + super.authorizationDetailsService + .replaceAccessTokenAuthorizationDetails(validationBean.getTokenId(), accessTokenBean, tokReqMsgCtx); if (log.isDebugEnabled()) { log.debug("Persisted an access token for the refresh token, " + @@ -275,6 +279,7 @@ private void setPropertiesForTokenGeneration(OAuthTokenReqMessageContext tokReqM // Store the old access token as a OAuthTokenReqMessageContext property, this is already // a preprocessed token. tokReqMsgCtx.addProperty(PREV_ACCESS_TOKEN, validationBean); + this.setRARPropertiesForTokenGeneration(tokReqMsgCtx, validationBean); /* Add the session id from the last access token to OAuthTokenReqMessageContext. First check whether the @@ -956,4 +961,31 @@ private RefreshTokenGrantProcessor getRefreshTokenGrantProcessor() { return OAuth2ServiceComponentHolder.getInstance().getRefreshTokenGrantProcessor(); } + + /** + * Sets the RAR properties for token generation. + *

It retrieves the token authorization details based on the provided OAuth token request context.

+ * + * @param oAuthTokenReqMessageContext Context of the OAuth token request message. + * @param validationBean Refresh token validation data. + * @throws IdentityOAuth2Exception If an error occurs while retrieving authorization details. + */ + private void setRARPropertiesForTokenGeneration(final OAuthTokenReqMessageContext oAuthTokenReqMessageContext, + final RefreshTokenValidationDataDO validationBean) + throws IdentityOAuth2Exception { + + if (log.isDebugEnabled()) { + log.debug("Retrieving token authorization details for user: " + + oAuthTokenReqMessageContext.getAuthorizedUser().getLoggableMaskedUserId()); + } + + final int tenantId = + OAuth2Util.getTenantId(oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO().getTenantDomain()); + + AuthorizationDetails tokenAuthorizationDetails = super.authorizationDetailsService + .getAccessTokenAuthorizationDetails(validationBean.getTokenId(), tenantId); + + oAuthTokenReqMessageContext.setAuthorizationDetails(AuthorizationDetailsUtils + .getTrimmedAuthorizationDetails(tokenAuthorizationDetails)); + } } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/TestConstants.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/TestConstants.java index b07c1e8dd2b..add9c801064 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/TestConstants.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/TestConstants.java @@ -88,4 +88,10 @@ public class TestConstants { public static final String FAPI_SIGNATURE_ALG_CONFIGURATION = "OAuth.OpenIDConnect.FAPI." + "AllowedSignatureAlgorithms.AllowedSignatureAlgorithm"; + + // Rich Authorization Requests + public static final String TEST_CONSENT_ID = "52481ccd-0927-4d17-8cfc-5110fc4aa009"; + public static final String TEST_USER_ID = "c2179b58-b048-49d1-acb4-45b672d6fe5f"; + public static final String TEST_APP_ID = "a49257ea-3d5d-4558-b0c5-f9b3b0ca2fb0"; + public static final String TEST_TYPE = "test_type_v1"; } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsServiceTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsServiceTest.java new file mode 100644 index 00000000000..3db8fc6952e --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/AuthorizationDetailsServiceTest.java @@ -0,0 +1,493 @@ +/* + * Copyright (c) 2025, 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.oltu.oauth2.common.exception.OAuthSystemException; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.application.common.model.ServiceProvider; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessor; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessorFactory; +import org.wso2.carbon.identity.oauth2.rar.dao.AuthorizationDetailsDAO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsConsentDTO; +import org.wso2.carbon.identity.oauth2.rar.dto.AuthorizationDetailsTokenDTO; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetailsContext; +import org.wso2.carbon.identity.oauth2.rar.model.ValidationResult; +import org.wso2.carbon.identity.oauth2.rar.utils.AuthorizationDetailsBaseTest; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toSet; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertNull; +import static org.wso2.carbon.identity.oauth2.TestConstants.ACESS_TOKEN_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.CLIENT_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.TENANT_DOMAIN; +import static org.wso2.carbon.identity.oauth2.TestConstants.TENANT_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.TEST_APP_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.TEST_CONSENT_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.TEST_TYPE; +import static org.wso2.carbon.identity.oauth2.TestConstants.TEST_USER_ID; + +/** + * Test class for {@link AuthorizationDetailsService}. + */ +@WithCarbonHome +public class AuthorizationDetailsServiceTest extends AuthorizationDetailsBaseTest { + + private AuthorizationDetailsDAO authorizationDetailsDAOMock; + private MockedStatic oAuth2UtilMock; + + private AuthorizationDetailsService uut; + + @BeforeClass + public void setUp() throws SQLException { + + this.oAuth2UtilMock = Mockito.mockStatic(OAuth2Util.class); + this.oAuth2UtilMock.when(() -> OAuth2Util.getTenantId(TENANT_DOMAIN)).thenReturn(TENANT_ID); + + ServiceProvider serviceProvider = new ServiceProvider(); + serviceProvider.setApplicationResourceId(TEST_APP_ID); + this.oAuth2UtilMock.when(() -> OAuth2Util.getServiceProvider(CLIENT_ID)).thenReturn(serviceProvider); + } + + @AfterClass + public void tearDown() { + + if (this.oAuth2UtilMock != null && !this.oAuth2UtilMock.isClosed()) { + this.oAuth2UtilMock.close(); + } + } + + @BeforeMethod() + public void setUpMethod() + throws SQLException, IdentityOAuth2ServerException, AuthorizationDetailsProcessingException { + + this.authorizationDetailsDAOMock = Mockito.mock(AuthorizationDetailsDAO.class); + when(this.authorizationDetailsDAOMock.getConsentIdByUserIdAndAppId(TEST_USER_ID, TEST_APP_ID, TENANT_ID)) + .thenReturn(TEST_CONSENT_ID); + + when(this.authorizationDetailsDAOMock.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TENANT_ID)) + .thenReturn(Collections.singleton(new AuthorizationDetailsConsentDTO(TEST_CONSENT_ID, + this.authorizationDetail, true, TENANT_ID))); + + when(this.authorizationDetailsDAOMock.getAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID)) + .thenReturn(Collections.singleton(new AuthorizationDetailsTokenDTO(ACESS_TOKEN_ID, + this.authorizationDetail, TENANT_ID))); + + AuthorizationDetailsProcessor processor = Mockito.mock(AuthorizationDetailsProcessor.class); + when(processor.isEqualOrSubset(any(AuthorizationDetail.class), any(AuthorizationDetails.class))) + .thenReturn(true); + when(processor.enrich(any(AuthorizationDetailsContext.class))).thenReturn(this.authorizationDetail); + when(processor.getType()).thenReturn(TEST_TYPE); + when(processor.validate(any(AuthorizationDetailsContext.class))).thenReturn(ValidationResult.valid()); + + this.processorFactoryMock = Mockito.mock(AuthorizationDetailsProcessorFactory.class); + when(this.processorFactoryMock.getAuthorizationDetailsProcessorByType(TEST_TYPE)) + .thenReturn(Optional.of(processor)); + + uut = new AuthorizationDetailsService(this.processorFactoryMock, this.authorizationDetailsDAOMock, true); + } + + @BeforeMethod(onlyForGroups = {"error-flow-tests"}, dependsOnMethods = {"setUpMethod"}) + public void setUpErrorMethod() throws SQLException { + + when(this.authorizationDetailsDAOMock.addUserConsentedAuthorizationDetails(anySet())) + .thenThrow(SQLException.class); + when(this.authorizationDetailsDAOMock.deleteUserConsentedAuthorizationDetails(anyString(), anyInt())) + .thenThrow(SQLException.class); + when(this.authorizationDetailsDAOMock.getUserConsentedAuthorizationDetails(anyString(), anyInt())) + .thenThrow(SQLException.class); + when(this.authorizationDetailsDAOMock.getAccessTokenAuthorizationDetails(anyString(), anyInt())) + .thenThrow(SQLException.class); + when(this.authorizationDetailsDAOMock.addAccessTokenAuthorizationDetails(anySet())) + .thenThrow(SQLException.class); + when(this.authorizationDetailsDAOMock.deleteAccessTokenAuthorizationDetails(anyString(), anyInt())) + .thenThrow(SQLException.class); + + uut = new AuthorizationDetailsService(this.processorFactoryMock, this.authorizationDetailsDAOMock, true); + } + + @BeforeMethod(onlyForGroups = {"feature-disabled-flow-tests"}, dependsOnMethods = {"setUpErrorMethod"}) + public void setUpFeatureDisabledMethod() { + + uut = new AuthorizationDetailsService(this.processorFactoryMock, this.authorizationDetailsDAOMock, false); + } + + @Test + public void shouldNotAddUserConsentedAuthorizationDetails_ifNotRichAuthorizationRequest() + throws OAuthSystemException, SQLException { + + uut.storeOrUpdateUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, + new OAuth2Parameters(), authorizationDetails); + + verify(authorizationDetailsDAOMock, times(0)).addUserConsentedAuthorizationDetails(anySet()); + } + + @Test + public void shouldNotAddUserConsentedAuthorizationDetails_whenConsentIsNotFound() + throws OAuthSystemException, SQLException { + + final OAuth2Parameters oAuth2Parameters = new OAuth2Parameters(); + oAuth2Parameters.setAuthorizationDetails(authorizationDetails); + + uut.storeOrUpdateUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, + oAuth2Parameters, authorizationDetails); + + verify(authorizationDetailsDAOMock, times(0)).addUserConsentedAuthorizationDetails(anySet()); + } + + @Test + public void shouldAddUserConsentedAuthorizationDetails_ifRichAuthorizationRequest() + throws OAuthSystemException, SQLException { + + when(this.authorizationDetailsDAOMock.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TENANT_ID)) + .thenReturn(Collections.emptySet()); + + uut.storeOrUpdateUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, + oAuth2Parameters, authorizationDetails); + + verify(authorizationDetailsDAOMock, times(1)).addUserConsentedAuthorizationDetails(anySet()); + } + + @Test + public void shouldNotDeleteUserConsentedAuthorizationDetails_ifNotRichAuthorizationRequest() + throws OAuthSystemException, SQLException { + + uut.deleteUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, new OAuth2Parameters()); + + verify(authorizationDetailsDAOMock, times(0)).deleteUserConsentedAuthorizationDetails(anyString(), anyInt()); + } + + @Test + public void shouldNotDeleteUserConsentedAuthorizationDetails_whenConsentIsNotFound() + throws OAuthSystemException, SQLException { + + final OAuth2Parameters oAuth2Parameters = new OAuth2Parameters(); + oAuth2Parameters.setAuthorizationDetails(authorizationDetails); + + uut.deleteUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, oAuth2Parameters); + + verify(authorizationDetailsDAOMock, times(0)).deleteUserConsentedAuthorizationDetails(anyString(), anyInt()); + } + + @Test + public void shouldDeleteUserConsentedAuthorizationDetails_ifRichAuthorizationRequest() + throws OAuthSystemException, SQLException { + + uut.deleteUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, oAuth2Parameters); + + verify(authorizationDetailsDAOMock, times(1)).deleteUserConsentedAuthorizationDetails(anyString(), anyInt()); + } + + @Test + public void shouldReplaceUserConsentedAuthorizationDetails_ifRichAuthorizationRequest() + throws OAuthSystemException, SQLException { + + when(this.authorizationDetailsDAOMock.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TENANT_ID)) + .thenReturn(Collections.emptySet()); + uut.replaceUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, + oAuth2Parameters, authorizationDetails); + + verify(authorizationDetailsDAOMock, times(1)) + .deleteUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TENANT_ID); + verify(authorizationDetailsDAOMock, times(1)).addUserConsentedAuthorizationDetails(anySet()); + } + + @Test + public void shouldReturnTrue_ifNotRichAuthorizationRequest() throws IdentityOAuth2Exception { + + assertTrue(uut.isUserAlreadyConsentedForAuthorizationDetails(authenticatedUser, new OAuth2Parameters())); + } + + @Test + public void shouldReturnFalse_ifAuthorizationDetailsAlreadyConsented() throws IdentityOAuth2Exception { + + assertFalse(uut.isUserAlreadyConsentedForAuthorizationDetails(authenticatedUser, oAuth2Parameters)); + } + + @Test + public void shouldReturnNull_whenConsentIsInvalid() throws IdentityOAuth2Exception { + + AuthenticatedUser invalidUser = new AuthenticatedUser(); + invalidUser.setUserId("invalid-user-id"); + + assertNull(uut.getUserConsentedAuthorizationDetails(invalidUser, CLIENT_ID, TENANT_ID)); + } + + @Test + public void shouldReturnUserConsentedAuthorizationDetails_whenConsentIsValid() throws IdentityOAuth2Exception { + + final AuthorizationDetails authorizationDetails = + uut.getUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, TENANT_ID); + + assertEquals(1, authorizationDetails.getDetails().size()); + authorizationDetails.stream().forEach(detail -> assertEquals(TEST_TYPE, detail.getType())); + + final AuthorizationDetails authorizationDetails1 = + uut.getUserConsentedAuthorizationDetails(authenticatedUser, oAuth2Parameters); + + assertEquals(1, authorizationDetails1.getDetails().size()); + authorizationDetails1.stream().forEach(detail -> assertEquals(TEST_TYPE, detail.getType())); + } + + @Test + public void shouldReturnEmptyAuthorizationDetails_whenAccessTokenIsNotFound() throws IdentityOAuth2Exception { + + assertTrue(uut.getAccessTokenAuthorizationDetails("invalid-access-token", TENANT_ID) + .getDetails().isEmpty()); + } + + @Test + public void shouldReturnAccessTokenAuthorizationDetails_whenTokenIsValid() throws IdentityOAuth2Exception { + + AuthorizationDetails authorizationDetails = uut.getAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID); + + assertEquals(1, authorizationDetails.getDetails().size()); + authorizationDetails.stream().forEach(ad -> assertEquals(TEST_TYPE, ad.getType())); + } + + @Test + public void shouldNotAddAccessTokenAuthorizationDetails_ifNotRichAuthorizationRequest() + throws SQLException, IdentityOAuth2Exception { + + uut.storeAccessTokenAuthorizationDetails(accessTokenDO, new OAuthAuthzReqMessageContext(null)); + + verify(authorizationDetailsDAOMock, times(0)).addAccessTokenAuthorizationDetails(anySet()); + } + + @Test + public void shouldAddAccessTokenAuthorizationDetails_ifRichAuthorizationRequest() + throws SQLException, IdentityOAuth2Exception { + + OAuthAuthzReqMessageContext oAuthAuthzReqMessageContext = new OAuthAuthzReqMessageContext(null); + oAuthAuthzReqMessageContext.setApprovedAuthorizationDetails(authorizationDetails); + + uut.storeAccessTokenAuthorizationDetails(accessTokenDO, oAuthAuthzReqMessageContext); + + verify(authorizationDetailsDAOMock, times(1)).addAccessTokenAuthorizationDetails(anySet()); + } + + @Test + public void shouldNotReplaceAccessTokenAuthorizationDetails_ifNotRichAuthorizationRequest() + throws SQLException, IdentityOAuth2Exception { + + uut.storeOrReplaceAccessTokenAuthorizationDetails(accessTokenDO, accessTokenDO, + new OAuthTokenReqMessageContext(new OAuth2AccessTokenReqDTO())); + + verify(authorizationDetailsDAOMock, times(0)).addAccessTokenAuthorizationDetails(anySet()); + verify(authorizationDetailsDAOMock, times(0)).deleteAccessTokenAuthorizationDetails(anyString(), anyInt()); + } + + @Test + public void shouldNotDeleteAccessTokenAuthorizationDetails_whenOldAccessTokenIsMissing() + throws SQLException, IdentityOAuth2Exception { + + OAuthTokenReqMessageContext messageContext = new OAuthTokenReqMessageContext(new OAuth2AccessTokenReqDTO()); + messageContext.setAuthorizationDetails(authorizationDetails); + + uut.storeOrReplaceAccessTokenAuthorizationDetails(accessTokenDO, null, messageContext); + + verify(authorizationDetailsDAOMock, times(1)).addAccessTokenAuthorizationDetails(anySet()); + verify(authorizationDetailsDAOMock, times(0)).deleteAccessTokenAuthorizationDetails(anyString(), anyInt()); + } + + @Test + public void shouldReplaceAccessTokenAuthorizationDetails_whenOldAccessTokenIsPresent() + throws SQLException, IdentityOAuth2Exception { + + OAuthTokenReqMessageContext messageContext = new OAuthTokenReqMessageContext(new OAuth2AccessTokenReqDTO()); + messageContext.setAuthorizationDetails(authorizationDetails); + + uut.storeOrReplaceAccessTokenAuthorizationDetails(accessTokenDO, accessTokenDO, messageContext); + + verify(authorizationDetailsDAOMock, times(1)).addAccessTokenAuthorizationDetails(anySet()); + verify(authorizationDetailsDAOMock, times(1)).deleteAccessTokenAuthorizationDetails(anyString(), anyInt()); + } + + @Test + public void shouldReplaceAccessTokenAuthorizationDetails_ifRichAuthorizationRequest() + throws SQLException, IdentityOAuth2Exception { + + OAuthTokenReqMessageContext messageContext = new OAuthTokenReqMessageContext(new OAuth2AccessTokenReqDTO()); + messageContext.setAuthorizationDetails(authorizationDetails); + + final String oldAccessTokenId = "b8488717-267c-4f45-b039-f31a8efe7cac"; + uut.replaceAccessTokenAuthorizationDetails(oldAccessTokenId, accessTokenDO, messageContext); + + verify(authorizationDetailsDAOMock, times(1)) + .deleteAccessTokenAuthorizationDetails(oldAccessTokenId, TENANT_ID); + verify(authorizationDetailsDAOMock, times(1)).addAccessTokenAuthorizationDetails(anySet()); + } + + @Test + public void shouldDeleteAccessTokenAuthorizationDetails_ifAccessTokenIsValid() + throws SQLException, IdentityOAuth2Exception { + + uut.deleteAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID); + + verify(authorizationDetailsDAOMock, times(1)).deleteAccessTokenAuthorizationDetails(anyString(), anyInt()); + } + + @Test + public void shouldReturnEmptyAuthorizationDetails_ifNotRichAuthorizationRequest() throws IdentityOAuth2Exception { + + assertTrue(uut.getConsentRequiredAuthorizationDetails(authenticatedUser, new OAuth2Parameters()) + .getDetails().isEmpty()); + } + + @Test + public void shouldReturnEmptyAuthorizationDetails_ifProcessorIsMissing() throws IdentityOAuth2Exception { + + assertTrue(uut.getConsentRequiredAuthorizationDetails(authenticatedUser, new OAuth2Parameters()) + .getDetails().isEmpty()); + } + + @Test + public void shouldReturnConsentRequiredAuthorizationDetails() throws IdentityOAuth2Exception { + + final String testTypeV2 = "test_type_v2"; + AuthorizationDetail authorizationDetail = new AuthorizationDetail(); + authorizationDetail.setType(testTypeV2); + + Set detailSet = + Stream.of(authorizationDetails.getDetails(), Collections.singleton(authorizationDetail)) + .flatMap(Set::stream) + .collect(toSet()); + + oAuth2Parameters.setAuthorizationDetails(new AuthorizationDetails(detailSet)); + + uut.getConsentRequiredAuthorizationDetails(authenticatedUser, oAuth2Parameters) + .stream() + .forEach(ad -> assertEquals(testTypeV2, ad.getType())); + } + + @Test(groups = {"error-flow-tests"}, expectedExceptions = {OAuthSystemException.class}) + public void shouldThrowOAuthSystemException_onUserConsentAuthorizationDetailsInsertionFailure() + throws OAuthSystemException { + + uut.storeOrUpdateUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, + oAuth2Parameters, authorizationDetails); + } + + @Test(groups = {"error-flow-tests"}, expectedExceptions = {OAuthSystemException.class}) + public void shouldThrowOAuthSystemException_onUserConsentAuthorizationDetailsDeletionFailure() + throws OAuthSystemException { + + uut.deleteUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, oAuth2Parameters); + } + + @Test(groups = {"error-flow-tests"}, expectedExceptions = {IdentityOAuth2Exception.class}) + public void shouldThrowIdentityOAuth2Exception_onUserConsentAuthorizationDetailsRetrievalFailure() + throws IdentityOAuth2Exception { + + uut.getUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, TENANT_ID); + } + + @Test(groups = {"error-flow-tests"}, expectedExceptions = {IdentityOAuth2Exception.class}) + public void shouldThrowIdentityOAuth2Exception_onAccessTokenAuthorizationDetailsRetrievalFailure() + throws IdentityOAuth2Exception { + + uut.getAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID); + } + + @Test(groups = {"error-flow-tests"}, expectedExceptions = {IdentityOAuth2Exception.class}) + public void shouldThrowIdentityOAuth2Exception_onAccessTokenAuthorizationDetailsInsertionFailure() + throws IdentityOAuth2Exception { + + uut.storeAccessTokenAuthorizationDetails(accessTokenDO, authorizationDetails); + } + + @Test(groups = {"error-flow-tests"}, expectedExceptions = {IdentityOAuth2Exception.class}) + public void shouldThrowIdentityOAuth2Exception_onAccessTokenAuthorizationDetailsDeletionFailure() + throws IdentityOAuth2Exception { + + uut.deleteAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID); + } + + @Test(groups = {"feature-disabled-flow-tests"}) + public void testUserConsentedAuthorizationDetailsWhenFeatureIsDisabled() + throws OAuthSystemException, SQLException, IdentityOAuth2Exception { + + this.uut.storeOrUpdateUserConsentedAuthorizationDetails(authenticatedUser, CLIENT_ID, oAuth2Parameters, + new AuthorizationDetails()); + verify(authorizationDetailsDAOMock, times(0)).addUserConsentedAuthorizationDetails(anySet()); + verify(authorizationDetailsDAOMock, times(0)).updateUserConsentedAuthorizationDetails(anySet()); + + this.uut.getUserConsentedAuthorizationDetails(TEST_CONSENT_ID, TENANT_ID); + verify(authorizationDetailsDAOMock, times(0)).getUserConsentedAuthorizationDetails(anyString(), anyInt()); + + this.uut.deleteUserConsentedAuthorizationDetails(authenticatedUser, TEST_CONSENT_ID, oAuth2Parameters); + verify(authorizationDetailsDAOMock, times(0)).deleteUserConsentedAuthorizationDetails(anyString(), anyInt()); + } + + @Test(groups = {"feature-disabled-flow-tests"}) + public void testAccessTokenAuthorizationDetailsWhenFeatureIsDisabled() + throws SQLException, IdentityOAuth2Exception { + + this.uut.storeAccessTokenAuthorizationDetails(accessTokenDO, authorizationDetails); + verify(authorizationDetailsDAOMock, times(0)).addAccessTokenAuthorizationDetails(anySet()); + + this.uut.getAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID); + verify(authorizationDetailsDAOMock, times(0)).getAccessTokenAuthorizationDetails(anyString(), anyInt()); + + this.uut.deleteAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID); + verify(authorizationDetailsDAOMock, times(0)).getAccessTokenAuthorizationDetails(anyString(), anyInt()); + } + + @Test(groups = {"feature-disabled-flow-tests"}) + public void testOAuth2CodeAuthorizationDetailsWhenFeatureIsDisabled() throws SQLException, IdentityOAuth2Exception { + + uut.storeAuthorizationCodeAuthorizationDetails(null, new OAuthAuthzReqMessageContext(null)); + verify(authorizationDetailsDAOMock, times(0)).addOAuth2CodeAuthorizationDetails(anySet()); + + this.uut.getAuthorizationCodeAuthorizationDetails("", TENANT_ID); + verify(authorizationDetailsDAOMock, times(0)).getOAuth2CodeAuthorizationDetails(anyString(), anyInt()); + } + +} diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/AccessTokenResponseRARHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/AccessTokenResponseRARHandlerTest.java new file mode 100644 index 00000000000..56f6cd8d2e5 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/AccessTokenResponseRARHandlerTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025, 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.token; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.rar.utils.AuthorizationDetailsBaseTest; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; + +/** + * Test class for {@link AccessTokenResponseRARHandler}. + */ +public class AccessTokenResponseRARHandlerTest extends AuthorizationDetailsBaseTest { + + private AccessTokenResponseRARHandler uut; + + @BeforeClass + public void setUp() { + this.uut = new AccessTokenResponseRARHandler(); + } + + @Test + public void shouldReturnAuthorizationDetails_ifRichAuthorizationRequest() throws IdentityOAuth2Exception { + + assertAuthorizationDetailsPresent(uut.getAdditionalTokenResponseAttributes(tokenReqMessageContext)); + } + + @Test + public void shouldReturnEmpty_ifNotRichAuthorizationRequest() throws IdentityOAuth2Exception { + + OAuthTokenReqMessageContext messageContext = new OAuthTokenReqMessageContext(new OAuth2AccessTokenReqDTO()); + assertAuthorizationDetailsMissing(uut.getAdditionalTokenResponseAttributes(messageContext)); + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/IntrospectionRARDataProviderTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/IntrospectionRARDataProviderTest.java new file mode 100644 index 00000000000..a365e646b58 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/IntrospectionRARDataProviderTest.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025, 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.token; + +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.oauth.tokenprocessor.TokenProvider; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationResponseDTO; +import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; +import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.utils.AuthorizationDetailsBaseTest; +import org.wso2.carbon.identity.oauth2.rar.validator.AuthorizationDetailsValidator; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; +import org.wso2.carbon.identity.oauth2.validators.OAuth2TokenValidationMessageContext; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +/** + * Test class for {@link IntrospectionRARDataProvider}. + */ +@WithCarbonHome +public class IntrospectionRARDataProviderTest extends AuthorizationDetailsBaseTest { + + private AuthorizationDetailsValidator validatorMock; + private OAuth2ServiceComponentHolder componentHolderMock; + + private IntrospectionRARDataProvider uut; + + @BeforeClass + public void setUpClass() throws IdentityOAuth2Exception, AuthorizationDetailsProcessingException { + + this.validatorMock = Mockito.mock(AuthorizationDetailsValidator.class); + when(validatorMock.getValidatedAuthorizationDetails(any(OAuth2TokenValidationMessageContext.class))) + .thenReturn(authorizationDetails); + + this.uut = new IntrospectionRARDataProvider(validatorMock); + + AccessTokenDO accessTokenDO = new AccessTokenDO(); + + TokenProvider tokenProviderMock = Mockito.mock(TokenProvider.class); + when(tokenProviderMock.getVerifiedAccessToken(anyString(), anyBoolean())).thenReturn(accessTokenDO); + + this.componentHolderMock = Mockito.mock(OAuth2ServiceComponentHolder.class); + when(componentHolderMock.getTokenProvider()).thenReturn(tokenProviderMock); + } + + @Test(priority = 1) + public void shouldNotReturnAuthorizationDetails_ifNotRichAuthorizationRequest() + throws IdentityOAuth2Exception, AuthorizationDetailsProcessingException { + + when(validatorMock.getValidatedAuthorizationDetails(any(OAuth2TokenValidationMessageContext.class))) + .thenReturn(new AuthorizationDetails()); + + try (MockedStatic componentHolderMock = + Mockito.mockStatic(OAuth2ServiceComponentHolder.class)) { + + componentHolderMock.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(this.componentHolderMock); + + assertAuthorizationDetailsMissing(uut.getIntrospectionData(tokenValidationRequestDTO, + introspectionResponseDTO)); + } + } + + @Test + public void shouldReturnAuthorizationDetails_ifRichAuthorizationRequestAndContextIsMissing() + throws IdentityOAuth2Exception { + + try (MockedStatic oAuth2UtilMock = Mockito.mockStatic(OAuth2Util.class); + MockedStatic componentHolderMock = + Mockito.mockStatic(OAuth2ServiceComponentHolder.class)) { + + oAuth2UtilMock.when(() -> OAuth2Util.buildScopeArray(any())).thenReturn(new String[0]); + componentHolderMock.when(OAuth2ServiceComponentHolder::getInstance) + .thenReturn(this.componentHolderMock); + + assertAuthorizationDetailsPresent(uut.getIntrospectionData(tokenValidationRequestDTO, + introspectionResponseDTO)); + } + } + + @Test + public void shouldReturnAuthorizationDetails_ifRichAuthorizationRequestAndContextIsPresent() + throws IdentityOAuth2Exception { + + OAuth2TokenValidationMessageContext context = new OAuth2TokenValidationMessageContext(tokenValidationRequestDTO, + new OAuth2TokenValidationResponseDTO()); + + Map properties = new HashMap<>(); + properties.put(OAuth2Util.OAUTH2_VALIDATION_MESSAGE_CONTEXT, context); + this.introspectionResponseDTO.setProperties(properties); + + try (MockedStatic oAuth2UtilMock = Mockito.mockStatic(OAuth2Util.class)) { + + oAuth2UtilMock.when(() -> OAuth2Util.buildScopeArray(any())).thenReturn(new String[0]); + assertAuthorizationDetailsPresent(uut.getIntrospectionData(tokenValidationRequestDTO, + introspectionResponseDTO)); + } + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/JWTAccessTokenRARClaimProviderTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/JWTAccessTokenRARClaimProviderTest.java new file mode 100644 index 00000000000..b1c66b14f83 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/token/JWTAccessTokenRARClaimProviderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025, 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.token; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeReqDTO; +import org.wso2.carbon.identity.oauth2.rar.utils.AuthorizationDetailsBaseTest; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; + +public class JWTAccessTokenRARClaimProviderTest extends AuthorizationDetailsBaseTest { + + private JWTAccessTokenRARClaimProvider uut; + + @BeforeClass + public void setUp() { + this.uut = new JWTAccessTokenRARClaimProvider(); + } + + @Test + public void shouldReturnEmptyForAuthzReq_ifNotRichAuthorizationRequest() throws IdentityOAuth2Exception { + + OAuthAuthzReqMessageContext messageContext = new OAuthAuthzReqMessageContext(new OAuth2AuthorizeReqDTO()); + assertAuthorizationDetailsMissing(uut.getAdditionalClaims(messageContext)); + } + + @Test + public void shouldReturnEmptyForTokenReq_ifNotRichAuthorizationRequest() throws IdentityOAuth2Exception { + + OAuthTokenReqMessageContext messageContext = new OAuthTokenReqMessageContext(new OAuth2AccessTokenReqDTO()); + + assertAuthorizationDetailsMissing(uut.getAdditionalClaims(messageContext)); + } + + @Test + public void shouldReturnAuthorizationDetailsForAuthzReq_ifNotRichAuthorizationRequest() + throws IdentityOAuth2Exception { + + assertAuthorizationDetailsPresent(uut.getAdditionalClaims(authzReqMessageContext)); + } + + @Test + public void shouldReturnAuthorizationDetailsForTokenReq_ifNotRichAuthorizationRequest() + throws IdentityOAuth2Exception { + + assertAuthorizationDetailsPresent(uut.getAdditionalClaims(tokenReqMessageContext)); + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/utils/AuthorizationDetailsBaseTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/utils/AuthorizationDetailsBaseTest.java new file mode 100644 index 00000000000..66862885162 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/utils/AuthorizationDetailsBaseTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025, 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.utils; + +import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedUser; +import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AuthorizeReqDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuth2IntrospectionResponseDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationRequestDTO; +import org.wso2.carbon.identity.oauth2.model.AccessTokenDO; +import org.wso2.carbon.identity.oauth2.model.OAuth2Parameters; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsSchemaValidator; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessor; +import org.wso2.carbon.identity.oauth2.rar.core.AuthorizationDetailsProcessorFactory; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetail; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; +import org.wso2.carbon.identity.oauth2.rar.util.AuthorizationDetailsConstants; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import static org.wso2.carbon.identity.oauth2.TestConstants.ACESS_TOKEN_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.CLIENT_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.TENANT_DOMAIN; +import static org.wso2.carbon.identity.oauth2.TestConstants.TENANT_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.TEST_TYPE; +import static org.wso2.carbon.identity.oauth2.TestConstants.TEST_USER_ID; + +public class AuthorizationDetailsBaseTest { + + protected AuthorizationDetail authorizationDetail; + protected AuthorizationDetails authorizationDetails; + protected OAuthAuthzReqMessageContext authzReqMessageContext; + protected OAuthTokenReqMessageContext tokenReqMessageContext; + protected OAuth2TokenValidationRequestDTO tokenValidationRequestDTO; + protected OAuth2IntrospectionResponseDTO introspectionResponseDTO; + protected AuthenticatedUser authenticatedUser; + protected OAuth2Parameters oAuth2Parameters; + protected AccessTokenDO accessTokenDO; + protected OAuth2AccessTokenReqDTO accessTokenReqDTO; + + protected AuthorizationDetailsProcessorFactory processorFactoryMock; + protected AuthorizationDetailsService serviceMock; + + protected AuthorizationDetailsSchemaValidator schemaValidatorMock; + + public AuthorizationDetailsBaseTest() { + + this.authorizationDetail = new AuthorizationDetail(); + this.authorizationDetail.setType(TEST_TYPE); + + this.authorizationDetails = new AuthorizationDetails(Collections.singleton(this.authorizationDetail)); + + final OAuth2AuthorizeReqDTO authorizeReqDTO = new OAuth2AuthorizeReqDTO(); + authorizeReqDTO.setConsumerKey(CLIENT_ID); + authorizeReqDTO.setTenantDomain(TENANT_DOMAIN); + authorizeReqDTO.setAuthorizationDetails(this.authorizationDetails); + + this.authzReqMessageContext = new OAuthAuthzReqMessageContext(authorizeReqDTO); + this.authzReqMessageContext.setRequestedAuthorizationDetails(this.authorizationDetails); + this.authzReqMessageContext.setApprovedAuthorizationDetails(this.authorizationDetails); + + this.accessTokenReqDTO = new OAuth2AccessTokenReqDTO(); + this.accessTokenReqDTO.setAuthorizationDetails(authorizationDetails); + + this.tokenReqMessageContext = new OAuthTokenReqMessageContext(this.accessTokenReqDTO); + this.tokenReqMessageContext.setAuthorizationDetails(this.authorizationDetails); + + this.tokenValidationRequestDTO = new OAuth2TokenValidationRequestDTO(); + OAuth2TokenValidationRequestDTO.OAuth2AccessToken accessToken = + this.tokenValidationRequestDTO.new OAuth2AccessToken(); + accessToken.setIdentifier(ACESS_TOKEN_ID); + this.tokenValidationRequestDTO.setAccessToken(accessToken); + + this.introspectionResponseDTO = new OAuth2IntrospectionResponseDTO(); + + this.authenticatedUser = new AuthenticatedUser(); + this.authenticatedUser.setUserId(TEST_USER_ID); + + this.oAuth2Parameters = new OAuth2Parameters(); + this.oAuth2Parameters.setTenantDomain(TENANT_DOMAIN); + this.oAuth2Parameters.setAuthorizationDetails(this.authorizationDetails); + this.oAuth2Parameters.setClientId(CLIENT_ID); + + this.accessTokenDO = new AccessTokenDO(); + this.accessTokenDO.setTokenId(ACESS_TOKEN_ID); + this.accessTokenDO.setTenantID(TENANT_ID); + + mockAuthorizationDetailsProcessorFactory(); + this.serviceMock = mock(AuthorizationDetailsService.class); + + this.schemaValidatorMock = spy(AuthorizationDetailsSchemaValidator.class); + } + + public static void assertAuthorizationDetailsPresent(final Map attributes) { + + assertTrue(attributes.containsKey(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS)); + assertEquals(((Set) + attributes.get(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS)).size(), 1); + } + + public static void assertAuthorizationDetailsMissing(final Map attributes) { + + assertFalse(attributes.containsKey(AuthorizationDetailsConstants.AUTHORIZATION_DETAILS)); + } + + private void mockAuthorizationDetailsProcessorFactory() { + + this.processorFactoryMock = spy(AuthorizationDetailsProcessorFactory.class); + try { + Field privateField = AuthorizationDetailsProcessorFactory.class + .getDeclaredField("authorizationDetailsProcessors"); + privateField.setAccessible(true); + + privateField.set(this.processorFactoryMock, new HashMap() {{ + put(TEST_TYPE, getAuthorizationDetailsProcessorMock()); + }}); + } catch (Exception e) { + // ignores the exceptions + } + } + + private AuthorizationDetailsProcessor getAuthorizationDetailsProcessorMock() { + final AuthorizationDetailsProcessor processorMock = mock(AuthorizationDetailsProcessor.class); + when(processorMock.getType()).thenReturn(TEST_TYPE); + when(processorMock.isEqualOrSubset(any(AuthorizationDetail.class), any(AuthorizationDetails.class))) + .thenAnswer(invocation -> { + AuthorizationDetail ad = invocation.getArgument(0, AuthorizationDetail.class); + AuthorizationDetails ads = invocation.getArgument(1, AuthorizationDetails.class); + + return ads.stream().map(AuthorizationDetail::getType).allMatch(type -> type.equals(ad.getType())); + }); + return processorMock; + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/validator/DefaultAuthorizationDetailsValidatorTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/validator/DefaultAuthorizationDetailsValidatorTest.java new file mode 100644 index 00000000000..9c13fe44613 --- /dev/null +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/rar/validator/DefaultAuthorizationDetailsValidatorTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025, 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.validator; + +import org.apache.oltu.oauth2.common.message.types.GrantType; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2ServerException; +import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.exception.AuthorizationDetailsProcessingException; +import org.wso2.carbon.identity.oauth2.rar.utils.AuthorizationDetailsBaseTest; +import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.wso2.carbon.identity.oauth2.TestConstants.ACESS_TOKEN_ID; +import static org.wso2.carbon.identity.oauth2.TestConstants.TENANT_ID; + +public class DefaultAuthorizationDetailsValidatorTest extends AuthorizationDetailsBaseTest { + + AuthorizationDetailsValidator uut; + + @BeforeClass + public void setUp() throws IdentityOAuth2Exception { + this.serviceMock = mock(AuthorizationDetailsService.class); + when(this.serviceMock.getAccessTokenAuthorizationDetails(ACESS_TOKEN_ID, TENANT_ID)) + .thenReturn(authorizationDetails); + + this.uut = new DefaultAuthorizationDetailsValidator(processorFactoryMock, serviceMock, schemaValidatorMock); + } + + @Test + public void shouldReturnContextAuthorizationDetails_ifGrantTypeIsAuthzCode() + throws IdentityOAuth2ServerException, AuthorizationDetailsProcessingException { + + OAuth2AccessTokenReqDTO reqDTO = new OAuth2AccessTokenReqDTO(); + reqDTO.setGrantType(GrantType.AUTHORIZATION_CODE.toString()); + + OAuthTokenReqMessageContext messageContext = new OAuthTokenReqMessageContext(reqDTO); + messageContext.setAuthorizationDetails(authorizationDetails); + + assertEquals(authorizationDetails, uut.getValidatedAuthorizationDetails(messageContext)); + } + + @Test + public void shouldReturnContextAuthorizationDetails_ifNoNewAuthorizationDetailsRequested() + throws IdentityOAuth2ServerException, AuthorizationDetailsProcessingException { + + OAuthTokenReqMessageContext messageContext = new OAuthTokenReqMessageContext(new OAuth2AccessTokenReqDTO()); + messageContext.setAuthorizationDetails(authorizationDetails); + + assertEquals(authorizationDetails, uut.getValidatedAuthorizationDetails(messageContext)); + } +} diff --git a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java index 5dd37ce3047..737414b8c71 100644 --- a/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java +++ b/components/org.wso2.carbon.identity.oauth/src/test/java/org/wso2/carbon/identity/oauth2/token/handlers/grant/RefreshGrantHandlerTest.java @@ -36,7 +36,10 @@ import org.wso2.carbon.identity.oauth2.dto.OAuth2AccessTokenReqDTO; import org.wso2.carbon.identity.oauth2.internal.OAuth2ServiceComponentHolder; import org.wso2.carbon.identity.oauth2.model.RefreshTokenValidationDataDO; +import org.wso2.carbon.identity.oauth2.rar.AuthorizationDetailsService; +import org.wso2.carbon.identity.oauth2.rar.model.AuthorizationDetails; import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext; +import org.wso2.carbon.identity.oauth2.util.OAuth2Util; import org.wso2.carbon.identity.test.common.testng.utils.MockAuthenticatedUser; import org.wso2.carbon.identity.user.profile.mgt.association.federation.FederatedAssociationManager; import org.wso2.carbon.identity.user.profile.mgt.association.federation.exception.FederatedAssociationManagerClientException; @@ -47,6 +50,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -54,6 +58,7 @@ import static org.mockito.Mockito.when; import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkErrorConstants.ErrorMessages.ERROR_WHILE_CHECKING_ACCOUNT_LOCK_STATUS; import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkErrorConstants.ErrorMessages.ERROR_WHILE_GETTING_USERNAME_ASSOCIATED_WITH_IDP; +import static org.wso2.carbon.identity.oauth2.TestConstants.TENANT_ID; /** * Unit tests for the RefreshGrantHandler class. @@ -66,6 +71,7 @@ public class RefreshGrantHandlerTest { private OAuthServerConfiguration oAuthServerConfiguration; private OAuth2AccessTokenReqDTO oAuth2AccessTokenReqDTO; private OAuth2ServiceComponentHolder oAuth2ServiceComponentHolder; + private AuthorizationDetailsService authorizationDetailsService; @BeforeMethod public void init() { @@ -75,6 +81,7 @@ public void init() { oAuthServerConfiguration = mock(OAuthServerConfiguration.class); oAuth2AccessTokenReqDTO = mock(OAuth2AccessTokenReqDTO.class); oAuth2ServiceComponentHolder = mock(OAuth2ServiceComponentHolder.class); + authorizationDetailsService = mock(AuthorizationDetailsService.class); } @DataProvider(name = "validateGrantWhenUserIsLockedInUserStoreEnd") @@ -143,6 +150,10 @@ public void testValidateGrantWhenUserIsLockedInUserStoreEnd(AuthenticatedUser us isValidateAuthenticatedUserForRefreshGrant); when(oAuth2ServiceComponentHolder.getRefreshTokenGrantProcessor()).thenReturn(refreshTokenGrantProcessor); when(oAuthTokenReqMessageContext.getOauth2AccessTokenReqDTO()).thenReturn(oAuth2AccessTokenReqDTO); + when(authorizationDetailsService + .getUserConsentedAuthorizationDetails(any(AuthenticatedUser.class), anyString(), anyInt())) + .thenReturn(new AuthorizationDetails()); + when(oAuth2ServiceComponentHolder.getAuthorizationDetailsService()).thenReturn(authorizationDetailsService); FederatedAssociationManager federatedAssociationManager = mock(FederatedAssociationManager.class); if (federatedAssociationManagerException instanceof FederatedAssociationManagerException) { @@ -169,7 +180,9 @@ public void testValidateGrantWhenUserIsLockedInUserStoreEnd(AuthenticatedUser us OAuthServerConfiguration.class); MockedStatic oAuth2ServiceComponentHolderMockedStatic = mockStatic( OAuth2ServiceComponentHolder.class); - MockedStatic frameworkUtilsMockedStatic = mockStatic(FrameworkUtils.class)) { + MockedStatic frameworkUtilsMockedStatic = mockStatic(FrameworkUtils.class); + MockedStatic oAuth2Util = mockStatic(OAuth2Util.class)) { + oAuth2Util.when(() -> OAuth2Util.getTenantId(anyString())).thenReturn(TENANT_ID); oAuthServerConfigurationMockedStatic.when(OAuthServerConfiguration::getInstance) .thenReturn(oAuthServerConfiguration); oAuth2ServiceComponentHolderMockedStatic.when(OAuth2ServiceComponentHolder::getInstance) @@ -185,6 +198,7 @@ public void testValidateGrantWhenUserIsLockedInUserStoreEnd(AuthenticatedUser us } RefreshGrantHandler refreshGrantHandler = new RefreshGrantHandler(); + refreshGrantHandler.init(); boolean validateResult = refreshGrantHandler.validateGrant(oAuthTokenReqMessageContext); assertTrue(validateResult); } diff --git a/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml index a31ffda9146..f1a5b35f9ee 100755 --- a/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.oauth/src/test/resources/testng.xml @@ -125,6 +125,11 @@ + + + + + @@ -202,6 +207,11 @@ + + + + + diff --git a/pom.xml b/pom.xml index 02e8da8ae2f..a7cc63d959a 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ components/org.wso2.carbon.identity.oauth.client.authn.filter components/org.wso2.carbon.identity.oauth.ciba components/org.wso2.carbon.identity.client.attestation.filter + components/org.wso2.carbon.identity.oauth.rar features/org.wso2.carbon.identity.oauth.common.feature features/org.wso2.carbon.identity.oauth.feature features/org.wso2.carbon.identity.oauth.server.feature @@ -409,6 +410,12 @@ gson ${com.google.code.gson.version}
+ + + io.vertx + vertx-json-schema + ${vertx.json.schema.version} + @@ -554,6 +561,12 @@ org.wso2.carbon.identity.oauth.extension ${project.version} + + org.wso2.carbon.identity.inbound.auth.oauth2 + org.wso2.carbon.identity.oauth.rar + provided + ${project.version} + @@ -944,7 +957,7 @@ [1.0.1, 2.0.0) - 7.7.49 + 7.7.90 [5.25.234, 8.0.0) [2.0.0, 3.0.0) @@ -1042,6 +1055,7 @@ 2.4.7 5.2 9.2 + 4.5.11 5.1.2