diff --git a/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java b/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java
index e962c92fde..659c020a97 100644
--- a/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java
+++ b/server-spi/src/main/java/org/keycloak/services/clientpolicy/ClientPolicyEvent.java
@@ -38,6 +38,7 @@ public enum ClientPolicyEvent {
USERINFO_REQUEST,
LOGOUT_REQUEST,
BACKCHANNEL_AUTHENTICATION_REQUEST,
+ BACKCHANNEL_TOKEN_REQUEST,
PUSHED_AUTHORIZATION_REQUEST
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java
index f3ec56b51b..dff4a9063c 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/CibaGrantType.java
@@ -44,12 +44,14 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
+import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenRequestContext;
import org.keycloak.protocol.oidc.grants.ciba.endpoints.CibaRootEndpoint;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
+import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.util.DefaultClientSessionContext;
@@ -189,6 +191,13 @@ public class CibaGrantType {
store.removeDeviceCode(realm, request.getId());
+ try {
+ session.clientPolicy().triggerOnEvent(new BackchannelTokenRequestContext(request, formParams));
+ } catch (ClientPolicyException cpe) {
+ event.error(cpe.getError());
+ throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
+ }
+
// Compute client scopes again from scope parameter. Check if user still has them granted
// (but in code-to-token request, it could just theoretically happen that they are not available)
String scopeParam = request.getScope();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/context/BackchannelTokenRequestContext.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/context/BackchannelTokenRequestContext.java
new file mode 100644
index 0000000000..04a64d8090
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/context/BackchannelTokenRequestContext.java
@@ -0,0 +1,53 @@
+/*
+ * 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.protocol.oidc.grants.ciba.clientpolicy.context;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.keycloak.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
+import org.keycloak.services.clientpolicy.ClientPolicyContext;
+import org.keycloak.services.clientpolicy.ClientPolicyEvent;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class BackchannelTokenRequestContext implements ClientPolicyContext {
+
+ private final CIBAAuthenticationRequest request;
+ private final MultivaluedMap requestParameters;
+
+ public BackchannelTokenRequestContext(CIBAAuthenticationRequest request,
+ MultivaluedMap requestParameters) {
+ this.request = request;
+ this.requestParameters = requestParameters;
+ }
+
+ @Override
+ public ClientPolicyEvent getEvent() {
+ return ClientPolicyEvent.BACKCHANNEL_TOKEN_REQUEST;
+ }
+
+ public CIBAAuthenticationRequest getRequest() {
+ return request;
+ }
+
+ public MultivaluedMap getRequestParameters() {
+ return requestParameters;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/services/clientpolicy/executor/TestRaiseExeptionExecutor.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/services/clientpolicy/executor/TestRaiseExeptionExecutor.java
index a95983a083..35da3419ae 100644
--- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/services/clientpolicy/executor/TestRaiseExeptionExecutor.java
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/services/clientpolicy/executor/TestRaiseExeptionExecutor.java
@@ -52,6 +52,7 @@ public class TestRaiseExeptionExecutor implements ClientPolicyExecutorProvider {
+ clientRep.setSecret(clientSecret);
+ clientRep.setStandardFlowEnabled(Boolean.TRUE);
+ clientRep.setImplicitFlowEnabled(Boolean.TRUE);
+ clientRep.setPublicClient(Boolean.FALSE);
+ clientRep.setBearerOnly(Boolean.FALSE);
+ Map attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
+ attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "poll");
+ attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
+ clientRep.setAttributes(attributes);
+ });
+
+ // register profiles
+ String json = (new ClientProfilesBuilder()).addProfile(
+ (new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
+ .addExecutor(TestRaiseExeptionExecutorFactory.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);
+
+ AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, "Pjb9eD8w", null, null);
+ assertEquals(400, response.getStatusCode());
+ assertEquals(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.toString(), response.getError());
+ assertEquals("Exception thrown intentionally", response.getErrorDescription());
+ }
+
+ @Test
+ public void testExtendedClientPolicyIntefacesForBackchannelTokenRequest() throws Exception {
+ String clientId = generateSuffixedName("confidential-app");
+ String clientSecret = "app-secret";
+ createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
+ clientRep.setSecret(clientSecret);
+ clientRep.setStandardFlowEnabled(Boolean.TRUE);
+ clientRep.setImplicitFlowEnabled(Boolean.TRUE);
+ clientRep.setPublicClient(Boolean.FALSE);
+ clientRep.setBearerOnly(Boolean.FALSE);
+ Map attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
+ attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "poll");
+ attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
+ clientRep.setAttributes(attributes);
+ });
+
+ final String bindingMessage = "BASTION";
+ Map additionalParameters = new HashMap<>();
+ additionalParameters.put("user_device", "mobile");
+
+ // user Backchannel Authentication Request
+ AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, bindingMessage, null, additionalParameters);
+ assertThat(response.getStatusCode(), is(equalTo(200)));
+ Assert.assertNotNull(response.getAuthReqId());
+
+ TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
+ TestAuthenticationChannelRequest authenticationChannelReq = oidcClientEndpointsResource.getAuthenticationChannel(bindingMessage);
+ int statusCode = oauth.doAuthenticationChannelCallback(authenticationChannelReq.getBearerToken(), SUCCEED);
+ assertThat(statusCode, is(equalTo(200)));
+
+ // register profiles
+ String json = (new ClientProfilesBuilder()).addProfile(
+ (new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
+ .addExecutor(TestRaiseExeptionExecutorFactory.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);
+
+ OAuthClient.AccessTokenResponse tokenRes = oauth.doBackchannelAuthenticationTokenRequest(clientId, clientSecret, response.getAuthReqId());
+ assertThat(tokenRes.getStatusCode(), is(equalTo(400)));
+ assertThat(tokenRes.getError(), is(OAuthErrorException.INVALID_GRANT));
+ assertThat(tokenRes.getErrorDescription(), is("Exception thrown intentionally"));
+ }
+
private void testBackchannelAuthenticationFlowNotRegisterSigAlgInAdvanceWithSignedAuthentication(String clientName, boolean useRequestUri, String requestedSigAlg, String sigAlg, int statusCode, String errorDescription) throws Exception {
String clientId = createClientDynamically(clientName, (OIDCClientRepresentation clientRep) -> {
List grantTypes = Optional.ofNullable(clientRep.getGrantTypes()).orElse(new ArrayList<>());
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 99eeca1af0..966d54759d 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
@@ -34,13 +34,11 @@ import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator;
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
import org.keycloak.client.registration.ClientRegistrationException;
-import org.keycloak.common.Profile;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.models.AdminRoles;
-import org.keycloak.models.CibaConfig;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
@@ -82,7 +80,6 @@ import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmExecuto
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtExecutorFactory;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
-import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject;
@@ -93,7 +90,6 @@ import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RoleBuilder;
import org.keycloak.testsuite.util.ServerURLs;
-import org.keycloak.testsuite.util.OAuthClient.AuthenticationRequestAcknowledgement;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
@@ -114,6 +110,7 @@ import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
+import static org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse.Status.SUCCEED;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
@@ -2242,47 +2239,6 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
}
}
- @Test
- @EnableFeature(value = Profile.Feature.CIBA, skipRestart = true)
- @AuthServerContainerExclude({REMOTE, QUARKUS})
- public void testExtendedClientPolicyIntefacesForCiba() throws Exception {
- String clientId = generateSuffixedName("confidential-app");
- String clientSecret = "app-secret";
- createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
- clientRep.setSecret(clientSecret);
- clientRep.setStandardFlowEnabled(Boolean.TRUE);
- clientRep.setImplicitFlowEnabled(Boolean.TRUE);
- clientRep.setPublicClient(Boolean.FALSE);
- clientRep.setBearerOnly(Boolean.FALSE);
- Map attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
- attributes.put(CibaConfig.CIBA_BACKCHANNEL_TOKEN_DELIVERY_MODE_PER_CLIENT, "poll");
- attributes.put(CibaConfig.OIDC_CIBA_GRANT_ENABLED, Boolean.TRUE.toString());
- clientRep.setAttributes(attributes);
- });
-
- // register profiles
- String json = (new ClientProfilesBuilder()).addProfile(
- (new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Den Forste Profilen")
- .addExecutor(TestRaiseExeptionExecutorFactory.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);
-
- AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientId, clientSecret, TEST_USER_NAME, "Pjb9eD8w", null, null);
- assertEquals(400, response.getStatusCode());
- assertEquals(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.toString(), response.getError());
- assertEquals("Exception thrown intentionally", response.getErrorDescription());
- }
-
private void checkMtlsFlow() throws IOException {
// Check login.
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);