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:
parent
c026884734
commit
f6ecc3f3f8
4 changed files with 176 additions and 67 deletions
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue