FAPI 2.0 security profile - not allow an authorization request whose parameters were not included in Request Object pushed to PAR request

closes #20710
This commit is contained in:
Takashi Norimatsu 2023-06-02 18:35:56 +09:00 committed by Marek Posolda
parent c026884734
commit f6ecc3f3f8
4 changed files with 176 additions and 67 deletions

View file

@ -63,6 +63,7 @@ import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -310,24 +311,10 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
authenticationSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
authenticationSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
if (request.getState() != null) authenticationSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
if (request.getNonce() != null) authenticationSession.setClientNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
performActionOnParameters(request, (paramName, paramValue) -> {if (paramValue != null) authenticationSession.setClientNote(paramName, paramValue);});
if (request.getMaxAge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
if (request.getScope() != null) authenticationSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
if (request.getLoginHint() != null) authenticationSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
if (request.getPrompt() != null) authenticationSession.setClientNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
if (request.getIdpHint() != null) authenticationSession.setClientNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
if (request.getAction() != null) authenticationSession.setClientNote(Constants.KC_ACTION, request.getAction());
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
if (request.getDisplay() != null) authenticationSession.setAuthNote(OAuth2Constants.DISPLAY, request.getDisplay());
if (request.getUiLocales() != null) authenticationSession.setAuthNote(LocaleSelectorProvider.CLIENT_REQUEST_LOCALE, request.getUiLocales());
// https://tools.ietf.org/html/rfc7636#section-4
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
if (request.getCodeChallengeMethod() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
Map<String, Integer> acrLoaMap = AcrUtils.getAcrLoaMap(authenticationSession.getClient());
List<String> acrValues = AcrUtils.getRequiredAcrValues(request.getClaims());
@ -393,4 +380,19 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return processor.authenticate();
}
public static void performActionOnParameters(AuthorizationEndpointRequest request, BiConsumer<String, String> paramAction) {
paramAction.accept(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
paramAction.accept(Constants.KC_ACTION, request.getAction());
paramAction.accept(OAuth2Constants.DISPLAY, request.getDisplay());
paramAction.accept(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
paramAction.accept(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
paramAction.accept(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
paramAction.accept(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
paramAction.accept(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
paramAction.accept(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
paramAction.accept(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
paramAction.accept(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
paramAction.accept(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
paramAction.accept(OIDCLoginProtocol.STATE_PARAM, request.getState());
}
}

View file

@ -17,15 +17,25 @@
package org.keycloak.services.clientpolicy.executor;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
import org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestObjectParser;
import org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestParser;
import org.keycloak.protocol.oidc.endpoints.request.RequestUriType;
import org.keycloak.protocol.oidc.par.endpoints.ParEndpoint;
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
@ -65,8 +75,8 @@ public class SecureParContentsExecutor implements ClientPolicyExecutorProvider<C
}
private void checkValidParContents(PreAuthorizationRequestContext preAuthorizationRequestContext) throws ClientPolicyException {
MultivaluedMap<String, String> requestParameters = preAuthorizationRequestContext.getRequestParameters();
String requestUri = requestParameters.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM);
MultivaluedMap<String, String> requestParametersFromQuery = preAuthorizationRequestContext.getRequestParameters();
String requestUri = requestParametersFromQuery.getFirst(OIDCLoginProtocol.REQUEST_URI_PARAM);
if (requestUri == null) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "request_uri not included.");
}
@ -76,17 +86,54 @@ public class SecureParContentsExecutor implements ClientPolicyExecutorProvider<C
String key = requestUri.substring(ParEndpoint.REQUEST_URI_PREFIX_LENGTH);
SingleUseObjectProvider singleUseStore = session.singleUseObjects();
Map<String, String> retrievedRequest = singleUseStore.get(key);
if (retrievedRequest == null) {
Map<String, String> requestParametersFromPAR = singleUseStore.get(key);
if (requestParametersFromPAR == null) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "PAR not found. not issued or used multiple times.");
}
Set<String> queryParameterNames = requestParameters.keySet();
for (String queryParamName : queryParameterNames) {
if (!retrievedRequest.keySet().contains(queryParamName) && !OIDCLoginProtocol.REQUEST_URI_PARAM.equals(queryParamName)) {
Set<String> requestParametersNameFromPAR = new HashSet<>();
if (requestParametersFromPAR.containsKey(OIDCLoginProtocol.REQUEST_PARAM)) {
// if PAR request includes request object (JAR), parsing the request is needed.
requestParametersNameFromPAR = getParRetrievedRequestParameters(requestParametersFromPAR, preAuthorizationRequestContext.getClientId());
} else {
requestParametersNameFromPAR = requestParametersFromPAR.keySet();
}
for (String queryParamName : requestParametersFromQuery.keySet()) {
if (!requestParametersNameFromPAR.contains(queryParamName) && !OIDCLoginProtocol.REQUEST_URI_PARAM.equals(queryParamName)) {
singleUseStore.remove(key);
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "PAR request did not include necessary parameters");
}
}
}
private Set<String> getParRetrievedRequestParameters(Map<String, String> requestParametersFromPAR, String clientId) {
AuthorizationEndpointRequest request = new AuthorizationEndpointRequest();
Set<String> parRetrievedRequest = new HashSet<>();
String requestObjectString = requestParametersFromPAR.get(OIDCLoginProtocol.REQUEST_PARAM);
RealmModel realm = session.getContext().getRealm();
ClientModel client = realm.getClientByClientId(clientId);
new AuthzEndpointRequestObjectParser(session, requestObjectString, client).parseRequest(request);
// from PAR request parameters other than ones included in a request object
for (String param : requestParametersFromPAR.keySet()) {
if (OIDCLoginProtocol.REQUEST_PARAM.equals(param)) continue;
parRetrievedRequest.add(param);
}
// from parsed PAR request parameters
AuthorizationEndpoint.performActionOnParameters(request, (paramName, paramValue) -> {if (paramValue != null) parRetrievedRequest.add(paramName);});
if (request.getClientId() != null) parRetrievedRequest.add(OIDCLoginProtocol.CLIENT_ID_PARAM);
if (request.getResponseType() != null) parRetrievedRequest.add(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
if (request.getRedirectUriParam() != null) parRetrievedRequest.add(OIDCLoginProtocol.REDIRECT_URI_PARAM);
if (request.getMaxAge() != null) parRetrievedRequest.add(OIDCLoginProtocol.MAX_AGE_PARAM);
if (request.getUiLocales() != null) parRetrievedRequest.add(OAuth2Constants.UI_LOCALES_PARAM);
for (String additionalParam : request.getAdditionalReqParams().keySet()) {
if (!AuthzEndpointRequestParser.KNOWN_REQ_PARAMS.contains(additionalParam)) {
parRetrievedRequest.add(additionalParam);
}
}
return parRetrievedRequest;
}
}

View file

@ -90,6 +90,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdaterContextConditio
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureClientUrisExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureLogoutExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory;
@ -99,6 +100,7 @@ import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSign
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.pages.OAuth2DeviceVerificationPage;
@ -109,6 +111,7 @@ import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPoliciesBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder;
import org.keycloak.testsuite.util.OAuthClient.ParResponse;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RoleBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@ -1434,4 +1437,106 @@ public class ClientPoliciesExecutorTest extends AbstractClientPoliciesTest {
assertTrue(driver.getPageSource().contains("Front-channel logout is not allowed for this client"));
}
@Test
public void testSecureParContentsExecutor() throws Exception {
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
.addExecutor(SecureParContentsExecutorFactory.PROVIDER_ID, null)
.toRepresentation()
).toString();
updateProfiles(json);
String clientBetaId = generateSuffixedName("Beta-App");
createClientByAdmin(clientBetaId, (ClientRepresentation clientRep) -> {
clientRep.setSecret("secretBeta");
});
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
// Pushed Authorization Request
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientBetaId, "secretBeta");
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
oauth.requestUri(requestUri);
oauth.clientId(clientBetaId);
oauth.openLoginForm();
assertTrue(errorPage.isCurrent());
assertEquals("PAR request did not include necessary parameters", errorPage.getError());
oauth.requestUri(null);
pResp = oauth.doPushedAuthorizationRequest(clientBetaId, "secretBeta");
assertEquals(201, pResp.getStatusCode());
requestUri = pResp.getRequestUri();
oauth.requestUri(requestUri);
oauth.stateParamHardcoded(null);
successfulLoginAndLogout(clientBetaId, "secretBeta");
}
@Test
public void testSecureParContentsExecutorWithRequestObject() throws Exception {
// Set up a request object
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
oidcClientEndpointsResource.setOIDCRequest(REALM_NAME, TEST_CLIENT, oauth.getRedirectUri(), "10", null, "none");
String encodedRequestObject = oidcClientEndpointsResource.getOIDCRequest();
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
.addExecutor(SecureParContentsExecutorFactory.PROVIDER_ID, null)
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
// Pushed Authorization Request without state parameter
oauth.addCustomParameter("request", encodedRequestObject);
ParResponse pResp = oauth.doPushedAuthorizationRequest(TEST_CLIENT, TEST_CLIENT_SECRET);
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
// only query parameters include state parameter
oauth.removeCustomParameter("request");
oauth.stateParamHardcoded("mystate2");
oauth.requestUri(requestUri);
oauth.openLoginForm();
assertTrue(errorPage.isCurrent());
assertEquals("PAR request did not include necessary parameters", errorPage.getError());
// Pushed Authorization Request with state parameter
oidcClientEndpointsResource.setOIDCRequest(REALM_NAME, TEST_CLIENT, oauth.getRedirectUri(), "10", "mystate2", "none");
encodedRequestObject = oidcClientEndpointsResource.getOIDCRequest();
oauth.requestUri(null);
oauth.addCustomParameter("request", encodedRequestObject);
pResp = oauth.doPushedAuthorizationRequest(TEST_CLIENT, TEST_CLIENT_SECRET);
assertEquals(201, pResp.getStatusCode());
requestUri = pResp.getRequestUri();
// both query parameters and PAR requests include state parameter
oauth.removeCustomParameter("request");
oauth.requestUri(requestUri);
successfulLoginAndLogout(TEST_CLIENT, TEST_CLIENT_SECRET);
}
}

View file

@ -1226,49 +1226,4 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
assertEquals(expectedErrorDescription, oauth.getCurrentFragment().get(OAuth2Constants.ERROR_DESCRIPTION));
}
@Test
public void testSecureParContentsExecutor() throws Exception {
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
.addExecutor(SecureParContentsExecutorFactory.PROVIDER_ID, null)
.toRepresentation()
).toString();
updateProfiles(json);
String clientBetaId = generateSuffixedName("Beta-App");
createClientByAdmin(clientBetaId, (ClientRepresentation clientRep) -> {
clientRep.setSecret("secretBeta");
});
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
createAnyClientConditionConfig())
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
// Pushed Authorization Request
ParResponse pResp = oauth.doPushedAuthorizationRequest(clientBetaId, "secretBeta");
assertEquals(201, pResp.getStatusCode());
String requestUri = pResp.getRequestUri();
oauth.requestUri(requestUri);
oauth.clientId(clientBetaId);
oauth.openLoginForm();
assertTrue(errorPage.isCurrent());
assertEquals("PAR request did not include necessary parameters", errorPage.getError());
oauth.requestUri(null);
pResp = oauth.doPushedAuthorizationRequest(clientBetaId, "secretBeta");
assertEquals(201, pResp.getStatusCode());
requestUri = pResp.getRequestUri();
oauth.requestUri(requestUri);
oauth.stateParamHardcoded(null);
successfulLoginAndLogout(clientBetaId, "secretBeta");
}
}