KEYCLOAK-18834 Client Policies : ClientScopesCondition needs to be evaluated on CIBA backchannel authentication request and token request
This commit is contained in:
parent
036239a901
commit
6436716514
7 changed files with 127 additions and 10 deletions
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(" ")));
|
||||
|
|
|
@ -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<>());
|
||||
|
|
Loading…
Reference in a new issue