diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSessionEnforceExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSessionEnforceExecutor.java new file mode 100644 index 0000000000..1ca7bbc179 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSessionEnforceExecutor.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed 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.keycloak.services.clientpolicy.executor; + +import org.jboss.logging.Logger; +import org.keycloak.OAuthErrorException; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest; +import org.keycloak.protocol.oidc.utils.OIDCResponseType; +import org.keycloak.services.clientpolicy.AuthorizationRequestContext; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; +import org.keycloak.services.clientpolicy.ClientPolicyLogger; +import org.keycloak.util.TokenUtil; + +public class SecureSessionEnforceExecutor implements ClientPolicyExecutorProvider { + + private static final Logger logger = Logger.getLogger(SecureSessionEnforceExecutor.class); + + private final KeycloakSession session; + private final ComponentModel componentModel; + + public SecureSessionEnforceExecutor(KeycloakSession session, ComponentModel componentModel) { + this.session = session; + this.componentModel = componentModel; + } + + @Override + public String getName() { + return componentModel.getName(); + } + + @Override + public String getProviderId() { + return componentModel.getProviderId(); + } + + @Override + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + switch (context.getEvent()) { + case AUTHORIZATION_REQUEST: + AuthorizationRequestContext authorizationRequestContext = (AuthorizationRequestContext)context; + executeOnAuthorizationRequest(authorizationRequestContext.getparsedResponseType(), + authorizationRequestContext.getAuthorizationEndpointRequest(), + authorizationRequestContext.getRedirectUri()); + return; + default: + return; + } + } + + private void executeOnAuthorizationRequest( + OIDCResponseType parsedResponseType, + AuthorizationEndpointRequest request, + String redirectUri) throws ClientPolicyException { + ClientPolicyLogger.log(logger, "Authz Endpoint - authz request"); + if (TokenUtil.isOIDCRequest(request.getScope())) { + if(request.getNonce() == null) { + ClientPolicyLogger.log(logger, "Missing parameter: nonce"); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: nonce"); + } + } else { + if(request.getState() == null) { + ClientPolicyLogger.log(logger, "Missing parameter: state"); + throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: state"); + } + } + ClientPolicyLogger.log(logger, "Passed."); + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSessionEnforceExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSessionEnforceExecutorFactory.java new file mode 100644 index 0000000000..18184513f4 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/SecureSessionEnforceExecutorFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed 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.keycloak.services.clientpolicy.executor; + +import java.util.Collections; +import java.util.List; + +import org.keycloak.Config.Scope; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +public class SecureSessionEnforceExecutorFactory implements ClientPolicyExecutorProviderFactory { + + public static final String PROVIDER_ID = "secure-session-enforce-executor"; + + @Override + public ClientPolicyExecutorProvider create(KeycloakSession session, ComponentModel model) { + return new SecureSessionEnforceExecutor(session, model); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "To prevent CSRF, it refuses the client's authorization request which lacks nonce in OIDC flow or state in OAuth2 grant."; + } + + @Override + public List getConfigProperties() { + return Collections.emptyList(); + } + +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory index 3aa581ca37..4fb3071840 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProviderFactory @@ -2,3 +2,4 @@ org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory org.keycloak.services.clientpolicy.executor.SecureClientAuthEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory +org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java index 48e833987e..fac6d92889 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPolicyBasicsTest.java @@ -88,6 +88,7 @@ import org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor; import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutorFactory; +import org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; @@ -812,6 +813,61 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { } } + @Test + public void testSecureSessionEnforceExecutor() throws ClientRegistrationException, ClientPolicyException { + String policyBetaName = "MyPolicy-beta"; + createPolicy(policyBetaName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null); + logger.info("... Created Policy : " + policyBetaName); + + createCondition("ClientRolesCondition-beta", ClientRolesConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { + setConditionClientRoles(provider, new ArrayList<>(Arrays.asList("sample-client-role-beta"))); + }); + registerCondition("ClientRolesCondition-beta", policyBetaName); + logger.info("... Registered Condition : ClientRolesCondition-beta"); + + createExecutor("SecureSessionEnforceExecutor-beta", SecureSessionEnforceExecutorFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> { + }); + registerExecutor("SecureSessionEnforceExecutor-beta", policyBetaName); + logger.info("... Registered Executor : SecureSessionEnforceExecutor-beta"); + + String clientAlphaId = "Alpha-App"; + String clientAlphaSecret = "secretAlpha"; + String cAlphaId = createClientByAdmin(clientAlphaId, (ClientRepresentation clientRep) -> { + clientRep.setDefaultRoles((String[]) Arrays.asList("sample-client-role-alpha").toArray(new String[1])); + clientRep.setSecret(clientAlphaSecret); + }); + + String clientBetaId = "Beta-App"; + String clientBetaSecret = "secretBeta"; + String cBetaId = createClientByAdmin(clientBetaId, (ClientRepresentation clientRep) -> { + clientRep.setDefaultRoles((String[]) Arrays.asList("sample-client-role-beta").toArray(new String[1])); + clientRep.setSecret(clientBetaSecret); + }); + + try { + successfulLoginAndLogout(clientAlphaId, clientAlphaSecret); + + oauth.openid(false); + successfulLoginAndLogout(clientAlphaId, clientAlphaSecret); + + oauth.openid(true); + failLoginWithoutSecureSessionParameter(clientBetaId, "Missing parameter: nonce"); + + oauth.nonce("yesitisnonce"); + successfulLoginAndLogout(clientBetaId, clientBetaSecret); + + oauth.openid(false); + oauth.stateParamHardcoded(null); + failLoginWithoutSecureSessionParameter(clientBetaId, "Missing parameter: state"); + + oauth.stateParamRandom(); + successfulLoginAndLogout(clientBetaId, clientBetaSecret); + } finally { + deleteClientByAdmin(cAlphaId); + deleteClientByAdmin(cBetaId); + } + } + private AuthorizationEndpointRequestObject createValidRequestObjectForSecureRequestObjectExecutor(String clientId) throws URISyntaxException { AuthorizationEndpointRequestObject requestObject = new AuthorizationEndpointRequestObject(); requestObject.id(KeycloakModelUtils.generateId()); @@ -960,6 +1016,13 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest { events.expectLogout(sessionId).clearDetails().assertEvent(); } + private void failLoginWithoutSecureSessionParameter(String clientId, String errorDescription) { + oauth.clientId(clientId); + oauth.openLoginForm(); + assertEquals(OAuthErrorException.INVALID_REQUEST, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); + assertEquals(errorDescription, oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); + } + private String generateS256CodeChallenge(String codeVerifier) throws Exception { MessageDigest md = MessageDigest.getInstance("SHA-256"); md.update(codeVerifier.getBytes("ISO_8859_1"));