diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/ConfidentialClientAcceptExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ConfidentialClientAcceptExecutor.java new file mode 100644 index 0000000000..bbeae4436c --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ConfidentialClientAcceptExecutor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 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.keycloak.OAuthErrorException; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author Takashi Norimatsu + */ +public class ConfidentialClientAcceptExecutor implements ClientPolicyExecutorProvider { + + protected final KeycloakSession session; + + public ConfidentialClientAcceptExecutor(KeycloakSession session) { + this.session = session; + } + + @Override + public String getProviderId() { + return ConfidentialClientAcceptExecutorFactory.PROVIDER_ID; + } + + @Override + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + switch (context.getEvent()) { + case AUTHORIZATION_REQUEST: + case TOKEN_REQUEST: + checkIsConfidentialClient(); + return; + default: + return; + } + } + + private void checkIsConfidentialClient() throws ClientPolicyException { + ClientModel client = session.getContext().getClient(); + if (client == null) { + throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT, "invalid client access type"); + } + if (client.isPublicClient()) { + throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT, "invalid client access type"); + } + if (client.isBearerOnly()) { + throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT, "invalid client access type"); + } + } +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/ConfidentialClientAcceptExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ConfidentialClientAcceptExecutorFactory.java new file mode 100644 index 0000000000..3c1fa3af4f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/ConfidentialClientAcceptExecutorFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021 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.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Takashi Norimatsu + */ +public class ConfidentialClientAcceptExecutorFactory implements ClientPolicyExecutorProviderFactory { + + public static final String PROVIDER_ID = "confidentialclient-accept-executor"; + + @Override + public ClientPolicyExecutorProvider create(KeycloakSession session) { + return new ConfidentialClientAcceptExecutor(session); + } + + @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 "On authorization endpoint and token endpoint, this executor checks whether the client is confidential client. If not, it denies its request."; + } + + @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 852431ab14..c4c6f59825 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 @@ -6,4 +6,5 @@ org.keycloak.services.clientpolicy.executor.SecureSessionEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureRedirectUriEnforceExecutorFactory org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtEnforceExecutorFactory -org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory \ No newline at end of file +org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory +org.keycloak.services.clientpolicy.executor.ConfidentialClientAcceptExecutorFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java index a32c3bbf5e..b1f41550d3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AbstractClientPoliciesTest.java @@ -116,6 +116,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceHostsCondi import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceHostsConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesCondition; import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesConditionFactory; +import org.keycloak.services.clientpolicy.executor.ConfidentialClientAcceptExecutor; import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutor; import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutor; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java index b2ab94db68..2df5efc7a2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/ClientPoliciesTest.java @@ -81,6 +81,7 @@ import org.keycloak.services.clientpolicy.condition.ClientUpdateContextCondition import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceGroupsConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceHostsConditionFactory; import org.keycloak.services.clientpolicy.condition.ClientUpdateSourceRolesConditionFactory; +import org.keycloak.services.clientpolicy.executor.ConfidentialClientAcceptExecutorFactory; import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureClientAuthEnforceExecutorFactory; @@ -1511,6 +1512,55 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { } } + @Test + public void testConfidentialClientAcceptExecutorExecutor() throws Exception { + // register profiles + String json = (new ClientProfilesBuilder()).addProfile( + (new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Erstes Profil", Boolean.FALSE, null) + .addExecutor(ConfidentialClientAcceptExecutorFactory.PROVIDER_ID, null) + .toRepresentation() + ).toString(); + updateProfiles(json); + + // register policies + json = (new ClientPoliciesBuilder()).addPolicy( + (new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Erstes Politik", Boolean.FALSE, Boolean.TRUE, null, null) + .addCondition(ClientRolesConditionFactory.PROVIDER_ID, + createClientRolesConditionConfig(Arrays.asList(SAMPLE_CLIENT_ROLE))) + .addProfile(PROFILE_NAME) + .toRepresentation() + ).toString(); + updatePolicies(json); + + String clientConfidentialId = generateSuffixedName("confidential-app"); + String clientConfidentialSecret = "app-secret"; + String cidConfidential = createClientByAdmin(clientConfidentialId, (ClientRepresentation clientRep) -> { + clientRep.setSecret(clientConfidentialSecret); + clientRep.setStandardFlowEnabled(Boolean.TRUE); + clientRep.setImplicitFlowEnabled(Boolean.TRUE); + clientRep.setPublicClient(Boolean.FALSE); + clientRep.setBearerOnly(Boolean.FALSE); + }); + adminClient.realm(REALM_NAME).clients().get(cidConfidential).roles().create(RoleBuilder.create().name(SAMPLE_CLIENT_ROLE).build()); + + successfulLoginAndLogout(clientConfidentialId, clientConfidentialSecret); + + String clientPublicId = generateSuffixedName("public-app"); + String cidPublic = createClientByAdmin(clientPublicId, (ClientRepresentation clientRep) -> { + clientRep.setSecret(clientConfidentialSecret); + clientRep.setStandardFlowEnabled(Boolean.TRUE); + clientRep.setImplicitFlowEnabled(Boolean.TRUE); + clientRep.setPublicClient(Boolean.TRUE); + clientRep.setBearerOnly(Boolean.FALSE); + }); + adminClient.realm(REALM_NAME).clients().get(cidPublic).roles().create(RoleBuilder.create().name(SAMPLE_CLIENT_ROLE).build()); + + oauth.clientId(clientPublicId); + oauth.openLoginForm(); + assertEquals(OAuthErrorException.INVALID_CLIENT, oauth.getCurrentQuery().get(OAuth2Constants.ERROR)); + assertEquals("invalid client access type", oauth.getCurrentQuery().get(OAuth2Constants.ERROR_DESCRIPTION)); + } + private void checkMtlsFlow() throws IOException { // Check login. OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);