KEYCLOAK-14196 Client Policy - Condition : Client - Client Scope
This commit is contained in:
parent
01be601dbd
commit
244a1b2382
4 changed files with 293 additions and 50 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue