diff --git a/server-spi-private/src/main/java/org/keycloak/models/ClientRegistrationAccessTokenConstants.java b/server-spi-private/src/main/java/org/keycloak/models/ClientRegistrationAccessTokenConstants.java new file mode 100644 index 0000000000..1078c42f20 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/ClientRegistrationAccessTokenConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 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.models; + +public class ClientRegistrationAccessTokenConstants { + + public static final String ROTATION_ENABLED = "client.registration.access.token.enabled"; + + private ClientRegistrationAccessTokenConstants(){} + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegistrationAccessTokenRotationDisabledExecutor.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegistrationAccessTokenRotationDisabledExecutor.java new file mode 100644 index 0000000000..02eae0d52f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegistrationAccessTokenRotationDisabledExecutor.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 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.models.ClientRegistrationAccessTokenConstants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; +import org.keycloak.services.clientpolicy.ClientPolicyContext; +import org.keycloak.services.clientpolicy.ClientPolicyException; + +public class RegistrationAccessTokenRotationDisabledExecutor implements ClientPolicyExecutorProvider { + + private final String providerId; + private final KeycloakSession session; + + public RegistrationAccessTokenRotationDisabledExecutor(String providerId, KeycloakSession session) { + this.providerId = providerId; + this.session = session; + } + + @Override + public String getProviderId() { + return providerId; + } + + @Override + public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { + if (session.getAttribute(ClientRegistrationAccessTokenConstants.ROTATION_ENABLED) == null){ + return; + } + session.setAttribute(ClientRegistrationAccessTokenConstants.ROTATION_ENABLED, false); + } + +} diff --git a/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegistrationAccessTokenRotationDisabledExecutorFactory.java b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegistrationAccessTokenRotationDisabledExecutorFactory.java new file mode 100644 index 0000000000..1a804a4278 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/clientpolicy/executor/RegistrationAccessTokenRotationDisabledExecutorFactory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 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; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +public class RegistrationAccessTokenRotationDisabledExecutorFactory implements ClientPolicyExecutorProviderFactory { + + public static final String PROVIDER_ID = "registration-access-token-rotation-disabled"; + + @Override + public String getHelpText() { + return "Disables registration access rotation for the client."; + } + + @Override + public List getConfigProperties() { + return Collections.emptyList(); + } + + @Override + public ClientPolicyExecutorProvider create(KeycloakSession session) { + return new RegistrationAccessTokenRotationDisabledExecutor(getId(), session); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public boolean isSupported() { + return true; + } +} diff --git a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java index 523e4a16c0..078a54169b 100755 --- a/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java +++ b/services/src/main/java/org/keycloak/services/clientregistration/AbstractClientRegistrationProvider.java @@ -22,6 +22,7 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.models.ClientInitialAccessModel; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientRegistrationAccessTokenConstants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; @@ -142,6 +143,7 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist event.event(EventType.CLIENT_UPDATE).client(clientId); ClientModel client = session.getContext().getRealm().getClientByClientId(clientId); + session.setAttribute(ClientRegistrationAccessTokenConstants.ROTATION_ENABLED, true); RegistrationAuth registrationAuth = auth.requireUpdate(context, client); if (!client.getClientId().equals(rep.getClientId())) { @@ -165,9 +167,15 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist } if (auth.isRegistrationAccessToken()) { - String registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth()); + String registrationAccessToken; + if ((boolean) session.getAttribute(ClientRegistrationAccessTokenConstants.ROTATION_ENABLED)) { + registrationAccessToken = ClientRegistrationTokenUtils.updateRegistrationAccessToken(session, client, auth.getRegistrationAuth()); + } else { + registrationAccessToken = ClientRegistrationTokenUtils.updateTokenSignature(session, auth); + } rep.setRegistrationAccessToken(registrationAccessToken); } + session.removeAttribute(ClientRegistrationAccessTokenConstants.ROTATION_ENABLED); try { session.getContext().setClient(client); 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 be75d59bd3..61cda773c0 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 @@ -18,4 +18,5 @@ org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentia org.keycloak.services.clientpolicy.executor.ClientSecretRotationExecutorFactory org.keycloak.services.clientpolicy.executor.RejectRequestExecutorFactory org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory -org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutorFactory \ No newline at end of file +org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutorFactory +org.keycloak.services.clientpolicy.executor.RegistrationAccessTokenRotationDisabledExecutorFactory 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 0f4d5992a5..2300ce545b 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 @@ -111,6 +111,7 @@ import org.keycloak.services.clientpolicy.executor.FullScopeDisabledExecutorFact import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutorFactory; import org.keycloak.services.clientpolicy.executor.IntentClientBindCheckExecutorFactory; import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory; +import org.keycloak.services.clientpolicy.executor.RegistrationAccessTokenRotationDisabledExecutorFactory; import org.keycloak.services.clientpolicy.executor.RejectRequestExecutorFactory; import org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutorFactory; import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutorFactory; @@ -3391,6 +3392,38 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest { assertEquals("no claim for an intent value for ID token" , oauth.getCurrentFragment().get(OAuth2Constants.ERROR_DESCRIPTION)); } + @Test + public void testRegistrationAccessTokenRotationDisabledExecutor() throws Exception { + // register profiles - client autoConfigured to disable registration access token rotation + String json = new ClientProfilesBuilder().addProfile( + new ClientProfileBuilder().createProfile(PROFILE_NAME, "Test Profile") + .addExecutor( + RegistrationAccessTokenRotationDisabledExecutorFactory.PROVIDER_ID, + new ClientPolicyExecutorConfigurationRepresentation() + ) + .toRepresentation() + ).toString(); + updateProfiles(json); + + // register policies + json = new ClientPoliciesBuilder().addPolicy( + new ClientPolicyBuilder().createPolicy(POLICY_NAME, "Test Policy", Boolean.TRUE) + .addCondition(AnyClientConditionFactory.PROVIDER_ID, + createAnyClientConditionConfig()) + .addProfile(PROFILE_NAME) + .toRepresentation() + ).toString(); + updatePolicies(json); + + String clientId = createClientDynamically(generateSuffixedName(CLIENT_NAME), r -> {}); + OIDCClientRepresentation createdClient = getClientDynamically(clientId); + + updateClientDynamically(clientId, clientRep -> + clientRep.setTokenEndpointAuthMethod(OIDCLoginProtocol.CLIENT_SECRET_BASIC)); + + assertEquals(createdClient.getRegistrationAccessToken(), getClientDynamically(clientId).getRegistrationAccessToken()); + } + private void openVerificationPage(String verificationUri) { driver.navigate().to(verificationUri); }