813 lines
36 KiB
Java
813 lines
36 KiB
Java
package org.gcube.common.keycloak;
|
|
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.ACCESS_TOKEN_TOKEN_TYPE;
|
|
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.OFFLINE_ACCESS_SCOPE;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.PASSWORD_GRANT_TYPE;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.PASSWORD_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.REFRESH_TOKEN_TOKEN_TYPE;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.REQUESTED_TOKEN_TYPE_PARAMETER;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.SCOPE_PARAMETER;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.SUBJECT_TOKEN_PARAMETER;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.SUBJECT_TOKEN_TYPE_PARAMETER;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.TOKEN_EXCHANGE_GRANT_TYPE;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.TOKEN_PARAMETER;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.UMA_TOKEN_GRANT_TYPE;
|
|
import static org.gcube.common.keycloak.model.OIDCConstants.USERNAME_PARAMETER;
|
|
|
|
import java.io.IOException;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.net.MalformedURLException;
|
|
import java.net.URL;
|
|
import java.net.URLDecoder;
|
|
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;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
public class DefaultKeycloakClient implements KeycloakClient {
|
|
|
|
protected static Logger logger = LoggerFactory.getLogger(KeycloakClient.class);
|
|
|
|
protected final static String AUTHORIZATION_HEADER = "Authorization";
|
|
|
|
public static final String DEFAULT_BASE_URL = "https://url.d4science.org/auth/realms/";
|
|
|
|
private String customBaseURL = null;
|
|
|
|
public void setCustomBaseURL(String customBaseURL) {
|
|
if (customBaseURL == null || customBaseURL.endsWith("/")) {
|
|
this.customBaseURL = customBaseURL;
|
|
} else {
|
|
this.customBaseURL = customBaseURL += "/";
|
|
}
|
|
}
|
|
|
|
public String getCustomBaseURL() {
|
|
return customBaseURL;
|
|
}
|
|
|
|
@Override
|
|
public URL getRealmBaseURL(String context) throws KeycloakClientException {
|
|
return getRealmBaseURL(context, DEFAULT_REALM);
|
|
}
|
|
|
|
@Override
|
|
public URL getRealmBaseURL(String context, String realm) throws KeycloakClientException {
|
|
String realmBaseURLString = null;
|
|
if (getCustomBaseURL() != null) {
|
|
realmBaseURLString = getCustomBaseURL() + realm + "/";
|
|
} else {
|
|
realmBaseURLString = DEFAULT_BASE_URL + realm + "/";
|
|
if (!context.startsWith(PROD_ROOT_SCOPE)) {
|
|
String root = checkContext(context).split("/")[1];
|
|
realmBaseURLString = realmBaseURLString.replace("url", "url." + root.replaceAll("\\.", "-"));
|
|
}
|
|
}
|
|
try {
|
|
return new URL(realmBaseURLString);
|
|
} catch (MalformedURLException e) {
|
|
logger.error("Cannot create base URL from string: {}", realmBaseURLString, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static String checkContext(String context) {
|
|
if (!context.startsWith("/")) {
|
|
try {
|
|
logger.trace("Context was provided in URL encoded form, decoding it");
|
|
return URLDecoder.decode(context, "UTF-8");
|
|
} catch (UnsupportedEncodingException e) {
|
|
logger.error("Cannot URL decode 'context'", e);
|
|
}
|
|
}
|
|
return context;
|
|
}
|
|
|
|
@Override
|
|
public URL getTokenEndpointURL(URL realmBaseURL) throws KeycloakClientException {
|
|
logger.debug("Constructing token endpoint URL starting from base URL: {}", realmBaseURL);
|
|
try {
|
|
URL tokenURL = null;
|
|
if (realmBaseURL.getPath().endsWith("/")) {
|
|
tokenURL = new URL(realmBaseURL, OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH);
|
|
} else {
|
|
tokenURL = new URL(realmBaseURL.toString() + "/" + OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH);
|
|
}
|
|
logger.debug("Constructed token URL is: {}", tokenURL);
|
|
return tokenURL;
|
|
} catch (MalformedURLException e) {
|
|
throw new KeycloakClientException("Cannot constructs token URL from base URL: " + realmBaseURL, e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public URL getIntrospectionEndpointURL(URL realmBaseURL) throws KeycloakClientException {
|
|
logger.debug("Constructing introspection URL starting from base URL: {}", realmBaseURL);
|
|
try {
|
|
URL tokenURL = null;
|
|
if (realmBaseURL.getPath().endsWith("/")) {
|
|
tokenURL = new URL(realmBaseURL,
|
|
OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH + "/" + TOKEN_INTROSPECT_URI_PATH);
|
|
} else {
|
|
tokenURL = new URL(realmBaseURL.toString() + "/" + OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH + "/"
|
|
+ TOKEN_INTROSPECT_URI_PATH);
|
|
}
|
|
logger.debug("Constructed introspection URL is: {}", tokenURL);
|
|
return tokenURL;
|
|
} catch (MalformedURLException e) {
|
|
throw new KeycloakClientException("Cannot constructs introspection URL from base URL: " + realmBaseURL, e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public URL getAvatarEndpointURL(URL realmBaseURL) throws KeycloakClientException {
|
|
logger.debug("Constructing token endpoint URL starting from base URL: {}", realmBaseURL);
|
|
try {
|
|
URL tokenURL = null;
|
|
if (realmBaseURL.getPath().endsWith("/")) {
|
|
tokenURL = new URL(realmBaseURL, AVATAR_URI_PATH);
|
|
} else {
|
|
tokenURL = new URL(realmBaseURL.toString() + "/" + AVATAR_URI_PATH);
|
|
}
|
|
logger.debug("Constructed avatar URL is: {}", tokenURL);
|
|
return tokenURL;
|
|
} catch (MalformedURLException e) {
|
|
throw new KeycloakClientException("Cannot constructs avatar URL from base URL: " + realmBaseURL, e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public URL computeIntrospectionEndpointURL(URL tokenEndpointURL) throws KeycloakClientException {
|
|
logger.debug("Computing introspection endpoint URL starting from token endpoint URL: {}", tokenEndpointURL);
|
|
try {
|
|
URL introspectionURL = null;
|
|
if (tokenEndpointURL.getPath().endsWith(TOKEN_URI_PATH + "/")) {
|
|
introspectionURL = new URL(tokenEndpointURL, TOKEN_INTROSPECT_URI_PATH);
|
|
} else {
|
|
introspectionURL = new URL(tokenEndpointURL, TOKEN_URI_PATH + "/" + TOKEN_INTROSPECT_URI_PATH);
|
|
}
|
|
logger.debug("Computed introspection URL is: {}", introspectionURL);
|
|
return introspectionURL;
|
|
} catch (MalformedURLException e) {
|
|
throw new KeycloakClientException("Cannot compute introspection URL from token URL: " + tokenEndpointURL,
|
|
e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret)
|
|
throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenWithContext(context, clientId, clientSecret, null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret,
|
|
String audience) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
|
|
audience);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username,
|
|
String password) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, (String) null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret,
|
|
String username, String password, String audience) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, audience, (Map<String, String>) null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret,
|
|
String username, String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
|
|
username, password, audience, extraHeaders);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret)
|
|
throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenWithContext(tokenURL, clientId, clientSecret, null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret,
|
|
String audience) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret),
|
|
audience);
|
|
}
|
|
|
|
protected static String constructBasicAuthenticationHeader(String clientId, String clientSecret) {
|
|
return "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username,
|
|
String password, String audience) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, username,
|
|
password, audience);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username,
|
|
String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, username,
|
|
password, audience, extraHeaders);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException {
|
|
return queryOIDCTokenWithContext(context, authorization, null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience)
|
|
throws KeycloakClientException {
|
|
return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret,
|
|
String username, String password, String audience) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(tokenURL, clientId, clientSecret, username, password, audience, (Map<String, String>) null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret,
|
|
String username, String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret),
|
|
username, password, audience, extraHeaders);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username,
|
|
String password, String audience) throws KeycloakClientException {
|
|
|
|
return queryOIDCTokenOfUserWithContext(tokenURL, authorization, username, password, audience, (Map<String, String>) null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username,
|
|
String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException {
|
|
|
|
Map<String, List<String>> params = new HashMap<>();
|
|
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(PASSWORD_GRANT_TYPE));
|
|
params.put(USERNAME_PARAMETER, Arrays.asList(username));
|
|
params.put(PASSWORD_PARAMETER, Arrays.asList(password));
|
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
logger.debug("Adding authorization header as: {}", authorization);
|
|
headers.put(AUTHORIZATION_HEADER, authorization);
|
|
if (extraHeaders != null) {
|
|
logger.debug("Adding provided extra headers: {}", extraHeaders);
|
|
headers.putAll(extraHeaders);
|
|
}
|
|
|
|
if (audience != null) {
|
|
logger.debug("Adding d4s context header as: {}", audience);
|
|
headers.put(D4S_CONTEXT_HEADER_NAME, audience);
|
|
}
|
|
|
|
return performRequest(tokenURL, headers, params);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException {
|
|
return queryOIDCTokenWithContext(tokenURL, authorization, null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience)
|
|
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));
|
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
logger.debug("Adding authorization header as: {}", authorization);
|
|
headers.put(AUTHORIZATION_HEADER, authorization);
|
|
|
|
if (audience != null) {
|
|
logger.debug("Adding d4s context header as: {}", audience);
|
|
headers.put(D4S_CONTEXT_HEADER_NAME, audience);
|
|
}
|
|
|
|
return performRequest(tokenURL, headers, params);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryUMAToken(String context, TokenResponse oidcTokenResponse, String audience,
|
|
List<String> permissions) throws KeycloakClientException {
|
|
|
|
return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, audience, permissions);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryUMAToken(URL tokenURL, TokenResponse oidcTokenResponse, String audience,
|
|
List<String> permissions) throws KeycloakClientException {
|
|
|
|
return queryUMAToken(tokenURL, constructBeareAuthenticationHeader(oidcTokenResponse), audience, permissions);
|
|
}
|
|
|
|
protected static String constructBeareAuthenticationHeader(TokenResponse oidcTokenResponse) {
|
|
return "Bearer " + oidcTokenResponse.getAccessToken();
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse queryUMAToken(String context, String clientId, String clientSecret, String audience,
|
|
List<String> permissions) throws KeycloakClientException {
|
|
|
|
return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, audience,
|
|
permissions);
|
|
}
|
|
|
|
@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(String context, String authorization, String audience,
|
|
List<String> permissions) throws KeycloakClientException {
|
|
|
|
return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), authorization, 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 {
|
|
String audienceToSend = URLEncoder.encode(checkAudience(audience), "UTF-8");
|
|
params.put(AUDIENCE_PARAMETER, Arrays.asList(audienceToSend));
|
|
logger.trace("audience is {}", audienceToSend);
|
|
} catch (UnsupportedEncodingException e) {
|
|
logger.error("Can't URL encode audience: {}", audience, e);
|
|
}
|
|
|
|
Map<String, String> headers = new HashMap<>();
|
|
logger.debug("Adding authorization header as: {}", authorization);
|
|
headers.put(AUTHORIZATION_HEADER, authorization);
|
|
|
|
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, headers, params);
|
|
}
|
|
|
|
protected TokenResponse performRequest(URL tokenURL, Map<String, String> headers, Map<String, List<String>> params)
|
|
throws KeycloakClientException {
|
|
|
|
if (tokenURL == null) {
|
|
throw new KeycloakClientException("Token URL must be not null");
|
|
}
|
|
|
|
// 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("");
|
|
|
|
logger.trace("Query string is: {}", queryString);
|
|
|
|
request = GXHTTPStringRequest.newRequest(tokenURL.toString())
|
|
.header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString);
|
|
|
|
safeSetAsExternalCallForOldAPI(request);
|
|
|
|
logger.trace("Adding provided headers: {}", headers);
|
|
for (String headerName : headers.keySet()) {
|
|
request.header(headerName, headers.get(headerName));
|
|
}
|
|
} catch (Exception e) {
|
|
throw new KeycloakClientException("Cannot construct the request object correctly", e);
|
|
}
|
|
|
|
GXInboundResponse response;
|
|
try {
|
|
response = request.post();
|
|
// TODO: Fill a bug ticket for the gxJRS lib for JSON responses in case of not 2XX code (e.g. 403 error with JSON details in this case).
|
|
} 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 {
|
|
String errorBody = "[empty]";
|
|
try {
|
|
errorBody = response.getStreamedContentAsString();
|
|
} catch (IOException e1) {
|
|
// Not interesting case
|
|
}
|
|
throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(),
|
|
response.getHeaderFields()
|
|
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
|
|
errorBody);
|
|
}
|
|
}
|
|
|
|
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(String context, TokenResponse tokenResponse) throws KeycloakClientException {
|
|
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), tokenResponse);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse refreshToken(URL tokenURL, TokenResponse tokenResponse) throws KeycloakClientException {
|
|
return refreshToken(tokenURL, null, null, tokenResponse);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse refreshToken(String context, String clientId, String clientSecret, TokenResponse tokenResponse)
|
|
throws KeycloakClientException {
|
|
|
|
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, 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(String context, String clientId, String clientSecret,
|
|
String refreshTokenJWTString) throws KeycloakClientException {
|
|
|
|
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
|
|
refreshTokenJWTString);
|
|
}
|
|
|
|
@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);
|
|
|
|
safeSetAsExternalCallForOldAPI(request);
|
|
} 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 refresh token", response.getHTTPCode(),
|
|
response.getHeaderFields()
|
|
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
|
|
response.getMessage());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse exchangeTokenForAccessToken(String context, TokenResponse oidcTokenResponse, String clientId,
|
|
String clientSecret, String audience) throws KeycloakClientException {
|
|
|
|
return exchangeTokenForAccessToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, clientId,
|
|
clientSecret, audience);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse exchangeTokenForAccessToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId,
|
|
String clientSecret, String audience) throws KeycloakClientException {
|
|
|
|
return exchangeToken(tokenURL, oidcTokenResponse.getAccessToken(), clientId, clientSecret, audience,
|
|
ACCESS_TOKEN_TOKEN_TYPE, null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse exchangeTokenForRefreshToken(String context, TokenResponse oidcTokenResponse, String clientId,
|
|
String clientSecret, String audience) throws KeycloakClientException {
|
|
|
|
return exchangeTokenForRefreshToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, clientId,
|
|
clientSecret, audience);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse exchangeTokenForRefreshToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId,
|
|
String clientSecret, String audience) throws KeycloakClientException {
|
|
|
|
return exchangeToken(tokenURL, oidcTokenResponse.getAccessToken(), clientId, clientSecret, audience,
|
|
REFRESH_TOKEN_TOKEN_TYPE, null);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse exchangeTokenForOfflineToken(String context, TokenResponse oidcTokenResponse, String clientId,
|
|
String clientSecret, String audience) throws KeycloakClientException {
|
|
|
|
return exchangeTokenForOfflineToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, clientId,
|
|
clientSecret, audience);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse exchangeTokenForOfflineToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId,
|
|
String clientSecret, String audience) throws KeycloakClientException {
|
|
|
|
return exchangeToken(tokenURL, oidcTokenResponse.getAccessToken(), clientId, clientSecret, audience,
|
|
REFRESH_TOKEN_TOKEN_TYPE, OFFLINE_ACCESS_SCOPE);
|
|
}
|
|
|
|
@Override
|
|
public TokenResponse exchangeToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret,
|
|
String audience, String requestedTokenType, String scope) throws KeycloakClientException {
|
|
|
|
if (audience == null || "".equals(audience)) {
|
|
throw new KeycloakClientException("Audience must be not null nor empty");
|
|
}
|
|
|
|
logger.debug("Exchanging token from Keycloak server with URL: {}", tokenURL);
|
|
|
|
Map<String, List<String>> params = new HashMap<>();
|
|
params.put(SUBJECT_TOKEN_PARAMETER, Arrays.asList(oidcAccessToken));
|
|
params.put(CLIENT_ID_PARAMETER, Arrays.asList(clientId));
|
|
params.put(CLIENT_SECRET_PARAMETER, Arrays.asList(clientSecret));
|
|
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(TOKEN_EXCHANGE_GRANT_TYPE));
|
|
params.put(SUBJECT_TOKEN_TYPE_PARAMETER, Arrays.asList(ACCESS_TOKEN_TOKEN_TYPE));
|
|
params.put(REQUESTED_TOKEN_TYPE_PARAMETER, Arrays.asList(requestedTokenType));
|
|
if (scope != null) {
|
|
params.put(SCOPE_PARAMETER, Arrays.asList(scope));
|
|
}
|
|
|
|
try {
|
|
String audienceToSend = URLEncoder.encode(checkAudience(audience), "UTF-8");
|
|
params.put(AUDIENCE_PARAMETER, Arrays.asList(audienceToSend));
|
|
logger.trace("audience is {}", audienceToSend);
|
|
} catch (UnsupportedEncodingException e) {
|
|
logger.error("Can't URL encode audience: {}", audience, e);
|
|
}
|
|
|
|
return performRequest(tokenURL, Collections.emptyMap(), params);
|
|
}
|
|
|
|
/**
|
|
* Queries from the OIDC server an exchanged token by using provided access token, for the given audience (context),
|
|
* in URLEncoded form or not, and optionally a list of permissions.
|
|
*
|
|
* @param tokenUrl the token endpoint {@link URL} of the OIDC server
|
|
* @param authorization the auth token (the access token URLEncoded by the "Bearer " string)
|
|
* @param audience the audience (context) where to request the issuing of the ticket (URLEncoded)
|
|
* @param permissions a list of permissions, can be <code>null</code>
|
|
* @return the issued token
|
|
* @throws OpenIdConnectRESTHelperException if an error occurs (also an unauthorized call), inspect the exception for details
|
|
*/
|
|
|
|
@Override
|
|
public TokenIntrospectionResponse introspectAccessToken(String context, String clientId, String clientSecret,
|
|
String accessTokenJWTString) throws KeycloakClientException {
|
|
|
|
return introspectAccessToken(getIntrospectionEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
|
|
accessTokenJWTString);
|
|
}
|
|
|
|
@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);
|
|
|
|
safeSetAsExternalCallForOldAPI(request);
|
|
|
|
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(String context, String clientId, String clientSecret,
|
|
String accessTokenJWTString) throws KeycloakClientException {
|
|
|
|
return isAccessTokenVerified(getIntrospectionEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
|
|
accessTokenJWTString);
|
|
}
|
|
|
|
@Override
|
|
public boolean isAccessTokenVerified(URL introspectionURL, String clientId, String clientSecret,
|
|
String accessTokenJWTString) throws KeycloakClientException {
|
|
|
|
return introspectAccessToken(introspectionURL, clientId, clientSecret, accessTokenJWTString).getActive();
|
|
}
|
|
|
|
protected void safeSetAsExternalCallForOldAPI(GXHTTPStringRequest request) {
|
|
try {
|
|
logger.trace("Looking for the 'isExternalCall' method in the 'GXHTTPStringRequest' class");
|
|
Method isExetnalCallMethod = request.getClass().getMethod("isExternalCall", boolean.class);
|
|
logger.trace("Method found, is the old gxJRS API. Invoking it with 'true' argument");
|
|
isExetnalCallMethod.invoke(request, true);
|
|
} catch (NoSuchMethodException e) {
|
|
logger.trace("Method not found, is the new gxJRS API");
|
|
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
|
logger.warn("Cannot invoke 'isExternalCall' method via reflection on 'GXHTTPStringRequest' class", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public byte[] getAvatarData(String context, TokenResponse tokenResponse) throws KeycloakClientException {
|
|
return getAvatarData(getAvatarEndpointURL(getRealmBaseURL(context)), tokenResponse);
|
|
}
|
|
|
|
@Override
|
|
public byte[] getAvatarData(URL avatarURL, TokenResponse tokenResponse) throws KeycloakClientException {
|
|
logger.debug("Getting user's avatar from URL: {}", avatarURL);
|
|
try {
|
|
GXHTTPStringRequest request;
|
|
try {
|
|
request = GXHTTPStringRequest.newRequest(avatarURL.toString());
|
|
|
|
safeSetAsExternalCallForOldAPI(request);
|
|
|
|
String authorization = constructBeareAuthenticationHeader(tokenResponse);
|
|
logger.debug("Adding authorization header as: {}", authorization);
|
|
request = request.header("Authorization", authorization);
|
|
request = request.header("Accept", "image/png, image/gif");
|
|
} catch (Exception e) {
|
|
throw new KeycloakClientException("Cannot construct the request object correctly", e);
|
|
}
|
|
|
|
GXInboundResponse response;
|
|
try {
|
|
response = request.get();
|
|
} catch (Exception e) {
|
|
throw new KeycloakClientException("Cannot send request correctly", e);
|
|
}
|
|
if (response.isSuccessResponse()) {
|
|
return response.getStreamedContent();
|
|
} else {
|
|
throw KeycloakClientException.create("Unable to get avatar image data", response.getHTTPCode(),
|
|
response.getHeaderFields()
|
|
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
|
|
response.getMessage());
|
|
}
|
|
} catch (IOException e) {
|
|
throw new KeycloakClientException("Error getting user's avatar data", e);
|
|
}
|
|
}
|
|
|
|
}
|