diff --git a/pom.xml b/pom.xml
index 9e30674..7201f7e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,8 +13,8 @@
UTF-8
- 8.0.1
- 1.8
+ 9.0.2
+ 11
@@ -34,11 +34,11 @@
org.apache.maven.plugins
maven-jar-plugin
- 3.1.1
+ 3.2.0
- org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services
+ java.net.http,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services
diff --git a/src/main/java/fr/sii/keycloak/HttpHandler.java b/src/main/java/fr/sii/keycloak/HttpHandler.java
new file mode 100644
index 0000000..c27a16d
--- /dev/null
+++ b/src/main/java/fr/sii/keycloak/HttpHandler.java
@@ -0,0 +1,59 @@
+package fr.sii.keycloak;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.utils.URIBuilder;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Map;
+
+class HttpHandler {
+
+ static JsonNode getJsonNode(String baseUrl, String contentType, Map headers, Map queryParameters, Map formParameters) {
+ HttpResponse response = getResponse(baseUrl, contentType, headers, queryParameters, formParameters);
+
+ if (response.statusCode() != 200) {
+ throw new JsonRemoteClaimException("Wrong status received for remote claim - Expected: 200, Received: " + response.statusCode(), baseUrl);
+ }
+ try {
+ return new ObjectMapper().readTree(response.body());
+ } catch (IOException e) {
+ throw new JsonRemoteClaimException("Error when parsing response for remote claim", baseUrl, e);
+ }
+ }
+
+ private static HttpResponse getResponse(String baseUrl, String contentType, Map headers, Map queryParameters, Map formParameters) {
+ try {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ URIBuilder uriBuilder = new URIBuilder(baseUrl);
+
+ // Build queryParameters
+ queryParameters.forEach(uriBuilder::setParameter);
+ URI uri = uriBuilder.build();
+
+ HttpRequest.Builder builder = HttpRequest.newBuilder().uri(uri);
+
+ // Build formParameters
+ if (formParameters != null) {
+ builder.POST(Utils.getFormData(formParameters));
+ }
+
+ // Build headers
+ builder.header(HttpHeaders.CONTENT_TYPE , contentType);
+ headers.forEach(builder::header);
+
+ // Call
+ return httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
+ } catch (InterruptedException | IOException e) {
+ throw new JsonRemoteClaimException("Error when accessing remote claim", baseUrl, e);
+ } catch (URISyntaxException e) {
+ throw new JsonRemoteClaimException("Wrong uri syntax ", baseUrl, e);
+ }
+ }
+}
diff --git a/src/main/java/fr/sii/keycloak/JsonRemoteClaim.java b/src/main/java/fr/sii/keycloak/JsonRemoteClaim.java
index 8d53378..6930e0d 100644
--- a/src/main/java/fr/sii/keycloak/JsonRemoteClaim.java
+++ b/src/main/java/fr/sii/keycloak/JsonRemoteClaim.java
@@ -1,21 +1,14 @@
package fr.sii.keycloak;
import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.http.HttpHeaders;
import org.keycloak.models.*;
import org.keycloak.protocol.oidc.mappers.*;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken;
+import org.keycloak.utils.MediaType;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.ClientBuilder;
-import javax.ws.rs.client.Invocation;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.stream.Collectors;
/**
@@ -31,8 +24,12 @@ public class JsonRemoteClaim extends AbstractOIDCProtocolMapper implements OIDCA
private final static String REMOTE_PARAMETERS = "remote.parameters";
private final static String REMOTE_PARAMETERS_USERNAME = "remote.parameters.username";
private final static String REMOTE_PARAMETERS_CLIENTID = "remote.parameters.clientid";
+ private static final String REMOTE_PARAMETERS_USER_ATTRIBUTES = "remote.parameters.user.attributes";
+ private static final String REMOTE_HEADERS_BEARER_TOKEN = "remote.headers.bearer.token";
+ private static final String CLIENT_AUTH_URL = "client.auth.url";
+ private static final String CLIENT_AUTH_ID = "client.auth.id";
+ private static final String CLIENT_AUTH_PASS = "client.auth.pass";
- private static Client client = ClientBuilder.newClient();
/**
* Inner configuration to cache retrieved authorization for multiple tokens
@@ -69,6 +66,14 @@ public class JsonRemoteClaim extends AbstractOIDCProtocolMapper implements OIDCA
property.setDefaultValue("false");
configProperties.add(property);
+ // User attributes
+ property = new ProviderConfigProperty();
+ property.setName(REMOTE_PARAMETERS_USER_ATTRIBUTES);
+ property.setLabel("User attributes");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Send custom user attributes as query parameter. Separate value by '&' sign.");
+ configProperties.add(property);
+
// URL
property = new ProviderConfigProperty();
property.setName(REMOTE_URL);
@@ -92,6 +97,38 @@ public class JsonRemoteClaim extends AbstractOIDCProtocolMapper implements OIDCA
property.setType(ProviderConfigProperty.STRING_TYPE);
property.setHelpText("List of headers to send separated by '&'. Separate header name and value by an equals sign '=', the value can contain equals signs (ex: Authorization=az89d).");
configProperties.add(property);
+
+ // Bearer token
+ property = new ProviderConfigProperty();
+ property.setName(REMOTE_HEADERS_BEARER_TOKEN);
+ property.setLabel("Send bearer token");
+ property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ property.setHelpText("Send the bearer token as auth header.");
+ configProperties.add(property);
+
+ // Client auth url
+ property = new ProviderConfigProperty();
+ property.setName(CLIENT_AUTH_URL);
+ property.setLabel("Client auth URL");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Full URL of the keycloak client auth endpoint.");
+ configProperties.add(property);
+
+ // Client id
+ property = new ProviderConfigProperty();
+ property.setName(CLIENT_AUTH_ID);
+ property.setLabel("Client id");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Client id to create a tech token.");
+ configProperties.add(property);
+
+ // Client password
+ property = new ProviderConfigProperty();
+ property.setName(CLIENT_AUTH_PASS);
+ property.setLabel("Client password");
+ property.setType(ProviderConfigProperty.STRING_TYPE);
+ property.setHelpText("Client password to create a tech token.");
+ configProperties.add(property);
}
@Override
@@ -123,7 +160,7 @@ public String getId() {
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {
JsonNode claims = clientSessionCtx.getAttribute(REMOTE_AUTHORIZATION_ATTR, JsonNode.class);
if (claims == null) {
- claims = getRemoteAuthorizations(mappingModel, userSession);
+ claims = getRemoteAuthorizations(mappingModel, userSession, clientSessionCtx);
clientSessionCtx.setAttribute(REMOTE_AUTHORIZATION_ATTR, claims);
}
@@ -139,26 +176,30 @@ protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSes
*/
@Override
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
- JsonNode claims = getRemoteAuthorizations(mappingModel, userSession);
+ JsonNode claims = getRemoteAuthorizations(mappingModel, userSession, null);
OIDCAttributeMapperHelper.mapClaim(token, mappingModel, claims);
}
- private Map getQueryParameters(ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+ private Map getQueryParameters(ProtocolMapperModel mappingModel, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
final String configuredParameter = mappingModel.getConfig().get(REMOTE_PARAMETERS);
final boolean sendUsername = "true".equals(mappingModel.getConfig().get(REMOTE_PARAMETERS_USERNAME));
final boolean sendClientID = "true".equals(mappingModel.getConfig().get(REMOTE_PARAMETERS_CLIENTID));
+ final String configuredUserAttributes = mappingModel.getConfig().get(REMOTE_PARAMETERS_USER_ATTRIBUTES);
// Get parameters
- final Map formattedParameters = buildMapFromStringConfig(configuredParameter);
+ final Map formattedParameters = Utils.buildMapFromStringConfig(configuredParameter);
// Get client ID
if (sendClientID) {
- String clientID = userSession.getAuthenticatedClientSessions().values().stream()
- .map(AuthenticatedClientSessionModel::getClient)
- .map(ClientModel::getClientId)
- .distinct()
- .collect( Collectors.joining( "," ) );
- formattedParameters.put("client_id", clientID);
+ if (clientSessionCtx != null) {
+ formattedParameters.put("client_id", clientSessionCtx.getClientSession().getClient().getId());
+ } else {
+ formattedParameters.put("client_id", userSession.getAuthenticatedClientSessions().values().stream()
+ .map(AuthenticatedClientSessionModel::getClient)
+ .map(ClientModel::getClientId)
+ .distinct()
+ .collect(Collectors.joining(",")));
+ }
}
// Get username
@@ -166,76 +207,55 @@ private Map getQueryParameters(ProtocolMapperModel mappingModel,
formattedParameters.put("username", userSession.getLoginUsername());
}
+ // Get custom user attributes
+ if (configuredUserAttributes != null && !"".equals(configuredUserAttributes.trim())) {
+ List userAttributes = Arrays.asList(configuredUserAttributes.trim().split("&"));
+ userAttributes.forEach(attribute -> formattedParameters.put(attribute, userSession.getUser().getFirstAttribute(attribute)));
+ }
return formattedParameters;
}
private Map getheaders(ProtocolMapperModel mappingModel, UserSessionModel userSession) {
final String configuredHeaders = mappingModel.getConfig().get(REMOTE_HEADERS);
+ final boolean sendBearerToken = "true".equals(mappingModel.getConfig().get(REMOTE_HEADERS_BEARER_TOKEN));
// Get headers
- return buildMapFromStringConfig(configuredHeaders);
- }
-
- private Map buildMapFromStringConfig(String config) {
- final Map map = new HashMap<>();
-
- //FIXME: using MULTIVALUED_STRING_TYPE would be better but it doesn't seem to work
- if (config != null && !"".equals(config.trim())) {
- String[] configList = config.trim().split("&");
- String[] keyValue;
- for (String configEntry : configList) {
- keyValue = configEntry.split("=", 2);
- if (keyValue.length == 2) {
- map.put(keyValue[0], keyValue[1]);
- }
- }
+ Map stringStringMap = Utils.buildMapFromStringConfig(configuredHeaders);
+ if (sendBearerToken) {
+ String signedRequestToken = getClientToken(mappingModel);
+ stringStringMap.put(HttpHeaders.AUTHORIZATION, "Bearer " + signedRequestToken);
}
-
- return map;
+ return stringStringMap;
}
- private JsonNode getRemoteAuthorizations(ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+ private String getClientToken(ProtocolMapperModel mappingModel) {
// Get parameters
- Map parameters = getQueryParameters(mappingModel, userSession);
+ Map parameters = new HashMap<>();
// Get headers
- Map headers = getheaders(mappingModel, userSession);
+ Map headers = new HashMap<>();
- // Call remote service
- Response response;
- final String url = mappingModel.getConfig().get(REMOTE_URL);
- try {
- WebTarget target = client.target(url);
- // Build parameters
- for (Map.Entry param : parameters.entrySet()) {
- target = target.queryParam(param.getKey(), param.getValue());
- }
- Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON);
- // Build headers
- for (Map.Entry header : headers.entrySet()) {
- builder = builder.header(header.getKey(), header.getValue());
- }
- // Call
- response = builder.get();
- } catch(RuntimeException e) {
- // exceptions are thrown to prevent token from being delivered without all information
- throw new JsonRemoteClaimException("Error when accessing remote claim", url, e);
- }
+ Map formParameters = new HashMap<>();
+ formParameters.put("grant_type", "client_credentials");
+ formParameters.put("client_id", mappingModel.getConfig().get(CLIENT_AUTH_ID));
+ formParameters.put("client_secret", mappingModel.getConfig().get(CLIENT_AUTH_PASS));
- // Check response status
- if (response.getStatus() != 200) {
- response.close();
- throw new JsonRemoteClaimException("Wrong status received for remote claim - Expected: 200, Received: " + response.getStatus(), url);
+ // Call remote service
+ String baseUrl = mappingModel.getConfig().get(CLIENT_AUTH_URL);
+ JsonNode jsonNode = HttpHandler.getJsonNode(baseUrl, MediaType.APPLICATION_FORM_URLENCODED, headers, parameters, formParameters);
+ if (!jsonNode.has("access_token")) {
+ throw new JsonRemoteClaimException("Access token not found", baseUrl);
}
+ return jsonNode.findValue("access_token").asText();
+ }
- // Bind JSON response
- try {
- return response.readEntity(JsonNode.class);
- } catch(RuntimeException e) {
- // exceptions are thrown to prevent token from being delivered without all information
- throw new JsonRemoteClaimException("Error when parsing response for remote claim", url, e);
- } finally {
- response.close();
- }
+ private JsonNode getRemoteAuthorizations(ProtocolMapperModel mappingModel, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
+ // Get parameters
+ Map parameters = getQueryParameters(mappingModel, userSession, clientSessionCtx);
+ // Get headers
+ Map headers = getheaders(mappingModel, userSession);
+ // Call remote service
+ String baseUrl = mappingModel.getConfig().get(REMOTE_URL);
+ return HttpHandler.getJsonNode(baseUrl, MediaType.APPLICATION_JSON, headers, parameters, null);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/fr/sii/keycloak/Utils.java b/src/main/java/fr/sii/keycloak/Utils.java
new file mode 100644
index 0000000..da2716a
--- /dev/null
+++ b/src/main/java/fr/sii/keycloak/Utils.java
@@ -0,0 +1,41 @@
+package fr.sii.keycloak;
+
+import java.net.URLEncoder;
+import java.net.http.HttpRequest;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+class Utils {
+
+ static Map buildMapFromStringConfig(String config) {
+ final Map map = new HashMap<>();
+
+ //FIXME: using MULTIVALUED_STRING_TYPE would be better but it doesn't seem to work
+ if (config != null && !"".equals(config.trim())) {
+ String[] configList = config.trim().split("&");
+ String[] keyValue;
+ for (String configEntry : configList) {
+ keyValue = configEntry.split("=", 2);
+ if (keyValue.length == 2) {
+ map.put(keyValue[0], keyValue[1]);
+ }
+ }
+ }
+
+ return map;
+ }
+
+ static HttpRequest.BodyPublisher getFormData(Map data) {
+ StringBuilder builder = new StringBuilder();
+ data.forEach((key, value) -> {
+ if (builder.length() > 0) {
+ builder.append("&");
+ }
+ builder.append(URLEncoder.encode(key, StandardCharsets.UTF_8));
+ builder.append("=");
+ builder.append(URLEncoder.encode(value, StandardCharsets.UTF_8));
+ });
+ return HttpRequest.BodyPublishers.ofString(builder.toString());
+ }
+}