Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore claims outside request object #2160

Merged
merged 14 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2399,7 +2399,7 @@ private String populateOauthParameters(OAuth2Parameters params, OAuthMessage oAu
}

if (isPkceSupportEnabled()) {
String pkceChallengeCode = getPkceCodeChallenge(oAuthMessage, params);
String pkceChallengeCode = getPkceCodeChallenge(oAuthMessage, params, validationResponse.isPkceMandatory());
String pkceChallengeMethod = getPkceCodeChallengeMethod(oAuthMessage, params);

String redirectURI = validatePKCEParameters(oAuthMessage, validationResponse, pkceChallengeCode,
Expand Down Expand Up @@ -2479,6 +2479,14 @@ private void handleOIDCRequestObject(OAuthMessage oAuthMessage, OAuthAuthzReques
} else if (isRequestParameter(oauthRequest)) {
requestObjValue = oauthRequest.getParam(REQUEST);
}
// Mandate request object for FAPI requests
RivinduM marked this conversation as resolved.
Show resolved Hide resolved
janakamarasena marked this conversation as resolved.
Show resolved Hide resolved
// 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);
Expand Down Expand Up @@ -2524,8 +2532,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.
Expand All @@ -2548,17 +2558,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,
Expand All @@ -2570,8 +2581,10 @@ 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,
ignoreClaimsOutsideRequestObject);
replaceIfPresent(requestObject, CODE_CHALLENGE_METHOD, params::setPkceCodeChallengeMethod,
ignoreClaimsOutsideRequestObject);
}

if (StringUtils.isNotEmpty(requestObject.getClaimValue(SCOPE))) {
Expand Down Expand Up @@ -2635,11 +2648,14 @@ private List<String> getAcrValues(RequestObject requestObject) {
return acrRequestedValues;
}

private void replaceIfPresent(RequestObject requestObject, String claim, Consumer<String> consumer) {
private void replaceIfPresent(RequestObject requestObject, String claim, Consumer<String> consumer,
boolean ignoreClaimsOutsideRequestObject) {

String claimValue = requestObject.getClaimValue(claim);
if (StringUtils.isNotEmpty(claimValue)) {
consumer.accept(claimValue);
} else if (ignoreClaimsOutsideRequestObject) {
consumer.accept(null);
}
}

Expand Down Expand Up @@ -4067,17 +4083,20 @@ private OAuth2Parameters getOAuth2ParamsFromOAuthMessage(OAuthMessage oAuthMessa
*
* @param oAuthMessage oAuthMessage
* @param params OAuth2 Parameters
* @param isPkcemandatory is PKCE mandatory
* @return PKCE code challenge. Priority will be given to the value inside the OAuth2Parameters.
*/
private String getPkceCodeChallenge(OAuthMessage oAuthMessage, OAuth2Parameters params) {
private String getPkceCodeChallenge(OAuthMessage oAuthMessage, OAuth2Parameters params, boolean isPkcemandatory)
throws InvalidRequestException {

String pkceChallengeCode;
String pkceChallengeCode = null;
boolean isFapiConformantApp = isFapiConformant(params.getClientId());
// If the code_challenge is in the request object, then it is added to Oauth2 params before this point.
if (params.getPkceCodeChallenge() != null) {
// If Oauth2 params contains code_challenge get value from Oauth2 params.
pkceChallengeCode = params.getPkceCodeChallenge();
} else {
// Else retrieve from request query params.
} else if (!isFapiConformantApp || isPkcemandatory) {
// Else retrieve from request query params if application is not FAPI compliant or PKCE is mandatory.
pkceChallengeCode = oAuthMessage.getOauthPKCECodeChallenge();
}

Expand All @@ -4093,15 +4112,17 @@ private String getPkceCodeChallenge(OAuthMessage oAuthMessage, OAuth2Parameters
* @param params OAuth2 Parameters
* @return PKCE code challenge method. Priority will be given to the value inside the OAuth2Parameters.
*/
private String getPkceCodeChallengeMethod(OAuthMessage oAuthMessage, OAuth2Parameters params) {
private String getPkceCodeChallengeMethod(OAuthMessage oAuthMessage, OAuth2Parameters params)
throws InvalidRequestException {

String pkceChallengeMethod;
String pkceChallengeMethod = null;
boolean isFapiConformantApp = isFapiConformant(params.getClientId());
// If the code_challenge_method is in the request object, then it is added to Oauth2 params before this point.
if (params.getPkceCodeChallengeMethod() != null) {
// If Oauth2 params contains code_challenge_method get value from Oauth2 params.
pkceChallengeMethod = params.getPkceCodeChallengeMethod();
} else {
// Else retrieve from request query params.
} else if (!isFapiConformantApp) {
// Else retrieve from request query params if application is not FAPI compliant.
pkceChallengeMethod = oAuthMessage.getOauthPKCECodeChallengeMethod();
}

Expand Down Expand Up @@ -4129,4 +4150,12 @@ private boolean isPromptSelectAccount(OAuth2Parameters oauth2Params) {

return OAuthConstants.Prompt.SELECT_ACCOUNT.equals(oauth2Params.getPrompt());
}

private boolean isFapiConformant(String clientId) throws InvalidRequestException {
try {
janakamarasena marked this conversation as resolved.
Show resolved Hide resolved
return OAuth2Util.isFapiConformantApp(clientId);
} catch (IdentityOAuth2Exception e) {
throw new InvalidRequestException(e.getMessage(), e.getErrorCode());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,18 +255,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 (isFAPIConformantApp(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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -1712,7 +1717,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.isEmpty(state) && oAuth2Parameters != null && oAuth2Parameters.getState() != null) {
RivinduM marked this conversation as resolved.
Show resolved Hide resolved
state = oAuth2Parameters.getState();
if (log.isDebugEnabled()) {
log.debug("Retrieved state value " + state + " from OAuth2Parameters.");
Expand All @@ -1727,6 +1743,32 @@ 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("request_param_value_builder");
RivinduM marked this conversation as resolved.
Show resolved Hide resolved
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) {
log.debug("Error while retrieving state from request object.", e);
RivinduM marked this conversation as resolved.
Show resolved Hide resolved
}
return null;
}

/**
* Return updated redirect URL.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.utils.DiagnosticLog;


import javax.servlet.http.HttpServletRequest;

/**
Expand Down