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 d362f31911..a59bf4721d 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
@@ -40,6 +40,8 @@ public enum ClientPolicyEvent {
LOGOUT_REQUEST,
BACKCHANNEL_AUTHENTICATION_REQUEST,
BACKCHANNEL_TOKEN_REQUEST,
- PUSHED_AUTHORIZATION_REQUEST
+ PUSHED_AUTHORIZATION_REQUEST,
+ DEVICE_AUTHORIZATION_REQUEST,
+ DEVICE_TOKEN_REQUEST
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java
index ebf9fec637..8c02540ccc 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/DeviceGrantType.java
@@ -42,8 +42,10 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
+import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceTokenRequestContext;
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
import org.keycloak.services.CorsErrorResponseException;
+import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.resources.Cors;
@@ -260,6 +262,14 @@ public class DeviceGrantType {
Response.Status.BAD_REQUEST);
}
+ try {
+ session.clientPolicy().triggerOnEvent(new DeviceTokenRequestContext(deviceCodeModel, 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 device_code-to-token request, it could just theoretically happen that they are not available)
String scopeParam = deviceCodeModel.getScope();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceAuthorizationRequestContext.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceAuthorizationRequestContext.java
new file mode 100644
index 0000000000..42a966f2c4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceAuthorizationRequestContext.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.device.clientpolicy.context;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
+import org.keycloak.services.clientpolicy.ClientPolicyContext;
+import org.keycloak.services.clientpolicy.ClientPolicyEvent;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class DeviceAuthorizationRequestContext implements ClientPolicyContext {
+
+ private final AuthorizationEndpointRequest request;
+ private final MultivaluedMap requestParameters;
+
+ public DeviceAuthorizationRequestContext(AuthorizationEndpointRequest request,
+ MultivaluedMap requestParameters) {
+ this.request = request;
+ this.requestParameters = requestParameters;
+ }
+
+ @Override
+ public ClientPolicyEvent getEvent() {
+ return ClientPolicyEvent.DEVICE_AUTHORIZATION_REQUEST;
+ }
+
+ public AuthorizationEndpointRequest getRequest() {
+ return request;
+ }
+
+ public MultivaluedMap getRequestParameters() {
+ return requestParameters;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceTokenRequestContext.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceTokenRequestContext.java
new file mode 100644
index 0000000000..fd260fdd0c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/clientpolicy/context/DeviceTokenRequestContext.java
@@ -0,0 +1,52 @@
+/*
+ * 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.device.clientpolicy.context;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.keycloak.models.OAuth2DeviceCodeModel;
+import org.keycloak.services.clientpolicy.ClientPolicyContext;
+import org.keycloak.services.clientpolicy.ClientPolicyEvent;
+
+/**
+ * @author Takashi Norimatsu
+ */
+public class DeviceTokenRequestContext implements ClientPolicyContext {
+
+ private final OAuth2DeviceCodeModel deviceCodeModel;
+ private final MultivaluedMap requestParameters;
+
+ public DeviceTokenRequestContext(OAuth2DeviceCodeModel deviceCodeModel,
+ MultivaluedMap requestParameters) {
+ this.deviceCodeModel = deviceCodeModel;
+ this.requestParameters = requestParameters;
+ }
+
+ @Override
+ public ClientPolicyEvent getEvent() {
+ return ClientPolicyEvent.DEVICE_AUTHORIZATION_REQUEST;
+ }
+
+ public OAuth2DeviceCodeModel getDeviceCodeModel() {
+ return deviceCodeModel;
+ }
+
+ public MultivaluedMap getRequestParameters() {
+ return requestParameters;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java
index 5feacd683b..9198c0ed56 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/grants/device/endpoints/DeviceEndpoint.java
@@ -46,6 +46,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
+import org.keycloak.protocol.oidc.grants.device.clientpolicy.context.DeviceAuthorizationRequestContext;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.OAuth2DeviceAuthorizationResponse;
import org.keycloak.saml.common.util.StringUtil;
@@ -53,6 +54,7 @@ import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
+import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resources.SessionCodeChecks;
@@ -132,6 +134,12 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
"Client not allowed for OAuth 2.0 Device Authorization Grant", Response.Status.BAD_REQUEST);
}
+ try {
+ session.clientPolicy().triggerOnEvent(new DeviceAuthorizationRequestContext(request, httpRequest.getDecodedFormParameters()));
+ } catch (ClientPolicyException cpe) {
+ throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
+ }
+
int expiresIn = realm.getOAuth2DeviceConfig().getLifespan(client);
int interval = realm.getOAuth2DeviceConfig().getPoolingInterval(client);
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 6178097111..9c889005e6 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
@@ -55,6 +55,8 @@ public class TestRaiseExeptionExecutor implements ClientPolicyExecutorProviderTakashi Norimatsu
*/
@@ -148,6 +156,19 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
private static final String TEST_USER_NAME = "test-user@localhost";
private static final String TEST_USER_PASSWORD = "password";
+ public static final String DEVICE_APP = "test-device";
+ public static final String DEVICE_APP_PUBLIC = "test-device-public";
+ private static String userId;
+
+ @Page
+ protected OAuth2DeviceVerificationPage verificationPage;
+
+ @Page
+ protected OAuthGrantPage grantPage;
+
+ @Page
+ protected ErrorPage errorPage;
+
@Override
public void addTestRealms(List testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
@@ -179,6 +200,30 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
realm.setUsers(users);
+ List clients = realm.getClients();
+
+ ClientRepresentation app = ClientBuilder.create()
+ .id(KeycloakModelUtils.generateId())
+ .clientId("test-device")
+ .secret("secret")
+ .attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true")
+ .build();
+ clients.add(app);
+
+ ClientRepresentation appPublic = ClientBuilder.create().id(KeycloakModelUtils.generateId()).publicClient()
+ .clientId(DEVICE_APP_PUBLIC).attribute(OAuth2DeviceConfig.OAUTH2_DEVICE_AUTHORIZATION_GRANT_ENABLED, "true")
+ .build();
+ clients.add(appPublic);
+
+ userId = KeycloakModelUtils.generateId();
+ UserRepresentation deviceUser = UserBuilder.create()
+ .id(userId)
+ .username("device-login")
+ .email("device-login@localhost")
+ .password("password")
+ .build();
+ users.add(deviceUser);
+
testRealms.add(realm);
}
@@ -2452,6 +2497,92 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
}
}
+ @Test
+ public void testExtendedClientPolicyIntefacesForDeviceAuthorizationRequest() throws Exception {
+ // 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);
+
+ // Device Authorization Request from device
+ oauth.realm(REALM_NAME);
+ oauth.clientId(DEVICE_APP);
+ OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
+ assertEquals(400, response.getStatusCode());
+ assertEquals(ClientPolicyEvent.DEVICE_AUTHORIZATION_REQUEST.toString(), response.getError());
+ assertEquals("Exception thrown intentionally", response.getErrorDescription());
+ }
+
+ @Test
+ public void testExtendedClientPolicyIntefacesForDeviceTokenRequest() throws Exception {
+ // Device Authorization Request from device
+ oauth.realm(REALM_NAME);
+ oauth.clientId(DEVICE_APP);
+ OAuthClient.DeviceAuthorizationResponse response = oauth.doDeviceAuthorizationRequest(DEVICE_APP, "secret");
+
+ Assert.assertEquals(200, response.getStatusCode());
+ assertNotNull(response.getDeviceCode());
+ assertNotNull(response.getUserCode());
+ assertNotNull(response.getVerificationUri());
+ assertNotNull(response.getVerificationUriComplete());
+
+ // Verify user code from verification page using browser
+ openVerificationPage(response.getVerificationUri());
+ verificationPage.assertCurrent();
+ verificationPage.submit(response.getUserCode());
+
+ loginPage.assertCurrent();
+
+ // Do Login
+ oauth.fillLoginForm("device-login", "password");
+
+ // Consent
+ grantPage.assertCurrent();
+ grantPage.assertGrants(OAuthGrantPage.PROFILE_CONSENT_TEXT, OAuthGrantPage.EMAIL_CONSENT_TEXT, OAuthGrantPage.ROLES_CONSENT_TEXT);
+ grantPage.accept();
+
+ verificationPage.assertApprovedPage();
+
+ // 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);
+
+ // Token request from device
+ OAuthClient.AccessTokenResponse tokenResponse = oauth.doDeviceTokenRequest(DEVICE_APP, "secret", response.getDeviceCode());
+ assertEquals(400, tokenResponse.getStatusCode());
+ assertEquals(OAuthErrorException.INVALID_GRANT, tokenResponse.getError());
+ assertEquals("Exception thrown intentionally", tokenResponse.getErrorDescription());
+ }
+
+ private void openVerificationPage(String verificationUri) {
+ driver.navigate().to(verificationUri);
+ }
+
private void checkMtlsFlow() throws IOException {
// Check login.
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(TEST_USER_NAME, TEST_USER_PASSWORD);