diff --git a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java index 198215e..da67800 100644 --- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java @@ -11,6 +11,7 @@ import static org.gcube.common.keycloak.model.OIDCConstants.REFRESH_TOKEN_PARAME 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.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; @@ -35,6 +36,9 @@ public class DefaultKeycloakClient implements KeycloakClient { protected static Logger logger = LoggerFactory.getLogger(KeycloakClient.class); + protected final static String AUTHORIZATION_HEADER = "Authorization"; + protected final static String D4S_CONTEXT_HEADER_NAME = "X-D4Science-Context"; + public static final String BASE_URL = "https://url.d4science.org/auth/realms/"; @Override @@ -116,6 +120,13 @@ public class DefaultKeycloakClient implements KeycloakClient { 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 queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret); } @@ -123,7 +134,14 @@ public class DefaultKeycloakClient implements KeycloakClient { public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret) throws KeycloakClientException { - return queryOIDCToken(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret)); + 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 String constructBasicAuthenticationHeader(String clientId, String clientSecret) { @@ -132,17 +150,37 @@ public class DefaultKeycloakClient implements KeycloakClient { @Override public TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException { - return queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), authorization); + 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 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> params = new HashMap<>(); params.put(GRANT_TYPE_PARAMETER, Arrays.asList(CLIENT_CREDENTIALS_GRANT_TYPE)); +// params.put(SCOPE_PARAMETER, Arrays.asList("conductor-server")); - return performRequest(tokenURL, authorization, params); + 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 @@ -207,6 +245,10 @@ public class DefaultKeycloakClient implements KeycloakClient { 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 -> { @@ -218,17 +260,17 @@ public class DefaultKeycloakClient implements KeycloakClient { }).collect(Collectors.toList())); } - return performRequest(tokenURL, authorization, params); + return performRequest(tokenURL, headers, params); } - protected TokenResponse performRequest(URL tokenURL, String authorization, Map> params) + protected TokenResponse performRequest(URL tokenURL, Map headers, Map> params) throws KeycloakClientException { if (tokenURL == null) { throw new KeycloakClientException("Token URL must be not null"); } - if (authorization == null || "".equals(authorization)) { + if (!headers.containsKey(AUTHORIZATION_HEADER) || "".equals(headers.get(AUTHORIZATION_HEADER))) { throw new KeycloakClientException("Authorization must be not null nor empty"); } // Constructing request object @@ -243,9 +285,9 @@ public class DefaultKeycloakClient implements KeycloakClient { request = GXHTTPStringRequest.newRequest(tokenURL.toString()) .header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString); - if (authorization != null) { - logger.debug("Adding authorization header as: {}", authorization); - request = request.header("Authorization", authorization); + 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); @@ -264,10 +306,16 @@ public class DefaultKeycloakClient implements KeycloakClient { 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), - response.getMessage()); + errorBody); } } @@ -377,7 +425,7 @@ public class DefaultKeycloakClient implements KeycloakClient { throw new KeycloakClientException("Cannot construct token response object correctly", e); } } else { - throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(), + throw KeycloakClientException.create("Unable to refresh token", response.getHTTPCode(), response.getHeaderFields() .getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0), response.getMessage()); diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java index 315886b..fbef138 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java @@ -72,6 +72,18 @@ public interface KeycloakClient { */ TokenResponse queryOIDCToken(String context, String clientId, String clientSecret) throws KeycloakClientException; + /** + * Queries an OIDC token from the context's Keycloak server, by using provided clientId and client secret, reducing the audience to the requested one. + * + * @param context the context where the Keycloak's is needed (e.g. /gcube for DEV) + * @param clientId the client id + * @param clientSecret the client secret + * @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret, String audience) throws KeycloakClientException; + /** * Queries an OIDC token from the Keycloak server, by using provided clientId and client secret. * @@ -83,6 +95,18 @@ public interface KeycloakClient { */ TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret) throws KeycloakClientException; + /** + * Queries an OIDC token from the Keycloak server, by using provided clientId and client secret, reducing the audience to the requested one. + * + * @param tokenURL the token endpoint {@link URL} of the Keycloak server + * @param clientId the client id + * @param clientSecret the client secret + * @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret, String audience) throws KeycloakClientException; + /** * Queries an OIDC token from the Keycloak server, by using provided authorization. * @@ -94,6 +118,18 @@ public interface KeycloakClient { */ TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException; + /** + * Queries an OIDC token from the Keycloak server, by using provided authorization, reducing the audience to the requested one. + * + * @param context the context where the Keycloak's is needed (e.g. /gcube for DEV) + * @param tokenUrl the token endpoint {@link URL} of the OIDC server + * @param authorization the authorization to be set as header (e.g. a "Basic ...." auth or an encoded JWT access token preceded by the "Bearer " string) + * @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience) throws KeycloakClientException; + /** * Queries an OIDC token from the Keycloak server, by using provided authorization. * @@ -104,6 +140,17 @@ public interface KeycloakClient { */ TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException; + /** + * Queries an OIDC token from the Keycloak server, by using provided authorization, reducing the audience to the requested one. + * + * @param tokenUrl the token endpoint {@link URL} of the OIDC server + * @param authorization the authorization to be set as header (e.g. a "Basic ...." auth or an encoded JWT access token preceded by the "Bearer " string) + * @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience) throws KeycloakClientException; + /** * Queries an UMA token from the Keycloak server, by using provided authorization, for the given audience (context), * in URLEncoded form or not, and optionally a list of permissions. diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java b/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java index 1222769..8aeaf4d 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java @@ -56,7 +56,7 @@ public class KeycloakClientException extends Exception { } public boolean hasJSONPayload() { - return getContentType().endsWith("json"); + return getContentType() != null && getContentType().endsWith("json"); } public void setResponseString(String responseString) { diff --git a/src/main/java/org/gcube/common/keycloak/model/AccessToken.java b/src/main/java/org/gcube/common/keycloak/model/AccessToken.java index e9c79a4..bcab683 100644 --- a/src/main/java/org/gcube/common/keycloak/model/AccessToken.java +++ b/src/main/java/org/gcube/common/keycloak/model/AccessToken.java @@ -68,6 +68,11 @@ public class AccessToken extends IDToken { this.verifyCaller = required; return this; } + + @Override + public String toString() { + return getRoles() != null ? getRoles().toString() : null; + } } @JsonProperty("trusted-certs") diff --git a/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java b/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java index 93b0568..188729e 100644 --- a/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java +++ b/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java @@ -43,6 +43,7 @@ public class ModelUtils { public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse, boolean prettyPrint) throws Exception { + return toJSONString(getAccessTokenFrom(tokenResponse, Object.class), prettyPrint); } diff --git a/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java b/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java index 6b9d25d..cc7963f 100644 --- a/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java +++ b/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java @@ -4,6 +4,7 @@ public class OIDCConstants { public static final String PERMISSION_PARAMETER = "permission"; public static final String GRANT_TYPE_PARAMETER = "grant_type"; + public static final String SCOPE_PARAMETER = "scope"; public static final String CLIENT_CREDENTIALS_GRANT_TYPE = "client_credentials"; public static final String UMA_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket"; public static final String AUDIENCE_PARAMETER = "audience"; diff --git a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java index 8fede0d..d0641fd 100644 --- a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java +++ b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java @@ -71,6 +71,7 @@ public class TestKeycloakClient { KeycloakClient client = KeycloakClientFactory.newInstance(); URL url = client .computeIntrospectionEndpointURL(client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT))); + Assert.assertNotNull(url); Assert.assertTrue(url.getProtocol().equals("https")); Assert.assertEquals(introspectionURL, url); @@ -92,12 +93,12 @@ public class TestKeycloakClient { @Test public void test12aQueryOIDCToken() throws Exception { - logger.info("*** [1.2] Start testing query OIDC token from Keycloak with URL..."); + logger.info("*** [1.2a] Start testing query OIDC token from Keycloak with URL..."); oidcTR = KeycloakClientFactory.newInstance().queryOIDCToken(tokenURL, CLIENT_ID, CLIENT_SECRET); - logger.info("*** [1.2] OIDC access token: {}", oidcTR.getAccessToken()); - logger.info("*** [1.2] OIDC refresh token: {}", oidcTR.getRefreshToken()); + logger.info("*** [1.2a] OIDC access token: {}", oidcTR.getAccessToken()); + logger.info("*** [1.2a] OIDC refresh token: {}", oidcTR.getRefreshToken()); TestModels.checkTokenResponse(oidcTR); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); } @@ -117,12 +118,12 @@ public class TestKeycloakClient { @Test public void test24aQueryUMAToken() throws Exception { - logger.info("*** [2.4] Start testing query UMA token from Keycloak with URL..."); + logger.info("*** [2.4a] Start testing query UMA token from Keycloak with URL..."); umaTR = KeycloakClientFactory.newInstance().queryUMAToken(tokenURL, CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null); - logger.info("*** [2.4] UMA access token: {}", umaTR.getAccessToken()); - logger.info("*** [2.4] UMA refresh token: {}", umaTR.getRefreshToken()); + logger.info("*** [2.4a] UMA access token: {}", umaTR.getAccessToken()); + logger.info("*** [2.4a] UMA refresh token: {}", umaTR.getRefreshToken()); TestModels.checkTokenResponse(umaTR); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true); @@ -139,7 +140,7 @@ public class TestKeycloakClient { @Test public void test302aIntrospectOIDCAccessToken() throws Exception { - logger.info("*** [3.2] Start testing introspect OIDC access token..."); + logger.info("*** [3.2a] Start testing introspect OIDC access token..."); TokenIntrospectionResponse tir = KeycloakClientFactory.newInstance().introspectAccessToken( introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()); @@ -157,7 +158,7 @@ public class TestKeycloakClient { @Test public void test304aIntrospectUMAAccessToken() throws Exception { - logger.info("*** [3.4] Start testing introspect UMA access token..."); + logger.info("*** [3.4a] Start testing introspect UMA access token..."); TokenIntrospectionResponse tir = KeycloakClientFactory.newInstance().introspectAccessToken( introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken()); @@ -173,7 +174,7 @@ public class TestKeycloakClient { @Test public void test306aOIDCAccessTokenVerification() throws Exception { - logger.info("*** [3.6] Start OIDC access token verification..."); + logger.info("*** [3.6a] Start OIDC access token verification..."); Assert.assertTrue(KeycloakClientFactory.newInstance().isAccessTokenVerified( introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken())); } @@ -187,7 +188,7 @@ public class TestKeycloakClient { @Test public void test307aOIDCAccessTokenNonVerification() throws Exception { - logger.info("*** [3.7] Start OIDC access token NON verification..."); + logger.info("*** [3.7a] Start OIDC access token NON verification..."); Assert.assertFalse(KeycloakClientFactory.newInstance().isAccessTokenVerified( introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_OIDC_ACCESS_TOKEN)); } @@ -201,7 +202,7 @@ public class TestKeycloakClient { @Test public void test309aUMAAccessTokenVerification() throws Exception { - logger.info("*** [3.9] Start UMA access token verification..."); + logger.info("*** [3.9a] Start UMA access token verification..."); Assert.assertTrue(KeycloakClientFactory.newInstance().isAccessTokenVerified( introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken())); } @@ -215,7 +216,7 @@ public class TestKeycloakClient { @Test public void test310aUMAAccessTokenNonVerification() throws Exception { - logger.info("*** [3.10] Start UMA access token NON verification..."); + logger.info("*** [3.10a] Start UMA access token NON verification..."); Assert.assertFalse(KeycloakClientFactory.newInstance().isAccessTokenVerified( introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_UMA_ACCESS_TOKEN)); }