IntentClientBindCheckExecutor.java
/*
* Copyright 2022 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.executor;
import java.io.IOException;
import java.io.Serializable;
import java.util.Optional;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import org.jboss.logging.Logger;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.representations.ClaimsRepresentation;
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
import org.keycloak.services.clientpolicy.ClientPolicyContext;
import org.keycloak.services.clientpolicy.ClientPolicyException;
import org.keycloak.services.clientpolicy.context.AuthorizationRequestContext;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
*/
public class IntentClientBindCheckExecutor implements ClientPolicyExecutorProvider<IntentClientBindCheckExecutor.Configuration> {
private static final Logger logger = Logger.getLogger(IntentClientBindCheckExecutor.class);
private final KeycloakSession session;
private Configuration configuration;
public IntentClientBindCheckExecutor(KeycloakSession session) {
this.session = session;
}
@Override
public String getProviderId() {
return IntentClientBindCheckExecutorFactory.PROVIDER_ID;
}
@Override
public void setupConfiguration(IntentClientBindCheckExecutor.Configuration config) {
this.configuration = Optional.ofNullable(config).orElse(createDefaultConfiguration());
}
@Override
public Class<Configuration> getExecutorConfigurationClass() {
return Configuration.class;
}
public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation {
@JsonProperty("intent-client-bind-check-endpoint")
protected String intentClientBindCheckEndpoint;
@JsonProperty("intent-name")
protected String intentName;
public String getIntentClientBindCheckEndpoint() {
return intentClientBindCheckEndpoint;
}
public void setIntentClientBindCheckEndpoint(String intentClientBindCheckEndpoint) {
this.intentClientBindCheckEndpoint = intentClientBindCheckEndpoint;
}
public String getIntentName() {
return intentName;
}
public void setIntentName(String intentName) {
this.intentName = intentName;
}
}
@Override
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
switch (context.getEvent()) {
case AUTHORIZATION_REQUEST:
checkIntentClientBind((AuthorizationRequestContext)context);
break;
default:
return;
}
}
private Configuration createDefaultConfiguration() {
Configuration conf = new Configuration();
return conf;
}
private void checkIntentClientBind(AuthorizationRequestContext context) throws ClientPolicyException {
if (!isValidIntentClientBindCheckEndpoint()) {
throw new ClientPolicyException(OAuthErrorException.SERVER_ERROR, "invalid Intent Client Bind Check Endpoint setting");
}
ClientModel client = session.getContext().getClient();
String clientId = client.getClientId();
String intentId = retrieveIntentId(context.getAuthorizationEndpointRequest());
IntentBindCheckRequest request = new IntentBindCheckRequest();
request.setClientId(clientId);
request.setIntentId(intentId);
SimpleHttp simpleHttp = SimpleHttp.doPost(configuration.getIntentClientBindCheckEndpoint(), session)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.json(request);
IntentBindCheckResponse response = null;
try {
response = simpleHttp.asJson(IntentBindCheckResponse.class);
} catch (IOException e) {
logger.warnv("HTTP connection failure: {0}", e);
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "checking intent bound with client failed");
}
if (!response.isBound.booleanValue()) {
logger.tracev("Not Bound: intentName = {0}, intentId = {1}, clientId = {2}", configuration.getIntentName(), intentId, clientId);
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "The intent is not bound with the client");
}
logger.tracev("Bound: intentName = {0}, intentId = {1}, clientId = {2}", configuration.getIntentName(), intentId, clientId);
}
private String retrieveIntentId(AuthorizationEndpointRequest request) throws ClientPolicyException {
String claims = request.getClaims();
if (claims == null || claims.isEmpty()) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "no claim for an intent value in an authorization request");
}
String intentName = configuration.getIntentName();
if (intentName == null || intentName.isEmpty()) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "invalid intent name setting");
}
ClaimsRepresentation claimsRep = null;
try {
claimsRep = JsonSerialization.readValue(claims, ClaimsRepresentation.class);
} catch (IOException e) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "invalid claim for an intent value");
}
if(!claimsRep.isPresent(intentName, ClaimsRepresentation.ClaimContext.ID_TOKEN) || claimsRep.isPresentAsNullClaim(intentName, ClaimsRepresentation.ClaimContext.ID_TOKEN)) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "no claim for an intent value for ID token");
}
ClaimsRepresentation.ClaimValue<String> claimValue = claimsRep.getClaimValue(intentName, ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
if (!claimValue.isEssential()) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "not specifying a claim for an intent as essential claim");
}
String value = claimValue.getValue();
if (value == null) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "invalid intent value");
}
return value;
}
private boolean isValidIntentClientBindCheckEndpoint() {
String endpoint = configuration.getIntentClientBindCheckEndpoint();
if (endpoint == null) return false;
if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) return false;
return true;
}
public static class IntentBindCheckRequest implements Serializable {
@JsonProperty("intent_id")
private String intentId;
@JsonProperty("client_id")
private String clientId;
public String getIntentId() {
return intentId;
}
public void setIntentId(String intentId) {
this.intentId = intentId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
}
public static class IntentBindCheckResponse implements Serializable {
@JsonProperty("is_bound")
private Boolean isBound;
public Boolean getIsBound() {
return isBound;
}
public void setIsBound(Boolean isBound) {
this.isBound = isBound;
}
}
}