KEYCLOAK-18558 Client Policy - Endpoint : support Device Authorization Endpoint
This commit is contained in:
parent
b4fe7bbda2
commit
375e47877e
7 changed files with 259 additions and 1 deletions
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
*/
|
||||
public class DeviceAuthorizationRequestContext implements ClientPolicyContext {
|
||||
|
||||
private final AuthorizationEndpointRequest request;
|
||||
private final MultivaluedMap<String, String> requestParameters;
|
||||
|
||||
public DeviceAuthorizationRequestContext(AuthorizationEndpointRequest request,
|
||||
MultivaluedMap<String, String> requestParameters) {
|
||||
this.request = request;
|
||||
this.requestParameters = requestParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientPolicyEvent getEvent() {
|
||||
return ClientPolicyEvent.DEVICE_AUTHORIZATION_REQUEST;
|
||||
}
|
||||
|
||||
public AuthorizationEndpointRequest getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public MultivaluedMap<String, String> getRequestParameters() {
|
||||
return requestParameters;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
*/
|
||||
public class DeviceTokenRequestContext implements ClientPolicyContext {
|
||||
|
||||
private final OAuth2DeviceCodeModel deviceCodeModel;
|
||||
private final MultivaluedMap<String, String> requestParameters;
|
||||
|
||||
public DeviceTokenRequestContext(OAuth2DeviceCodeModel deviceCodeModel,
|
||||
MultivaluedMap<String, String> requestParameters) {
|
||||
this.deviceCodeModel = deviceCodeModel;
|
||||
this.requestParameters = requestParameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientPolicyEvent getEvent() {
|
||||
return ClientPolicyEvent.DEVICE_AUTHORIZATION_REQUEST;
|
||||
}
|
||||
|
||||
public OAuth2DeviceCodeModel getDeviceCodeModel() {
|
||||
return deviceCodeModel;
|
||||
}
|
||||
|
||||
public MultivaluedMap<String, String> getRequestParameters() {
|
||||
return requestParameters;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -55,6 +55,8 @@ public class TestRaiseExeptionExecutor implements ClientPolicyExecutorProvider<C
|
|||
case BACKCHANNEL_AUTHENTICATION_REQUEST:
|
||||
case BACKCHANNEL_TOKEN_REQUEST:
|
||||
case PUSHED_AUTHORIZATION_REQUEST:
|
||||
case DEVICE_AUTHORIZATION_REQUEST:
|
||||
case DEVICE_TOKEN_REQUEST:
|
||||
return true;
|
||||
default :
|
||||
return false;
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.apache.http.HttpResponse;
|
|||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
|
@ -44,6 +45,7 @@ import org.keycloak.jose.jws.JWSInput;
|
|||
import org.keycloak.models.AdminRoles;
|
||||
import org.keycloak.models.CibaConfig;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.OAuth2DeviceConfig;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
|
@ -88,14 +90,19 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
|||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
|
||||
import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResource;
|
||||
import org.keycloak.testsuite.pages.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.OAuth2DeviceVerificationPage;
|
||||
import org.keycloak.testsuite.pages.OAuthGrantPage;
|
||||
import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource.AuthorizationEndpointRequestObject;
|
||||
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExeptionConditionFactory;
|
||||
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExeptionExecutorFactory;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
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.UserBuilder;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -137,6 +144,7 @@ import static org.keycloak.testsuite.util.ClientPoliciesUtil.createSecureSigning
|
|||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createTestRaiseExeptionConditionConfig;
|
||||
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createFullScopeDisabledExecutorConfig;
|
||||
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
|
||||
*/
|
||||
|
@ -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<RealmRepresentation> testRealms) {
|
||||
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||
|
@ -179,6 +200,30 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
|
|||
|
||||
realm.setUsers(users);
|
||||
|
||||
List<ClientRepresentation> 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);
|
||||
|
|
Loading…
Reference in a new issue