KEYCLOAK-14196 Client Policy - Condition : Client - Client Scope

This commit is contained in:
Takashi Norimatsu 2020-11-12 09:45:38 +09:00 committed by Marek Posolda
parent 01be601dbd
commit 244a1b2382
4 changed files with 293 additions and 50 deletions

View file

@ -0,0 +1,115 @@
/*
* Copyright 2020 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.condition;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.services.clientpolicy.AuthorizationRequestContext;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.ClientPolicyLogger;
import org.keycloak.services.clientpolicy.ClientPolicyVote;
import org.keycloak.services.clientpolicy.TokenRequestContext;
public class ClientScopesCondition implements ClientPolicyConditionProvider {
private static final Logger logger = Logger.getLogger(ClientScopesCondition.class);
private final KeycloakSession session;
private final ComponentModel componentModel;
public ClientScopesCondition(KeycloakSession session, ComponentModel componentModel) {
this.session = session;
this.componentModel = componentModel;
}
@Override
public ClientPolicyVote applyPolicy(ClientPolicyContext context) throws ClientPolicyException {
switch (context.getEvent()) {
case AUTHORIZATION_REQUEST:
if (isScopeMatched(((AuthorizationRequestContext)context).getAuthorizationEndpointRequest())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case TOKEN_REQUEST:
if (isScopeMatched(((TokenRequestContext)context).getParseResult().getClientSession())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
default:
return ClientPolicyVote.ABSTAIN;
}
}
@Override
public String getName() {
return componentModel.getName();
}
@Override
public String getProviderId() {
return componentModel.getProviderId();
}
private boolean isScopeMatched(AuthenticatedClientSessionModel clientSession) {
if (clientSession == null) return false;
return isScopeMatched(clientSession.getNote(OAuth2Constants.SCOPE), clientSession.getClient());
}
private boolean isScopeMatched(AuthorizationEndpointRequest request) {
if (request == null) return false;
return isScopeMatched(request.getScope(), session.getContext().getRealm().getClientByClientId(request.getClientId()));
}
private boolean isScopeMatched(String explicitScopes, ClientModel client) {
Collection<String> explicitSpecifiedScopes = new HashSet<>(Arrays.asList(explicitScopes.split(" ")));
Set<String> defaultScopes = client.getClientScopes(true, true).keySet();
Set<String> optionalScopes = client.getClientScopes(false, true).keySet();
List<String> expectedScopes = componentModel.getConfig().get(ClientScopesConditionFactory.SCOPES);
if (logger.isTraceEnabled()) {
explicitSpecifiedScopes.stream().forEach(i -> ClientPolicyLogger.log(logger, " explicit specified client scope = " + i));
defaultScopes.stream().forEach(i -> ClientPolicyLogger.log(logger, " default client scope = " + i));
optionalScopes.stream().forEach(i -> ClientPolicyLogger.log(logger, " optional client scope = " + i));
expectedScopes.stream().forEach(i -> ClientPolicyLogger.log(logger, " expected scope = " + i));
}
boolean isDefaultScope = ClientScopesConditionFactory.DEFAULT.equals(componentModel.getConfig().getFirst(ClientScopesConditionFactory.TYPE));
if (isDefaultScope) {
expectedScopes.retainAll(defaultScopes);
return expectedScopes.isEmpty() ? false : true;
} else {
explicitSpecifiedScopes.retainAll(expectedScopes);
explicitSpecifiedScopes.retainAll(optionalScopes);
if (!explicitSpecifiedScopes.isEmpty()) {
explicitSpecifiedScopes.stream().forEach(i->{ClientPolicyLogger.log(logger, " matched scope = " + i);});
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright 2020 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.condition;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.Config.Scope;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
public class ClientScopesConditionFactory implements ClientPolicyConditionProviderFactory {
public static final String PROVIDER_ID = "clientscopes-condition";
public static final String SCOPES = "scopes";
public static final String TYPE = "type";
public static final String DEFAULT = "Default";
public static final String OPTIONAL = "Optional";
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty(SCOPES, PROVIDER_ID + ".label", PROVIDER_ID + ".tooltip", ProviderConfigProperty.MULTIVALUED_STRING_TYPE, "offline_access");
configProperties.add(property);
property = new ProviderConfigProperty(TYPE, "Scope Type", "Default or Optional", ProviderConfigProperty.LIST_TYPE, OPTIONAL);
configProperties.add(property);
}
@Override
public ClientPolicyConditionProvider create(KeycloakSession session, ComponentModel model) {
return new ClientScopesCondition(session, model);
}
@Override
public void init(Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "It uses the scopes requested or assigned in advance to the client to determine whether the policy is applied to this client.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
}

View file

@ -1,3 +1,4 @@
org.keycloak.services.clientpolicy.condition.ClientUpdateContextConditionFactory
org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory
org.keycloak.services.clientpolicy.condition.ClientIpAddressConditionFactory
org.keycloak.services.clientpolicy.condition.ClientIpAddressConditionFactory
org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory

View file

@ -82,6 +82,7 @@ import org.keycloak.services.clientpolicy.condition.ClientIpAddressConditionFact
import org.keycloak.services.clientpolicy.condition.ClientPolicyConditionProvider;
import org.keycloak.services.clientpolicy.condition.ClientUpdateContextConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory;
import org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider;
import org.keycloak.services.clientpolicy.executor.SecureClientAuthEnforceExecutorFactory;
import org.keycloak.services.clientpolicy.executor.PKCEEnforceExecutorFactory;
@ -326,55 +327,7 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
clientRep.setDefaultRoles(Arrays.asList("sample-client-role").toArray(new String[1]));
});
oauth.clientId(response.getClientId());
String codeVerifier = "1a345A7890123456r8901c3456789012b45K7890l23"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
oauth.nonce("bjapewiziIE083d");
oauth.doLogin(userName, userPassword);
EventRepresentation loginEvent = events.expectLogin().client(response.getClientId()).assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.codeVerifier(codeVerifier);
OAuthClient.AccessTokenResponse res = oauth.doAccessTokenRequest(code, clientSecret);
assertEquals(200, res.getStatusCode());
events.expectCodeToToken(codeId, sessionId).client(response.getClientId()).assertEvent();
AccessToken token = oauth.verifyToken(res.getAccessToken());
String userId = findUserByUsername(adminClient.realm(REALM_NAME), userName).getId();
assertEquals(userId, token.getSubject());
Assert.assertNotEquals(userName, token.getSubject());
assertEquals(sessionId, token.getSessionState());
assertEquals(response.getClientId(), token.getIssuedFor());
String refreshTokenString = res.getRefreshToken();
RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
assertEquals(sessionId, refreshToken.getSessionState());
assertEquals(response.getClientId(), refreshToken.getIssuedFor());
OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(refreshTokenString, clientSecret);
assertEquals(200, refreshResponse.getStatusCode());
AccessToken refreshedToken = oauth.verifyToken(refreshResponse.getAccessToken());
RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(refreshResponse.getRefreshToken());
assertEquals(sessionId, refreshedToken.getSessionState());
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
assertEquals(findUserByUsername(adminClient.realm(REALM_NAME), userName).getId(), refreshedToken.getSubject());
events.expectRefresh(refreshToken.getId(), sessionId).client(response.getClientId()).assertEvent();
doIntrospectAccessToken(refreshResponse, userName, clientId, clientSecret);
doTokenRevoke(refreshResponse.getRefreshToken(), clientId, clientSecret, userId, false);
successfulLoginAndLogoutWithPKCE(response.getClientId(), clientSecret, userName, userPassword);
}
@Test
@ -868,6 +821,45 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
}
}
@Test
public void testClientScopesCondition() throws ClientRegistrationException, ClientPolicyException {
String policyName = "MyPolicy";
createPolicy(policyName, DefaultClientPolicyProviderFactory.PROVIDER_ID, null, null, null);
logger.info("... Created Policy : " + policyName);
createCondition("ClientScopesCondition", ClientScopesConditionFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> {
setConditionClientScopes(provider, new ArrayList<>(Arrays.asList("offline_access", "microprofile-jwt")));
});
registerCondition("ClientScopesCondition", policyName);
logger.info("... Registered Condition : ClientScopesCondition");
createExecutor("PKCEEnforceExecutor", PKCEEnforceExecutorFactory.PROVIDER_ID, null, (ComponentRepresentation provider) -> {
setExecutorAugmentActivate(provider);
});
registerExecutor("PKCEEnforceExecutor", policyName);
logger.info("... Registered Executor : PKCEEnforceExecutor");
String clientAlphaId = "Alpha-App";
String clientAlphaSecret = "secretAlpha";
String cAlphaId = createClientByAdmin(clientAlphaId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientAlphaSecret);
});
try {
oauth.scope("address" + " " + "phone");
successfulLoginAndLogout(clientAlphaId, clientAlphaSecret);
oauth.scope("microprofile-jwt" + " " + "profile");
failLoginByNotFollowingPKCE(clientAlphaId);
successfulLoginAndLogoutWithPKCE(clientAlphaId, clientAlphaSecret, "test-user@localhost", "password");
} catch (Exception e) {
fail();
} finally {
deleteClientByAdmin(cAlphaId);
}
}
private AuthorizationEndpointRequestObject createValidRequestObjectForSecureRequestObjectExecutor(String clientId) throws URISyntaxException {
AuthorizationEndpointRequestObject requestObject = new AuthorizationEndpointRequestObject();
requestObject.id(KeycloakModelUtils.generateId());
@ -990,6 +982,58 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
events.expectLogout(sessionId).client(clientId).clearDetails().assertEvent();
}
private void successfulLoginAndLogoutWithPKCE(String clientId, String clientSecret, String userName, String userPassword) throws Exception {
oauth.clientId(clientId);
String codeVerifier = "1a345A7890123456r8901c3456789012b45K7890l23"; // 43
String codeChallenge = generateS256CodeChallenge(codeVerifier);
oauth.codeChallenge(codeChallenge);
oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
oauth.nonce("bjapewiziIE083d");
oauth.doLogin(userName, userPassword);
EventRepresentation loginEvent = events.expectLogin().client(clientId).assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.codeVerifier(codeVerifier);
OAuthClient.AccessTokenResponse res = oauth.doAccessTokenRequest(code, clientSecret);
assertEquals(200, res.getStatusCode());
events.expectCodeToToken(codeId, sessionId).client(clientId).assertEvent();
AccessToken token = oauth.verifyToken(res.getAccessToken());
String userId = findUserByUsername(adminClient.realm(REALM_NAME), userName).getId();
assertEquals(userId, token.getSubject());
Assert.assertNotEquals(userName, token.getSubject());
assertEquals(sessionId, token.getSessionState());
assertEquals(clientId, token.getIssuedFor());
String refreshTokenString = res.getRefreshToken();
RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
assertEquals(sessionId, refreshToken.getSessionState());
assertEquals(clientId, refreshToken.getIssuedFor());
OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(refreshTokenString, clientSecret);
assertEquals(200, refreshResponse.getStatusCode());
AccessToken refreshedToken = oauth.verifyToken(refreshResponse.getAccessToken());
RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(refreshResponse.getRefreshToken());
assertEquals(sessionId, refreshedToken.getSessionState());
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
assertEquals(findUserByUsername(adminClient.realm(REALM_NAME), userName).getId(), refreshedToken.getSubject());
events.expectRefresh(refreshToken.getId(), sessionId).client(clientId).assertEvent();
doIntrospectAccessToken(refreshResponse, userName, clientId, clientSecret);
doTokenRevoke(refreshResponse.getRefreshToken(), clientId, clientSecret, userId, false);
}
private void failLoginByNotFollowingPKCE(String clientId) {
oauth.clientId(clientId);
oauth.openLoginForm();
@ -1264,6 +1308,10 @@ public class ClientPolicyBasicsTest extends AbstractKeycloakTest {
provider.getConfig().put(ClientIpAddressConditionFactory.IPADDR, clientIpAddresses);
}
private void setConditionClientScopes(ComponentRepresentation provider, List<String> clientScopes) {
provider.getConfig().put(ClientScopesConditionFactory.SCOPES, clientScopes);
}
private void setExecutorAugmentActivate(ComponentRepresentation provider) {
provider.getConfig().putSingle("is-augment", Boolean.TRUE.toString());
}