KEYCLOAK-18834 Client Policies : ClientScopesCondition needs to be evaluated on CIBA backchannel authentication request and token request

This commit is contained in:
Takashi Norimatsu 2021-07-23 05:36:28 +09:00 committed by Marek Posolda
parent 036239a901
commit 6436716514
7 changed files with 127 additions and 10 deletions

View file

@ -152,6 +152,7 @@ public class CibaGrantType {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, "Invalid Auth Req ID", Response.Status.BAD_REQUEST);
}
request.setClient(client);
try {
session.clientPolicy().triggerOnEvent(new BackchannelTokenRequestContext(request, formParams));
} catch (ClientPolicyException cpe) {

View file

@ -19,11 +19,7 @@ package org.keycloak.protocol.oidc.grants.ciba.channel;
import javax.crypto.SecretKey;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.keycloak.OAuth2Constants;
import org.keycloak.crypto.Algorithm;

View file

@ -19,6 +19,7 @@ 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.protocol.oidc.grants.ciba.endpoints.request.BackchannelAuthenticationEndpointRequest;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyEvent;
@ -29,11 +30,14 @@ import org.keycloak.services.clientpolicy.ClientPolicyEvent;
public class BackchannelAuthenticationRequestContext implements ClientPolicyContext {
private final BackchannelAuthenticationEndpointRequest request;
private final CIBAAuthenticationRequest parsedRequest;
private final MultivaluedMap<String, String> requestParameters;
public BackchannelAuthenticationRequestContext(BackchannelAuthenticationEndpointRequest request,
CIBAAuthenticationRequest parsedRequest,
MultivaluedMap<String, String> requestParameters) {
this.request = request;
this.parsedRequest = parsedRequest;
this.requestParameters = requestParameters;
}
@ -46,6 +50,10 @@ public class BackchannelAuthenticationRequestContext implements ClientPolicyCont
return request;
}
public CIBAAuthenticationRequest getParsedRequest() {
return parsedRequest;
}
public MultivaluedMap<String, String> getRequestParameters() {
return requestParameters;
}

View file

@ -28,12 +28,12 @@ import org.keycloak.services.clientpolicy.ClientPolicyEvent;
*/
public class BackchannelTokenRequestContext implements ClientPolicyContext {
private final CIBAAuthenticationRequest request;
private final CIBAAuthenticationRequest parsedRequest;
private final MultivaluedMap<String, String> requestParameters;
public BackchannelTokenRequestContext(CIBAAuthenticationRequest request,
public BackchannelTokenRequestContext(CIBAAuthenticationRequest parsedRequest,
MultivaluedMap<String, String> requestParameters) {
this.request = request;
this.parsedRequest = parsedRequest;
this.requestParameters = requestParameters;
}
@ -42,8 +42,8 @@ public class BackchannelTokenRequestContext implements ClientPolicyContext {
return ClientPolicyEvent.BACKCHANNEL_TOKEN_REQUEST;
}
public CIBAAuthenticationRequest getRequest() {
return request;
public CIBAAuthenticationRequest getParsedRequest() {
return parsedRequest;
}
public MultivaluedMap<String, String> getRequestParameters() {

View file

@ -196,7 +196,7 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
extractAdditionalParams(endpointRequest, request);
try {
session.clientPolicy().triggerOnEvent(new BackchannelAuthenticationRequestContext(endpointRequest, params));
session.clientPolicy().triggerOnEvent(new BackchannelAuthenticationRequestContext(endpointRequest, request, params));
} catch (ClientPolicyException cpe) {
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
}

View file

@ -29,6 +29,9 @@ 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.protocol.oidc.grants.ciba.channel.CIBAAuthenticationRequest;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelAuthenticationRequestContext;
import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTokenRequestContext;
import org.keycloak.representations.idm.ClientPolicyConditionConfigurationRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
@ -88,6 +91,12 @@ public class ClientScopesCondition extends AbstractClientPolicyConditionProvider
case TOKEN_REQUEST:
if (isScopeMatched(((TokenRequestContext)context).getParseResult().getClientSession())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case BACKCHANNEL_AUTHENTICATION_REQUEST:
if (isScopeMatched(((BackchannelAuthenticationRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
case BACKCHANNEL_TOKEN_REQUEST:
if (isScopeMatched(((BackchannelTokenRequestContext)context).getParsedRequest())) return ClientPolicyVote.YES;
return ClientPolicyVote.NO;
default:
return ClientPolicyVote.ABSTAIN;
}
@ -103,6 +112,11 @@ public class ClientScopesCondition extends AbstractClientPolicyConditionProvider
return isScopeMatched(request.getScope(), session.getContext().getRealm().getClientByClientId(request.getClientId()));
}
private boolean isScopeMatched(CIBAAuthenticationRequest request) {
if (request == null || request.getClient() == null) return false;
return isScopeMatched(request.getScope(), session.getContext().getRealm().getClientByClientId(request.getClient().getClientId()));
}
private boolean isScopeMatched(String explicitScopes, ClientModel client) {
if (explicitScopes == null) explicitScopes = "";
Collection<String> explicitSpecifiedScopes = new HashSet<>(Arrays.asList(explicitScopes.split(" ")));

View file

@ -38,6 +38,7 @@ import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientRolesConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientScopesConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createClientUpdateContextConditionConfig;
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createSecureCibaAuthenticationRequestSigningAlgorithmExecutorConfig;
@ -93,6 +94,7 @@ import org.keycloak.services.clientpolicy.ClientPolicyEvent;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientRolesConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientScopesConditionFactory;
import org.keycloak.services.clientpolicy.condition.ClientUpdaterContextConditionFactory;
import org.keycloak.services.clientpolicy.executor.ConfidentialClientAcceptExecutorFactory;
import org.keycloak.services.clientpolicy.executor.HolderOfKeyEnforcerExecutorFactory;
@ -1935,6 +1937,102 @@ public class CIBATest extends AbstractClientPoliciesTest {
assertThat(tokenRes.getErrorDescription(), is("invalid client access type"));
}
@Test
public void testClientScopesCondition() throws Exception {
String username = "nutzername-rot";
String bindingMessage = "ThisIsBindingMessage";
Map<String, String> additionalParameters = new HashMap<>();
additionalParameters.put("user_device", "mobile");
String clientConfidentialId = generateSuffixedName("confidential-app");
String clientConfidentialSecret = "app-secret";
String cidConfidential = createClientByAdmin(clientConfidentialId, (ClientRepresentation clientRep) -> {
clientRep.setSecret(clientConfidentialSecret);
clientRep.setStandardFlowEnabled(Boolean.TRUE);
clientRep.setImplicitFlowEnabled(Boolean.TRUE);
clientRep.setPublicClient(Boolean.FALSE);
clientRep.setBearerOnly(Boolean.FALSE);
Map<String, String> 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);
});
oauth.clientId(clientConfidentialId);
oauth.scope("microprofile-jwt");
// register profiles
String json = (new ClientProfilesBuilder()).addProfile(
(new ClientProfileBuilder()).createProfile(PROFILE_NAME, "Het Eerste Profiel")
.addExecutor(TestRaiseExeptionExecutorFactory.PROVIDER_ID, null)
.toRepresentation()
).toString();
updateProfiles(json);
// register policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Het Eerste Beleid", Boolean.TRUE)
.addCondition(ClientScopesConditionFactory.PROVIDER_ID,
createClientScopesConditionConfig(ClientScopesConditionFactory.OPTIONAL, Arrays.asList("microprofile-jwt")))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
// user Backchannel Authentication Request
AuthenticationRequestAcknowledgement response = oauth.doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(400)));
assertThat(response.getError(), is(ClientPolicyEvent.BACKCHANNEL_AUTHENTICATION_REQUEST.name()));
assertThat(response.getErrorDescription(), is("Exception thrown intentionally"));
updatePolicies("{}");
response = oauth.doBackchannelAuthenticationRequest(clientConfidentialId, clientConfidentialSecret, username, bindingMessage, null, additionalParameters);
assertThat(response.getStatusCode(), is(equalTo(200)));
Assert.assertNotNull(response.getAuthReqId());
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Het Eerste Beleid", Boolean.TRUE)
.addCondition(ClientScopesConditionFactory.PROVIDER_ID,
createClientScopesConditionConfig(ClientScopesConditionFactory.OPTIONAL, Arrays.asList("microprofile-jwt")))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
OAuthClient.AccessTokenResponse tokenRes = oauth.doBackchannelAuthenticationTokenRequest(clientConfidentialId, clientConfidentialSecret, response.getAuthReqId());
assertThat(tokenRes.getStatusCode(), is(equalTo(400)));
assertThat(tokenRes.getError(), is(OAuthErrorException.INVALID_GRANT));
assertThat(tokenRes.getErrorDescription(), is("Exception thrown intentionally"));
updatePolicies("{}");
// user Authentication Channel Request
TestAuthenticationChannelRequest testRequest = doAuthenticationChannelRequest(bindingMessage);
AuthenticationChannelRequest authenticationChannelReq = testRequest.getRequest();
assertThat(authenticationChannelReq.getBindingMessage(), is(equalTo(bindingMessage)));
assertThat(authenticationChannelReq.getScope(), is(containsString(OAuth2Constants.SCOPE_OPENID)));
assertThat(authenticationChannelReq.getAdditionalParameters().get("user_device"), is(equalTo("mobile")));
// user Authentication Channel completed
doAuthenticationChannelCallback(testRequest);
tokenRes = oauth.doBackchannelAuthenticationTokenRequest(clientConfidentialId, clientConfidentialSecret, response.getAuthReqId());
assertThat(tokenRes.getStatusCode(), is(equalTo(200)));
AccessToken accessToken = oauth.verifyToken(tokenRes.getAccessToken());
assertThat(accessToken.getIssuedFor(), is(equalTo(clientConfidentialId)));
RefreshToken refreshToken = oauth.parseRefreshToken(tokenRes.getRefreshToken());
assertThat(refreshToken.getIssuedFor(), is(equalTo(clientConfidentialId)));
assertThat(refreshToken.getAudience()[0], is(equalTo(refreshToken.getIssuer())));
IDToken idToken = oauth.verifyIDToken(tokenRes.getIdToken());
assertThat(idToken.getPreferredUsername(), is(equalTo(username)));
assertThat(idToken.getIssuedFor(), is(equalTo(clientConfidentialId)));
assertThat(idToken.getAudience()[0], is(equalTo(idToken.getIssuedFor())));
}
private void testBackchannelAuthenticationFlowNotRegisterSigAlgInAdvanceWithSignedAuthentication(String clientName, boolean useRequestUri, String requestedSigAlg, String sigAlg, int statusCode, String errorDescription) throws Exception {
String clientId = createClientDynamically(clientName, (OIDCClientRepresentation clientRep) -> {
List<String> grantTypes = Optional.ofNullable(clientRep.getGrantTypes()).orElse(new ArrayList<>());