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 7c741f71e2e..014cbffab9c 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 @@ -2498,6 +2498,14 @@ private void handleOIDCRequestObject(OAuthMessage oAuthMessage, OAuthAuthzReques } else if (isRequestParameter(oauthRequest)) { requestObjValue = oauthRequest.getParam(REQUEST); } + /* Mandate request object for FAPI requests. + https://openid.net/specs/openid-financial-api-part-2-1_0.html#authorization-server (5.2.2-1) */ + if (isFapiConformant(oAuthMessage.getClientId())) { + if (requestObjValue == null) { + throw new InvalidRequestException("Request Object is mandatory for FAPI Conformant Applications.", + OAuth2ErrorCodes.INVALID_REQUEST, "Request object is missing."); + } + } if (StringUtils.isNotEmpty(requestObjValue)) { handleRequestObject(oAuthMessage, oauthRequest, parameters); @@ -2543,8 +2551,10 @@ private void handleRequestObject(OAuthMessage oAuthMessage, OAuthAuthzRequest oa When the request parameter is used, the OpenID Connect request parameter values contained in the JWT supersede those passed using the OAuth 2.0 request syntax */ + boolean isFapiConformant = isFapiConformant(oAuthMessage.getClientId()); + // If FAPI conformant, claims outside request object should be ignored. overrideAuthzParameters(oAuthMessage, parameters, oauthRequest.getParam(REQUEST), - oauthRequest.getParam(REQUEST_URI), requestObject); + oauthRequest.getParam(REQUEST_URI), requestObject, isFapiConformant); // If the redirect uri was not given in auth request the registered redirect uri will be available here, // so validating if the registered redirect uri is a single uri that can be properly redirected. @@ -2567,17 +2577,18 @@ private void handleRequestObject(OAuthMessage oAuthMessage, OAuthAuthzRequest oa private void overrideAuthzParameters(OAuthMessage oAuthMessage, OAuth2Parameters params, String requestParameterValue, - String requestURIParameterValue, RequestObject requestObject) { + String requestURIParameterValue, RequestObject requestObject, + boolean ignoreClaimsOutsideRequestObject) { if (StringUtils.isNotBlank(requestParameterValue) || StringUtils.isNotBlank(requestURIParameterValue)) { - replaceIfPresent(requestObject, REDIRECT_URI, params::setRedirectURI); - replaceIfPresent(requestObject, NONCE, params::setNonce); - replaceIfPresent(requestObject, STATE, params::setState); - replaceIfPresent(requestObject, DISPLAY, params::setDisplay); - replaceIfPresent(requestObject, RESPONSE_MODE, params::setResponseMode); - replaceIfPresent(requestObject, LOGIN_HINT, params::setLoginHint); - replaceIfPresent(requestObject, ID_TOKEN_HINT, params::setIDTokenHint); - replaceIfPresent(requestObject, PROMPT, params::setPrompt); + replaceIfPresent(requestObject, REDIRECT_URI, params::setRedirectURI, ignoreClaimsOutsideRequestObject); + replaceIfPresent(requestObject, NONCE, params::setNonce, ignoreClaimsOutsideRequestObject); + replaceIfPresent(requestObject, STATE, params::setState, ignoreClaimsOutsideRequestObject); + replaceIfPresent(requestObject, DISPLAY, params::setDisplay, ignoreClaimsOutsideRequestObject); + replaceIfPresent(requestObject, RESPONSE_MODE, params::setResponseMode, ignoreClaimsOutsideRequestObject); + replaceIfPresent(requestObject, LOGIN_HINT, params::setLoginHint, ignoreClaimsOutsideRequestObject); + replaceIfPresent(requestObject, ID_TOKEN_HINT, params::setIDTokenHint, ignoreClaimsOutsideRequestObject); + replaceIfPresent(requestObject, PROMPT, params::setPrompt, ignoreClaimsOutsideRequestObject); if (requestObject.getClaim(CLAIMS) instanceof net.minidev.json.JSONObject) { // Claims in the request object is in the type of net.minidev.json.JSONObject, @@ -2589,8 +2600,8 @@ private void overrideAuthzParameters(OAuthMessage oAuthMessage, OAuth2Parameters if (isPkceSupportEnabled()) { // If code_challenge and code_challenge_method is sent inside the request object then add them to // Oauth2 parameters. - replaceIfPresent(requestObject, CODE_CHALLENGE, params::setPkceCodeChallenge); - replaceIfPresent(requestObject, CODE_CHALLENGE_METHOD, params::setPkceCodeChallengeMethod); + replaceIfPresent(requestObject, CODE_CHALLENGE, params::setPkceCodeChallenge, false); + replaceIfPresent(requestObject, CODE_CHALLENGE_METHOD, params::setPkceCodeChallengeMethod, false); } if (StringUtils.isNotEmpty(requestObject.getClaimValue(SCOPE))) { @@ -2654,11 +2665,14 @@ private List getAcrValues(RequestObject requestObject) { return acrRequestedValues; } - private void replaceIfPresent(RequestObject requestObject, String claim, Consumer consumer) { + private void replaceIfPresent(RequestObject requestObject, String claim, Consumer consumer, + boolean ignoreClaimsOutsideRequestObject) { String claimValue = requestObject.getClaimValue(claim); if (StringUtils.isNotEmpty(claimValue)) { consumer.accept(claimValue); + } else if (ignoreClaimsOutsideRequestObject) { + consumer.accept(null); } } @@ -4211,4 +4225,13 @@ private void addUserAttributesToCache(SessionDataCacheEntry sessionDataCacheEntr new DeviceAuthorizationGrantCacheEntry(sessionDataCacheEntry.getLoggedInUser().getUserAttributes()); DeviceAuthorizationGrantCache.getInstance().addToCache(cacheKey, cacheEntry); } + + private boolean isFapiConformant(String clientId) throws InvalidRequestException { + + try { + return OAuth2Util.isFapiConformantApp(clientId); + } catch (IdentityOAuth2Exception e) { + throw new InvalidRequestException(e.getMessage(), e.getErrorCode()); + } + } } diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/par/OAuth2ParEndpoint.java b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/par/OAuth2ParEndpoint.java index da291f8cbf0..0e1d83e1ce7 100644 --- a/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/par/OAuth2ParEndpoint.java +++ b/components/org.wso2.carbon.identity.oauth.endpoint/src/main/java/org/wso2/carbon/identity/oauth/endpoint/par/OAuth2ParEndpoint.java @@ -35,6 +35,7 @@ import org.wso2.carbon.identity.oauth.par.exceptions.ParClientException; import org.wso2.carbon.identity.oauth.par.exceptions.ParCoreException; import org.wso2.carbon.identity.oauth.par.model.ParAuthData; +import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; import org.wso2.carbon.identity.oauth2.RequestObjectException; import org.wso2.carbon.identity.oauth2.bean.OAuthClientAuthnContext; import org.wso2.carbon.identity.oauth2.dto.OAuth2ClientValidationResponseDTO; @@ -255,18 +256,25 @@ private void validateInputParameters(HttpServletRequest request) throws ParClien private void validateRequestObject(OAuthAuthzRequest oAuthAuthzRequest) throws ParCoreException { try { - if (OAuth2Util.isOIDCAuthzRequest(oAuthAuthzRequest.getScopes()) && - StringUtils.isNotBlank(oAuthAuthzRequest.getParam(REQUEST))) { - - OAuth2Parameters parameters = new OAuth2Parameters(); - parameters.setClientId(oAuthAuthzRequest.getClientId()); - parameters.setRedirectURI(oAuthAuthzRequest.getRedirectURI()); - parameters.setResponseType(oAuthAuthzRequest.getResponseType()); - parameters.setTenantDomain(getSPTenantDomainFromClientId(oAuthAuthzRequest.getClientId())); - - RequestObject requestObject = OIDCRequestObjectUtil.buildRequestObject(oAuthAuthzRequest, parameters); - if (requestObject == null) { - throw new ParClientException(OAuth2ErrorCodes.INVALID_REQUEST, ParConstants.INVALID_REQUEST_OBJECT); + if (OAuth2Util.isOIDCAuthzRequest(oAuthAuthzRequest.getScopes())) { + if (StringUtils.isNotBlank(oAuthAuthzRequest.getParam(REQUEST))) { + + OAuth2Parameters parameters = new OAuth2Parameters(); + parameters.setClientId(oAuthAuthzRequest.getClientId()); + parameters.setRedirectURI(oAuthAuthzRequest.getRedirectURI()); + parameters.setResponseType(oAuthAuthzRequest.getResponseType()); + parameters.setTenantDomain(getSPTenantDomainFromClientId(oAuthAuthzRequest.getClientId())); + + RequestObject requestObject = + OIDCRequestObjectUtil.buildRequestObject(oAuthAuthzRequest, parameters); + if (requestObject == null) { + throw new ParClientException(OAuth2ErrorCodes.INVALID_REQUEST, + ParConstants.INVALID_REQUEST_OBJECT); + } + } else if (isFapiConformant(oAuthAuthzRequest.getClientId())) { + /* Mandate request object for FAPI requests + https://openid.net/specs/openid-financial-api-part-2-1_0.html#authorization-server (5.2.2-1) */ + throw new ParClientException(OAuth2ErrorCodes.INVALID_REQUEST, ParConstants.REQUEST_OBJECT_MISSING); } } } catch (RequestObjectException e) { @@ -276,4 +284,13 @@ private void validateRequestObject(OAuthAuthzRequest oAuthAuthzRequest) throws P throw new ParClientException(e.getErrorCode(), e.getMessage(), e); } } + + private boolean isFapiConformant(String clientId) throws ParClientException { + + try { + return OAuth2Util.isFapiConformantApp(clientId); + } catch (IdentityOAuth2Exception e) { + throw new ParClientException(e.getMessage(), e.getErrorCode()); + } + } } 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 9bf9c1ad3e1..48c5a7bfd65 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 @@ -91,6 +91,7 @@ import org.wso2.carbon.identity.oauth2.OAuth2Service; import org.wso2.carbon.identity.oauth2.OAuth2TokenValidationService; import org.wso2.carbon.identity.oauth2.Oauth2ScopeConstants; +import org.wso2.carbon.identity.oauth2.RequestObjectException; import org.wso2.carbon.identity.oauth2.bean.OAuthClientAuthnContext; import org.wso2.carbon.identity.oauth2.bean.Scope; import org.wso2.carbon.identity.oauth2.dto.OAuth2ClientValidationResponseDTO; @@ -100,7 +101,11 @@ import org.wso2.carbon.identity.oauth2.scopeservice.OAuth2Resource; import org.wso2.carbon.identity.oauth2.scopeservice.ScopeMetadataService; import org.wso2.carbon.identity.oauth2.util.OAuth2Util; +import org.wso2.carbon.identity.openidconnect.OIDCRequestObjectUtil; +import org.wso2.carbon.identity.openidconnect.RequestObjectBuilder; import org.wso2.carbon.identity.openidconnect.RequestObjectService; +import org.wso2.carbon.identity.openidconnect.RequestObjectValidator; +import org.wso2.carbon.identity.openidconnect.model.RequestObject; import org.wso2.carbon.identity.webfinger.DefaultWebFingerProcessor; import org.wso2.carbon.identity.webfinger.WebFingerProcessor; import org.wso2.carbon.idp.mgt.IdentityProviderManagementException; @@ -1719,7 +1724,18 @@ public static void setParAuthService(ParAuthService parAuthService) { public static String retrieveStateForErrorURL(HttpServletRequest request, OAuth2Parameters oAuth2Parameters) { String state = null; - if (oAuth2Parameters != null && oAuth2Parameters.getState() != null) { + + if (request.getParameter(OAuthConstants.OAuth20Params.REQUEST) != null) { + String stateInsideRequestObj = getStateFromRequestObject(request, oAuth2Parameters); + if (StringUtils.isNotBlank(stateInsideRequestObj)) { + state = stateInsideRequestObj; + if (log.isDebugEnabled()) { + log.debug("Retrieved state value " + state + " from request object."); + } + } + } + + if (StringUtils.isBlank(state) && oAuth2Parameters != null && oAuth2Parameters.getState() != null) { state = oAuth2Parameters.getState(); if (log.isDebugEnabled()) { log.debug("Retrieved state value " + state + " from OAuth2Parameters."); @@ -1734,6 +1750,35 @@ public static String retrieveStateForErrorURL(HttpServletRequest request, OAuth2 return state; } + private static String getStateFromRequestObject(HttpServletRequest request, OAuth2Parameters oAuth2Parameters) { + + try { + RequestObjectValidator requestObjectValidator = OAuthServerConfiguration.getInstance() + .getRequestObjectValidator(); + RequestObjectBuilder requestObjectBuilder = OAuthServerConfiguration.getInstance() + .getRequestObjectBuilders().get(OIDCRequestObjectUtil.REQUEST_PARAM_VALUE_BUILDER); + RequestObject requestObject = + requestObjectBuilder.buildRequestObject(request.getParameter(OAuthConstants.OAuth20Params.REQUEST), + oAuth2Parameters); + if (StringUtils.isBlank(oAuth2Parameters.getClientId())) { + // Set client id and tenant domain required for signature validation if not already set. + String clientId = request.getParameter(PROP_CLIENT_ID); + oAuth2Parameters.setClientId(clientId); + oAuth2Parameters.setTenantDomain(getSPTenantDomainFromClientId(clientId)); + } + // Validate request object signature to ensure request object is not tampered. + OIDCRequestObjectUtil.validateRequestObjectSignature(oAuth2Parameters, requestObject, + requestObjectValidator); + return requestObject.getClaimValue(OAuthConstants.OAuth20Params.STATE); + } catch (RequestObjectException e) { + /* If request object signature validation fails, logs and return null from this method and the state value + will be overridden from oauth2 parameters or request parameters if present inside the + retrieveStateForErrorURL method. */ + log.debug("Error while retrieving state from request object.", e); + } + return null; + } + /** * Return updated redirect URL. * 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 d9c7f45947c..8b95fe21ba2 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 @@ -17,12 +17,20 @@ */ package org.wso2.carbon.identity.oauth.endpoint.authz; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton; import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.PlainJWT; import com.nimbusds.jwt.SignedJWT; import org.apache.axis2.transport.http.HTTPConstants; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.SerializationUtils; import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest; import org.apache.oltu.oauth2.as.validator.CodeValidator; import org.apache.oltu.oauth2.as.validator.TokenValidator; @@ -40,6 +48,7 @@ import org.testng.Assert; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.wso2.carbon.base.CarbonBaseConstants; @@ -67,6 +76,7 @@ import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils; import org.wso2.carbon.identity.claim.metadata.mgt.ClaimMetadataHandler; import org.wso2.carbon.identity.claim.metadata.mgt.model.ExternalClaim; +import org.wso2.carbon.identity.common.testng.TestConstants; import org.wso2.carbon.identity.core.ServiceURL; import org.wso2.carbon.identity.core.ServiceURLBuilder; import org.wso2.carbon.identity.core.URLBuilderException; @@ -94,6 +104,7 @@ import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception; import org.wso2.carbon.identity.oauth2.OAuth2ScopeService; import org.wso2.carbon.identity.oauth2.OAuth2Service; +import org.wso2.carbon.identity.oauth2.RequestObjectException; import org.wso2.carbon.identity.oauth2.authz.AuthorizationHandlerManager; import org.wso2.carbon.identity.oauth2.authz.OAuthAuthzReqMessageContext; import org.wso2.carbon.identity.oauth2.device.api.DeviceAuthService; @@ -118,22 +129,33 @@ import org.wso2.carbon.identity.openidconnect.DefaultOIDCClaimsCallbackHandler; import org.wso2.carbon.identity.openidconnect.OIDCConstants; import org.wso2.carbon.identity.openidconnect.OpenIDConnectClaimFilterImpl; +import org.wso2.carbon.identity.openidconnect.RequestObjectBuilder; import org.wso2.carbon.identity.openidconnect.RequestObjectService; +import org.wso2.carbon.identity.openidconnect.RequestObjectValidator; +import org.wso2.carbon.identity.openidconnect.RequestObjectValidatorImpl; +import org.wso2.carbon.identity.openidconnect.RequestParamRequestObjectBuilder; import org.wso2.carbon.identity.openidconnect.model.RequestObject; import org.wso2.carbon.identity.openidconnect.model.RequestedClaim; import org.wso2.carbon.utils.CarbonUtils; +import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.file.Path; import java.nio.file.Paths; +import java.security.Key; +import java.security.KeyStore; +import java.security.interfaces.RSAPrivateKey; import java.sql.Connection; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; @@ -182,6 +204,9 @@ import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.testng.FileAssert.fail; +import static org.wso2.carbon.identity.oauth2.util.OAuth2Util.EXP; +import static org.wso2.carbon.identity.oauth2.util.OAuth2Util.NBF; +import static org.wso2.carbon.identity.openidconnect.OIDCRequestObjectUtil.REQUEST_PARAM_VALUE_BUILDER; @PrepareForTest({OAuth2Util.class, SessionDataCache.class, OAuthServerConfiguration.class, IdentityDatabaseUtil.class, EndpointUtil.class, FrameworkUtils.class, EndpointUtil.class, OpenIDConnectUserRPStore.class, SignedJWT.class, @@ -304,12 +329,16 @@ public class OAuth2AuthzEndpointTest extends TestOAuthEndpointBase { private static final String SP_NAME = "Name"; private static final String STATE = "JEZGpTb8IF"; private static final String OIDC_DIALECT = "http://wso2.org/oidc/claim"; + private static final int MILLISECONDS_PER_SECOND = 1000; + private static final int TIME_MARGIN_IN_SECONDS = 3000; private OAuth2AuthzEndpoint oAuth2AuthzEndpoint; private Object authzEndpointObject; private OAuth2ScopeConsentResponse oAuth2ScopeConsentResponse; private ServiceProvider dummySp; + private KeyStore clientKeyStore; + @BeforeClass public void setUp() throws Exception { @@ -1994,6 +2023,162 @@ public Object answer(InvocationOnMock invocation) { assertEquals(cacheEntry[0].getoAuth2Parameters().getDisplayName(), savedDisplayName); } + @BeforeMethod + public void setupKeystore() throws Exception { + + clientKeyStore = getKeyStoreFromFile("testkeystore.jks", "wso2carbon", + System.getProperty(CarbonBaseConstants.CARBON_HOME)); + } + + @DataProvider(name = "provideHandleRequestObjectData") + public Object[][] provideHandleRequestObjectData() throws Exception { + + OAuth2Parameters oAuth2Parameters = new OAuth2Parameters(); + oAuth2Parameters.setClientId(TestConstants.CLIENT_ID); + oAuth2Parameters.setRedirectURI(TestConstants.CALLBACK); + oAuth2Parameters.setTenantDomain(TestConstants.TENANT_DOMAIN); + oAuth2Parameters.setNonce("nonceInParams"); + oAuth2Parameters.setState("stateInParams"); + oAuth2Parameters.setPrompt("promptInParams"); + + Map defaultClaims = new HashMap<>(); + defaultClaims.put(OAuthConstants.OAuth20Params.REDIRECT_URI, TestConstants.CALLBACK); + defaultClaims.put(NBF, System.currentTimeMillis() / MILLISECONDS_PER_SECOND); + defaultClaims.put(EXP, System.currentTimeMillis() / MILLISECONDS_PER_SECOND + TIME_MARGIN_IN_SECONDS); + defaultClaims.put(OAuthConstants.OAuth20Params.SCOPE, TestConstants.SCOPE_STRING); + + Map claims1 = new HashMap<>(defaultClaims); + claims1.put(OAuthConstants.STATE, "stateInRequestObject"); + claims1.put(OAuthConstants.OAuth20Params.NONCE, "nonceInRequestObject"); + claims1.put(OAuthConstants.OAuth20Params.PROMPT, "promptInRequestObject"); + + return new Object[][]{ + {true, SerializationUtils.clone(oAuth2Parameters), claims1, + "Test override claims from request object."}, + {true, SerializationUtils.clone(oAuth2Parameters), defaultClaims, + "Test ignore claims outside request object."}, // No overridable claims sent in the req obj. + {false, SerializationUtils.clone(oAuth2Parameters), defaultClaims, + "Test request without request object."} + }; + } + + @Test(dataProvider = "provideHandleRequestObjectData") + public void testHandleOIDCRequestObjectForFAPI(boolean withRequestObject, Object oAuth2ParametersObj, + Map claims, + String testName) throws Exception { + + OAuth2Parameters oAuth2Parameters = (OAuth2Parameters) oAuth2ParametersObj; + OAuth2Parameters originalOAuth2Parameters = SerializationUtils.clone(oAuth2Parameters); + + Key privateKey = clientKeyStore.getKey("wso2carbon", "wso2carbon".toCharArray()); + + if (withRequestObject) { + String jsonWebToken = + buildJWTWithExpiry(oAuth2Parameters.getClientId(), oAuth2Parameters.getClientId(), "1000", + "audience", + JWSAlgorithm.PS256.getName(), + privateKey, 0, claims, 3600 * 1000); + when(oAuthAuthzRequest.getParam(OAuthConstants.OAuth20Params.REQUEST)).thenReturn(jsonWebToken); + } + + Map requestObjectBuilderMap = new HashMap<>(); + requestObjectBuilderMap.put(REQUEST_PARAM_VALUE_BUILDER, new RequestParamRequestObjectBuilder()); + RequestObjectValidator requestObjectValidator = PowerMockito.spy(new RequestObjectValidatorImpl()); + doReturn(true).when(requestObjectValidator, "validateSignature", any(), any()); + doReturn(true).when(requestObjectValidator, "isValidAudience", any(), any()); + mockOAuthServerConfiguration(); + when((oAuthServerConfiguration.getRequestObjectBuilders())).thenReturn(requestObjectBuilderMap); + when((oAuthServerConfiguration.getRequestObjectValidator())).thenReturn(requestObjectValidator); + mockStatic(LoggerUtils.class); + when(LoggerUtils.isDiagnosticLogsEnabled()).thenReturn(false); + + OAuthAppDO appDO = new OAuthAppDO(); + appDO.setRequestObjectSignatureValidationEnabled(false); + spy(OAuth2Util.class); + doReturn(appDO).when(OAuth2Util.class, "getAppInformationByClientId", oAuth2Parameters.getClientId()); + doReturn(true).when(OAuth2Util.class, "isFapiConformantApp", any()); + mockEndpointUtil(false); + when(oAuth2Service.isPKCESupportEnabled()).thenReturn(false); + + Assert.assertEquals(oAuth2Parameters.getNonce(), originalOAuth2Parameters.getNonce()); + Assert.assertEquals(oAuth2Parameters.getState(), originalOAuth2Parameters.getState()); + Assert.assertEquals(oAuth2Parameters.getPrompt(), originalOAuth2Parameters.getPrompt()); + + Method handleOIDCRequestObject = authzEndpointObject.getClass().getDeclaredMethod( + "handleOIDCRequestObject", OAuthMessage.class, OAuthAuthzRequest.class, OAuth2Parameters.class); + handleOIDCRequestObject.setAccessible(true); + try { + handleOIDCRequestObject.invoke(authzEndpointObject, oAuthMessage, oAuthAuthzRequest, oAuth2Parameters); + Assert.assertEquals(oAuth2Parameters.getNonce(), claims.get(OAuthConstants.OAuth20Params.NONCE), testName); + Assert.assertEquals(oAuth2Parameters.getState(), claims.get(OAuthConstants.OAuth20Params.STATE), testName); + Assert.assertEquals(oAuth2Parameters.getPrompt(), claims.get(OAuthConstants.OAuth20Params.PROMPT), + testName); + } catch (InvocationTargetException e) { + Assert.assertEquals(e.getTargetException().getMessage(), + "Request Object is mandatory for FAPI Conformant Applications.", testName); + } + } + + private static KeyStore getKeyStoreFromFile(String keystoreName, String password, String home) throws Exception { + + Path tenantKeystorePath = Paths.get(home, "repository", "resources", "security", keystoreName); + FileInputStream file = new FileInputStream(tenantKeystorePath.toString()); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(file, password.toCharArray()); + return keystore; + } + + private static String buildJWTWithExpiry(String issuer, String subject, String jti, String audience, String + algorithm, Key privateKey, long notBeforeMillis, Map claims, long lifetimeInMillis) + throws RequestObjectException { + + JWTClaimsSet jwtClaimsSet = getJwtClaimsSet(issuer, subject, jti, audience, notBeforeMillis, claims, + lifetimeInMillis); + if (JWSAlgorithm.NONE.getName().equals(algorithm)) { + return new PlainJWT(jwtClaimsSet).serialize(); + } + + return signJWTWithRSA(jwtClaimsSet, privateKey, JWSAlgorithm.parse(algorithm)); + } + + private static String signJWTWithRSA(JWTClaimsSet jwtClaimsSet, Key privateKey, JWSAlgorithm jwsAlgorithm) + throws RequestObjectException { + + try { + JWSSigner signer = new RSASSASigner((RSAPrivateKey) privateKey); + SignedJWT signedJWT = new SignedJWT(new JWSHeader(jwsAlgorithm), jwtClaimsSet); + signer.getJCAContext().setProvider(BouncyCastleProviderSingleton.getInstance()); + signedJWT.sign(signer); + return signedJWT.serialize(); + } catch (JOSEException e) { + throw new RequestObjectException("error_signing_jwt", "Error occurred while signing JWT."); + } + } + + private static JWTClaimsSet getJwtClaimsSet(String issuer, String subject, String jti, String audience, long + notBeforeMillis, Map claims, long lifetimeInMillis) { + + long curTimeInMillis = Calendar.getInstance().getTimeInMillis(); + // Set claims to jwt token. + JWTClaimsSet.Builder jwtClaimsSetBuilder = new JWTClaimsSet.Builder(); + jwtClaimsSetBuilder.issuer(issuer); + jwtClaimsSetBuilder.subject(subject); + jwtClaimsSetBuilder.audience(Arrays.asList(audience)); + jwtClaimsSetBuilder.jwtID(jti); + jwtClaimsSetBuilder.expirationTime(new Date((curTimeInMillis + lifetimeInMillis))); + jwtClaimsSetBuilder.issueTime(new Date(curTimeInMillis)); + + if (notBeforeMillis > 0) { + jwtClaimsSetBuilder.notBeforeTime(new Date(curTimeInMillis + notBeforeMillis)); + } + if (claims != null && !claims.isEmpty()) { + for (Map.Entry entry : claims.entrySet()) { + jwtClaimsSetBuilder.claim(entry.getKey().toString(), entry.getValue()); + } + } + return jwtClaimsSetBuilder.build(); + } + @Test(dependsOnGroups = "testWithConnection") public void testIdentityOAuthAdminException() throws Exception { 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 b6e8965493a..c69565d4f96 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 @@ -103,6 +103,7 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; @@ -208,6 +209,7 @@ public class EndpointUtilTest extends PowerMockIdentityBaseTest { private static final String REQUESTED_OIDC_SCOPES_KEY = "requested_oidc_scopes="; private static final String REQUESTED_OIDC_SCOPES_VALUES = "openid+profile"; private static final String EXTERNAL_CONSENTED_APP_NAME = "testApp"; + private static final String REDIRECT = "redirect"; private static final String EXTERNAL_CONSENT_URL = "https://localhost:9443/consent"; private String username; private String password; @@ -646,7 +648,7 @@ public void testGetErrorPageURL(boolean isImplicitResponse, boolean isHybridResp when(OAuth2Util.OAuthURL.getOAuth2ErrorPageUrl()).thenReturn(ERROR_PAGE_URL); when(mockedOAuthResponse.getLocationUri()).thenReturn("http://localhost:8080/location"); - when(mockedHttpServletRequest.getParameter(anyString())).thenReturn("http://localhost:8080/location"); + when(mockedHttpServletRequest.getParameter(contains(REDIRECT))).thenReturn("http://localhost:8080/location"); String url = EndpointUtil.getErrorPageURL(mockedHttpServletRequest, "invalid request", "invalid request object", "invalid request", "test", parameters); diff --git a/components/org.wso2.carbon.identity.oauth.endpoint/src/test/resources/repository/resources/security/testkeystore.jks b/components/org.wso2.carbon.identity.oauth.endpoint/src/test/resources/repository/resources/security/testkeystore.jks new file mode 100644 index 00000000000..062f0e33999 Binary files /dev/null and b/components/org.wso2.carbon.identity.oauth.endpoint/src/test/resources/repository/resources/security/testkeystore.jks differ diff --git a/components/org.wso2.carbon.identity.oauth.par/src/main/java/org/wso2/carbon/identity/oauth/par/common/ParConstants.java b/components/org.wso2.carbon.identity.oauth.par/src/main/java/org/wso2/carbon/identity/oauth/par/common/ParConstants.java index cc17604c13f..0fa817d45c1 100644 --- a/components/org.wso2.carbon.identity.oauth.par/src/main/java/org/wso2/carbon/identity/oauth/par/common/ParConstants.java +++ b/components/org.wso2.carbon.identity.oauth.par/src/main/java/org/wso2/carbon/identity/oauth/par/common/ParConstants.java @@ -43,6 +43,7 @@ public class ParConstants { public static final String INVALID_CLIENT_ERROR = "A valid OAuth client could not be found for client_id: "; public static final String INVALID_REQUEST_OBJECT = "Unable to build a valid Request Object from the" + " pushed authorization request."; + public static final String REQUEST_OBJECT_MISSING = "Request object is missing."; private ParConstants() { diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/CarbonOAuthAuthzRequest.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/CarbonOAuthAuthzRequest.java index df12865bc01..793bf4053b2 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/CarbonOAuthAuthzRequest.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/oauth2/model/CarbonOAuthAuthzRequest.java @@ -32,7 +32,6 @@ import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration; import org.wso2.carbon.utils.DiagnosticLog; - import javax.servlet.http.HttpServletRequest; /** diff --git a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/openidconnect/OIDCRequestObjectUtil.java b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/openidconnect/OIDCRequestObjectUtil.java index 1ddac4da221..b6dcdc27e4b 100644 --- a/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/openidconnect/OIDCRequestObjectUtil.java +++ b/components/org.wso2.carbon.identity.oauth/src/main/java/org/wso2/carbon/identity/openidconnect/OIDCRequestObjectUtil.java @@ -45,7 +45,7 @@ public class OIDCRequestObjectUtil { private static final Log log = LogFactory.getLog(OIDCRequestObjectUtil.class); private static final String REQUEST = "request"; private static final String REQUEST_URI = "request_uri"; - private static final String REQUEST_PARAM_VALUE_BUILDER = "request_param_value_builder"; + public static final String REQUEST_PARAM_VALUE_BUILDER = "request_param_value_builder"; private static final String REQUEST_URI_PARAM_VALUE_BUILDER = "request_uri_param_value_builder"; /**