Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<keycloak.version>8.0.1</keycloak.version>
<java.version>1.8</java.version>
<keycloak.version>9.0.2</keycloak.version>
<java.version>11</java.version>
</properties>

<build>
Expand All @@ -34,11 +34,11 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<version>3.2.0</version>
<configuration>
<archive>
<manifestEntries>
<Dependencies>org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services</Dependencies>
<Dependencies>java.net.http,org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services</Dependencies>
</manifestEntries>
</archive>
</configuration>
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/fr/sii/keycloak/HttpHandler.java
Original file line number Diff line number Diff line change
@@ -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<String, String> headers, Map<String, String> queryParameters, Map<String, String> formParameters) {
HttpResponse<String> 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<String> getResponse(String baseUrl, String contentType, Map<String, String> headers, Map<String, String> queryParameters, Map<String, String> 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);
}
}
}
172 changes: 96 additions & 76 deletions src/main/java/fr/sii/keycloak/JsonRemoteClaim.java
Original file line number Diff line number Diff line change
@@ -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;

/**
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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);
}

Expand All @@ -139,103 +176,86 @@ 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<String, String> getQueryParameters(ProtocolMapperModel mappingModel, UserSessionModel userSession) {
private Map<String, String> 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<String, String> formattedParameters = buildMapFromStringConfig(configuredParameter);
final Map<String, String> 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
if (sendUsername) {
formattedParameters.put("username", userSession.getLoginUsername());
}

// Get custom user attributes
if (configuredUserAttributes != null && !"".equals(configuredUserAttributes.trim())) {
List<String> userAttributes = Arrays.asList(configuredUserAttributes.trim().split("&"));
userAttributes.forEach(attribute -> formattedParameters.put(attribute, userSession.getUser().getFirstAttribute(attribute)));
}
return formattedParameters;
}

private Map<String, String> 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<String, String> buildMapFromStringConfig(String config) {
final Map<String, String> 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<String, String> 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<String, String> parameters = getQueryParameters(mappingModel, userSession);
Map<String, String> parameters = new HashMap<>();
// Get headers
Map<String, String> headers = getheaders(mappingModel, userSession);
Map<String, String> 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<String, String> param : parameters.entrySet()) {
target = target.queryParam(param.getKey(), param.getValue());
}
Invocation.Builder builder = target.request(MediaType.APPLICATION_JSON);
// Build headers
for (Map.Entry<String, String> 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<String, String> 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<String, String> parameters = getQueryParameters(mappingModel, userSession, clientSessionCtx);
// Get headers
Map<String, String> headers = getheaders(mappingModel, userSession);

// Call remote service
String baseUrl = mappingModel.getConfig().get(REMOTE_URL);
return HttpHandler.getJsonNode(baseUrl, MediaType.APPLICATION_JSON, headers, parameters, null);
}
}
}
41 changes: 41 additions & 0 deletions src/main/java/fr/sii/keycloak/Utils.java
Original file line number Diff line number Diff line change
@@ -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<String, String> buildMapFromStringConfig(String config) {
final Map<String, String> 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<String, String> 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());
}
}