package org.gcube.common.keycloak; import static org.gcube.resources.discovery.icclient.ICFactory.clientFor; import static org.gcube.resources.discovery.icclient.ICFactory.queryFor; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.gcube.common.gxrest.request.GXHTTPStringRequest; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import org.gcube.common.keycloak.model.TokenResponse; import org.gcube.common.resources.gcore.ServiceEndpoint; import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint; import org.gcube.common.scope.api.ScopeProvider; import org.gcube.resources.discovery.client.api.DiscoveryClient; import org.gcube.resources.discovery.client.queries.api.SimpleQuery; public class DefaultKeycloakClient implements KeycloakClient { private static final String PERMISSION_PARAMETER = "permission"; private static final String GRANT_TYPE_PARAMETER = "grant_type"; private static final String UMA_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket"; private static final String AUDIENCE_PARAMETER = "audience"; @Override public URL findTokenEndpointURL() throws KeycloakClientException { logger.debug("Creating simple query"); SimpleQuery query = queryFor(ServiceEndpoint.class); query.addCondition( String.format("$resource/Profile/Category/text() eq '%s'", CATEGORY)) .addCondition(String.format("$resource/Profile/Name/text() eq '%s'", NAME)) .setResult(String.format("$resource/Profile/AccessPoint[Description/text() eq '%s']", DESCRIPTION)); logger.debug("Creating client for AccessPoint"); DiscoveryClient client = clientFor(AccessPoint.class); logger.trace("Submitting query: {}", query); List accessPoints = client.submit(query); if (accessPoints.size() == 0) { throw new KeycloakClientException("Service endpoint not found"); } else if (accessPoints.size() > 1) { throw new KeycloakClientException("Found more than one endpoint with query"); } String address = accessPoints.iterator().next().address(); logger.debug("Found address: {}", address); try { return new URL(address); } catch (MalformedURLException e) { throw new KeycloakClientException("Cannot create URL from address: " + address, e); } } @Override public TokenResponse queryUMAToken(String clientId, String clientSecret, List permissions) throws KeycloakClientException { return queryUMAToken(clientId, clientSecret, ScopeProvider.instance.get(), permissions); } @Override public TokenResponse queryUMAToken(String clientId, String clientSecret, String audience, List permissions) throws KeycloakClientException { return queryUMAToken(findTokenEndpointURL(), clientId, clientSecret, audience, permissions); } @Override public TokenResponse queryUMAToken(URL tokenURL, String clientId, String clientSecret, String audience, List permissions) throws KeycloakClientException { return queryUMAToken(tokenURL, "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes()), audience, permissions); } @Override public TokenResponse queryUMAToken(URL tokenURL, String authorization, String audience, List permissions) throws KeycloakClientException { logger.debug("Querying token from Keycloak server with URL: {}", tokenURL); Map> params = new HashMap<>(); params.put(GRANT_TYPE_PARAMETER, Arrays.asList(UMA_TOKEN_GRANT_TYPE)); try { params.put(AUDIENCE_PARAMETER, Arrays.asList(URLEncoder.encode(checkAudience(audience), "UTF-8"))); } catch (UnsupportedEncodingException e) { logger.error("Cannot URL encode 'audience'", e); } if (permissions != null && !permissions.isEmpty()) { params.put( PERMISSION_PARAMETER, permissions.stream().map(s -> { try { return URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { return ""; } }).collect(Collectors.toList())); } // Constructing request object GXHTTPStringRequest request; try { String queryString = params.entrySet().stream() .flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v)) .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); request = GXHTTPStringRequest.newRequest(tokenURL.toString()) .header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString); request.isExternalCall(true); if (authorization != null) { logger.debug("Adding authorization header as: {}", authorization); request = request.header("Authorization", authorization); } } catch (Exception e) { throw new KeycloakClientException("Cannot construct the request object correctly", e); } GXInboundResponse response; try { response = request.post(); } catch (Exception e) { throw new KeycloakClientException("Cannot send request correctly", e); } if (response.isSuccessResponse()) { try { return response.tryConvertStreamedContentFromJson(TokenResponse.class); } catch (Exception e) { throw new KeycloakClientException("Cannot construct token response object correctly", e); } } else { throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(), response.getHeaderFields() .getOrDefault("Content-Type", Collections.singletonList("unknown/unknown")).get(0), response.getMessage()); } } private static String checkAudience(String audience) { if (audience.startsWith("/")) { try { logger.trace("Audience was provided in non URL encoded form, encoding it"); return URLEncoder.encode(audience, "UTF-8"); } catch (UnsupportedEncodingException e) { logger.error("Cannot URL encode 'audience'", e); } } return audience; } }