keycloak-client/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java

332 lines
14 KiB
Java

package org.gcube.common.keycloak;
import static org.gcube.common.keycloak.model.OIDCConstants.AUDIENCE_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.CLIENT_CREDENTIALS_GRANT_TYPE;
import static org.gcube.common.keycloak.model.OIDCConstants.CLIENT_ID_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.CLIENT_SECRET_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.GRANT_TYPE_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.PERMISSION_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.REFRESH_TOKEN_GRANT_TYPE;
import static org.gcube.common.keycloak.model.OIDCConstants.REFRESH_TOKEN_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.TOKEN_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.UMA_TOKEN_GRANT_TYPE;
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.ModelUtils;
import org.gcube.common.keycloak.model.TokenIntrospectionResponse;
import org.gcube.common.keycloak.model.TokenResponse;
public class DefaultKeycloakClient implements KeycloakClient {
@Override
public URL computeIntrospectionEndpointURL(URL tokenEndpointURL) throws KeycloakClientException {
logger.debug("Computing introspection URL starting from token endpoint URL: {}", tokenEndpointURL);
try {
URL introspectionURL = null;
if (tokenEndpointURL.getPath().endsWith("token/")) {
introspectionURL = new URL(tokenEndpointURL, "introspect");
} else {
introspectionURL = new URL(tokenEndpointURL, "token/introspect");
}
logger.debug("Computed introspection URL is: {}", introspectionURL);
return introspectionURL;
} catch (MalformedURLException e) {
throw new KeycloakClientException("Cannot create introspection URL from token URL: " + tokenEndpointURL, e);
}
}
@Override
public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret)
throws KeycloakClientException {
return queryOIDCToken(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret));
}
protected String constructBasicAuthenticationHeader(String clientId, String clientSecret) {
return "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
}
@Override
public TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException {
logger.debug("Querying OIDC token from Keycloak server with URL: {}", tokenURL);
Map<String, List<String>> params = new HashMap<>();
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(CLIENT_CREDENTIALS_GRANT_TYPE));
return performRequest(tokenURL, authorization, params);
}
@Override
public TokenResponse queryUMAToken(URL tokenURL, TokenResponse oidcTokenResponse, String audience,
List<String> permissions) throws KeycloakClientException {
return queryUMAToken(tokenURL, constructBeareAuthenticationHeader(oidcTokenResponse), audience, permissions);
}
protected String constructBeareAuthenticationHeader(TokenResponse oidcTokenResponse) {
return "Bearer " + oidcTokenResponse.getAccessToken();
}
@Override
public TokenResponse queryUMAToken(URL tokenURL, String clientId, String clientSecret, String audience,
List<String> permissions) throws KeycloakClientException {
return queryUMAToken(tokenURL,
constructBasicAuthenticationHeader(clientId, clientSecret),
audience, permissions);
}
@Override
public TokenResponse queryUMAToken(URL tokenURL, String authorization, String audience,
List<String> permissions) throws KeycloakClientException {
if (audience == null || "".equals(audience)) {
throw new KeycloakClientException("Audience must be not null nor empty");
}
logger.debug("Querying UMA token from Keycloak server with URL: {}", tokenURL);
Map<String, List<String>> 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("Can't URL encode audience: {}", 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()));
}
return performRequest(tokenURL, authorization, params);
}
protected TokenResponse performRequest(URL tokenURL, String authorization, Map<String, List<String>> params)
throws KeycloakClientException {
if (tokenURL == null) {
throw new KeycloakClientException("Token URL must be not null");
}
if (authorization == null || "".equals(authorization)) {
throw new KeycloakClientException("Authorization must be not null nor empty");
}
// 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;
}
@Override
public TokenResponse refreshToken(URL tokenURL, TokenResponse tokenResponse) throws KeycloakClientException {
return refreshToken(tokenURL, null, null, tokenResponse);
}
@Override
public TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, TokenResponse tokenResponse)
throws KeycloakClientException {
if (clientId == null) {
logger.debug("Client id not set, trying to get it from access token info");
try {
clientId = ModelUtils.getClientIdFromToken(ModelUtils.getAccessTokenFrom(tokenResponse));
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct access token object from token response", e);
}
}
return refreshToken(tokenURL, clientId, clientSecret, tokenResponse.getRefreshToken());
}
@Override
public TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, String refreshTokenJWTString)
throws KeycloakClientException {
if (tokenURL == null) {
throw new KeycloakClientException("Token URL must be not null");
}
if (clientId == null || "".equals(clientId)) {
throw new KeycloakClientException("Client id must be not null nor empty");
}
if (refreshTokenJWTString == null || "".equals(clientId)) {
throw new KeycloakClientException("Refresh token JWT encoded string must be not null nor empty");
}
logger.debug("Refreshing token from Keycloak server with URL: {}", tokenURL);
// Constructing request object
GXHTTPStringRequest request;
try {
Map<String, String> params = new HashMap<>();
params.put(GRANT_TYPE_PARAMETER, REFRESH_TOKEN_GRANT_TYPE);
params.put(REFRESH_TOKEN_PARAMETER, refreshTokenJWTString);
params.put(CLIENT_ID_PARAMETER, URLEncoder.encode(clientId, "UTF-8"));
if (clientSecret != null && !"".equals(clientSecret)) {
params.put(CLIENT_SECRET_PARAMETER, URLEncoder.encode(clientSecret, "UTF-8"));
}
String queryString = params.entrySet().stream()
.map(p -> p.getKey() + "=" + p.getValue())
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
request = GXHTTPStringRequest.newRequest(tokenURL.toString()).header("Content-Type",
"application/x-www-form-urlencoded").withBody(queryString);
request.isExternalCall(true);
} 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());
}
}
@Override
public TokenIntrospectionResponse introspectAccessToken(URL introspectionURL, String clientId, String clientSecret,
String accessTokenJWTString) throws KeycloakClientException {
if (introspectionURL == null) {
throw new KeycloakClientException("Introspection URL must be not null");
}
if (clientId == null || "".equals(clientId)) {
throw new KeycloakClientException("Client id must be not null nor empty");
}
if (clientSecret == null || "".equals(clientSecret)) {
throw new KeycloakClientException("Client secret must be not null nor empty");
}
logger.debug("Verifying access token against Keycloak server with URL: {}", introspectionURL);
// Constructing request object
GXHTTPStringRequest request;
try {
Map<String, String> params = new HashMap<>();
params.put(TOKEN_PARAMETER, accessTokenJWTString);
String queryString = params.entrySet().stream()
.map(p -> p.getKey() + "=" + p.getValue())
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
request = GXHTTPStringRequest.newRequest(introspectionURL.toString()).header("Content-Type",
"application/x-www-form-urlencoded").withBody(queryString);
request.isExternalCall(true);
request = request.header("Authorization", constructBasicAuthenticationHeader(clientId, clientSecret));
} 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(TokenIntrospectionResponse.class);
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct introspection response object correctly", e);
}
} else {
throw KeycloakClientException.create("Unable to get token introspection response", response.getHTTPCode(),
response.getHeaderFields()
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
response.getMessage());
}
}
@Override
public boolean isAccessTokenVerified(URL introspectionURL, String clientId, String clientSecret,
String accessTokenJWTString) throws KeycloakClientException {
return introspectAccessToken(introspectionURL, clientId, clientSecret, accessTokenJWTString).getActive();
}
}