From 7cdcf0f93e12e0bf3c4ea63fb713cd7e38052ab6 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Mon, 5 Jul 2021 05:11:41 +0900 Subject: [PATCH] KEYCLOAK-18654 Client Policy - Endpoint : support Token Request by CIBA Backchannel Authentication --- .../clientpolicy/ClientPolicyEvent.java | 1 + .../oidc/grants/ciba/CibaGrantType.java | 9 ++ .../BackchannelTokenRequestContext.java | 53 ++++++++++ .../executor/TestRaiseExeptionExecutor.java | 1 + .../keycloak/testsuite/client/CIBATest.java | 97 +++++++++++++++++++ .../testsuite/client/ClientPoliciesTest.java | 46 +-------- 6 files changed, 162 insertions(+), 45 deletions(-) create mode 100644 services/src/main/java/org/keycloak/protocol/oidc/grants/ciba/clientpolicy/context/BackchannelTokenRequestContext.java 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);