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 extends OAuthAuthzRequest> 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
+
+ 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