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.gxhttp.util.ContentUtils; import org.gcube.common.gxrest.request.GXHTTPStringRequest; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; import org.gcube.common.gxrest.response.inbound.JsonUtils; import org.gcube.common.keycloak.model.AccessToken; import org.gcube.common.keycloak.model.JSONWebKeySet; import org.gcube.common.keycloak.model.ModelUtils; import org.gcube.common.keycloak.model.PublishedRealmRepresentation; 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 getJWKEndpointURL(URL realmBaseURL) throws KeycloakClientException { logger.debug("Constructing JWK endpoint URL starting from base URL: {}", realmBaseURL); try { URL jwkURL = null; if (realmBaseURL.getPath().endsWith("/")) { jwkURL = new URL(realmBaseURL, OPEN_ID_URI_PATH + "/" + JWK_URI_PATH); } else { jwkURL = new URL(realmBaseURL.toString() + "/" + OPEN_ID_URI_PATH + "/" + JWK_URI_PATH); } logger.debug("Constructed JWK URL is: {}", jwkURL); return jwkURL; } catch (MalformedURLException e) { throw new KeycloakClientException("Cannot constructs JWK 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 PublishedRealmRepresentation getRealmInfo(URL realmURL) throws KeycloakClientException { try { return JsonUtils.fromJson(ContentUtils.toByteArray(realmURL.openStream()), PublishedRealmRepresentation.class); } catch (Exception e) { throw new KeycloakClientException("Getting realm's info", e); } } @Override public JSONWebKeySet getRealmJSONWebKeySet(URL jwkURL) throws KeycloakClientException { try { return JsonUtils.fromJson(ContentUtils.toByteArray(jwkURL.openStream()), JSONWebKeySet.class); } catch (Exception e) { throw new KeycloakClientException("Getting realm's JWK", e); } } @Override public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret) throws KeycloakClientException { return queryOIDCToken(context, clientId, clientSecret, null); } @Override public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret, Map extraHeaders) throws KeycloakClientException { return queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, extraHeaders); } @Override public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret) throws KeycloakClientException { return queryOIDCToken(tokenURL, clientId, clientSecret, null); } @Override public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenWithContext(tokenURL, clientId, clientSecret, null, extraHeaders); } @Override public TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException { return queryOIDCTokenWithContext(context, authorization, null); } @Override public TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException { return queryOIDCToken(tokenURL, authorization, (Map) null); } @Override public TokenResponse queryOIDCToken(String context, String authorization, Map extraHeaders) throws KeycloakClientException { return queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), authorization, extraHeaders); } @Override public TokenResponse queryOIDCToken(URL tokenURL, String authorization, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenWithContext(tokenURL, authorization, null, extraHeaders); } @Override public TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username, String password) throws KeycloakClientException { return queryOIDCTokenOfUser(context, clientId, clientSecret, username, password, null); } @Override public TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username, String password, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, null, extraHeaders); } @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 queryOIDCTokenWithContext(String context, String authorization, String audience) throws KeycloakClientException { return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience); } @Override public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret, String audience) throws KeycloakClientException { return queryOIDCTokenWithContext(tokenURL, clientId, clientSecret, audience, null); } @Override public TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret, String audience, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, audience, extraHeaders); } @Override public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret, String audience, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret), audience, extraHeaders); } @Override public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience, extraHeaders); } @Override public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience) throws KeycloakClientException { return queryOIDCTokenWithContext(tokenURL, authorization, audience, (Map) null); } protected static String constructBasicAuthenticationHeader(String clientId, String clientSecret) { return "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes()); } @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) null); } @Override public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret, String username, String password, String audience, Map extraHeaders) throws KeycloakClientException { return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, username, password, audience, extraHeaders); } @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 extraHeaders) throws KeycloakClientException { return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, username, password, audience, extraHeaders); } @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) null); } @Override public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret, String username, String password, String audience, Map 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) null); } @Override public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username, String password, String audience, Map extraHeaders) throws KeycloakClientException { Map> 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)); // params.put(SCOPE_PARAMETER, Arrays.asList("openid profile " + OFFLINE_ACCESS_SCOPE)); Map 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 queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience, Map extraHeaders) throws KeycloakClientException { logger.debug("Querying OIDC token from Keycloak server with URL: {}", tokenURL); Map> params = new HashMap<>(); params.put(GRANT_TYPE_PARAMETER, Arrays.asList(CLIENT_CREDENTIALS_GRANT_TYPE)); Map 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 permissions) throws KeycloakClientException { return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, audience, permissions); } @Override public TokenResponse queryUMAToken(URL tokenURL, TokenResponse oidcTokenResponse, String audience, List 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 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 permissions) throws KeycloakClientException { return queryUMAToken(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret), audience, permissions); } @Override public TokenResponse queryUMAToken(String context, String authorization, String audience, List permissions) throws KeycloakClientException { return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience, permissions); } @Override public TokenResponse queryUMAToken(URL tokenURL, String authorization, String audience, List 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> 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 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 headers, Map> params) throws KeycloakClientException { return performRequest(TokenResponse.class, tokenURL, headers, params); } protected T performRequest(Class returnObjectClass, URL url, Map headers, Map> params) throws KeycloakClientException { if (url == null) { throw new KeycloakClientException("Token URL must be not null"); } // Constructing request object GXHTTPStringRequest request; try { String queryString = ""; if (params != null) { queryString = params.entrySet().stream() .flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v)) .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); } else { if (logger.isDebugEnabled()) { logger.debug("Params map is null"); } } logger.trace("Query string is: {}", queryString); request = GXHTTPStringRequest.newRequest(url.toString()) .header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString); safeSetAsExternalCallForOldAPI(request); if (headers != null) { logger.trace("Adding provided headers: {}", headers); for (String headerName : headers.keySet()) { request.header(headerName, headers.get(headerName)); } } else { if (logger.isDebugEnabled()) { logger.debug("HTTP headers map is null"); } } } 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(returnObjectClass); } 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); try { Map> params = new HashMap<>(); params.put(GRANT_TYPE_PARAMETER, Collections.singletonList(REFRESH_TOKEN_GRANT_TYPE)); params.put(REFRESH_TOKEN_PARAMETER, Collections.singletonList(refreshTokenJWTString)); params.put(CLIENT_ID_PARAMETER, Collections.singletonList(URLEncoder.encode(clientId, "UTF-8"))); if (clientSecret != null && !"".equals(clientSecret)) { params.put(CLIENT_SECRET_PARAMETER, Collections.singletonList(URLEncoder.encode(clientSecret, "UTF-8"))); } return performRequest(tokenURL, null, params); } catch (UnsupportedEncodingException e) { throw new KeycloakClientException("Cannot encode parameters", e); } } @Override public TokenResponse exchangeTokenForAccessToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { return exchangeTokenForAccessToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId, clientSecret, audience); } @Override public TokenResponse exchangeTokenForAccessToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, ACCESS_TOKEN_TOKEN_TYPE, null); } @Override public TokenResponse exchangeTokenForRefreshToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { return exchangeTokenForRefreshToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId, clientSecret, audience); } @Override public TokenResponse exchangeTokenForRefreshToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, REFRESH_TOKEN_TOKEN_TYPE, null); } @Override public TokenResponse exchangeTokenForOfflineToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException { return exchangeTokenForOfflineToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId, clientSecret, audience); } @Override public TokenResponse exchangeTokenForOfflineToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException { AccessToken at = null; try { at = ModelUtils.getAccessTokenFrom(oidcAccessToken); } catch (Exception e) { throw new IllegalArgumentException("Impossible to parse the access token as JSON", e); } if (at.getScope().indexOf(OFFLINE_ACCESS_SCOPE) < 0) { logger.info("Token to be exchanged doesn't contain 'offline_token' within scopes"); throw new IllegalArgumentException("Orignal access token doesn't contain the 'offline_token' scope"); } return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, REFRESH_TOKEN_TOKEN_TYPE, OFFLINE_ACCESS_SCOPE); } /** * Queries from the OIDC server an exchanged token by using provided access token, for the given audience (context), * in URLEncoded form or not, * * @param tokenURL the token endpoint {@link URL} of the OIDC server * @param oidcAccessToken the auth token (the access token URLEncoded by the "Bearer " string) * @param clientId the client id * @param clientSecret the client secret * @param audience the audience (context) where to request the issuing of the ticket (URLEncoded) * @param requestedTokenType the token type (e.g. refresh) * @param scope the scope, optional can be null * @return the issued exchanged token * @throws KeycloakClientException if an error occurs, inspect the exception for details */ protected 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> 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, null, params); } @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); return performRequest(TokenIntrospectionResponse.class, introspectionURL, Collections.singletonMap("Authorization", constructBasicAuthenticationHeader(clientId, clientSecret)), Collections.singletonMap(TOKEN_PARAMETER, Collections.singletonList(accessTokenJWTString))); } @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); } } }