Added support for the user of the D4S mapper that maps/shrink the `aud` to the value requested via `X-D4Science-Context` HTTP header

This commit is contained in:
Mauro Mugnaini 2023-03-23 18:27:24 +01:00
parent 168a1d4b35
commit 5f3e02c6e4
7 changed files with 127 additions and 24 deletions

View File

@ -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.TOKEN_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.UMA_TOKEN_GRANT_TYPE; import static org.gcube.common.keycloak.model.OIDCConstants.UMA_TOKEN_GRANT_TYPE;
import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
@ -35,6 +36,9 @@ public class DefaultKeycloakClient implements KeycloakClient {
protected static Logger logger = LoggerFactory.getLogger(KeycloakClient.class); 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/"; public static final String BASE_URL = "https://url.d4science.org/auth/realms/";
@Override @Override
@ -116,6 +120,13 @@ public class DefaultKeycloakClient implements KeycloakClient {
public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret) public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret)
throws KeycloakClientException { 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); 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) public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret)
throws KeycloakClientException { 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) { protected String constructBasicAuthenticationHeader(String clientId, String clientSecret) {
@ -132,17 +150,37 @@ public class DefaultKeycloakClient implements KeycloakClient {
@Override @Override
public TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException { 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 @Override
public TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException { 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); logger.debug("Querying OIDC token from Keycloak server with URL: {}", tokenURL);
Map<String, List<String>> params = new HashMap<>(); Map<String, List<String>> params = new HashMap<>();
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(CLIENT_CREDENTIALS_GRANT_TYPE)); 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<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 @Override
@ -207,6 +245,10 @@ public class DefaultKeycloakClient implements KeycloakClient {
logger.error("Can't URL encode audience: {}", audience, 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()) { if (permissions != null && !permissions.isEmpty()) {
params.put( params.put(
PERMISSION_PARAMETER, permissions.stream().map(s -> { PERMISSION_PARAMETER, permissions.stream().map(s -> {
@ -218,17 +260,17 @@ public class DefaultKeycloakClient implements KeycloakClient {
}).collect(Collectors.toList())); }).collect(Collectors.toList()));
} }
return performRequest(tokenURL, authorization, params); return performRequest(tokenURL, headers, params);
} }
protected TokenResponse performRequest(URL tokenURL, String authorization, Map<String, List<String>> params) protected TokenResponse performRequest(URL tokenURL, Map<String, String> headers, Map<String, List<String>> params)
throws KeycloakClientException { throws KeycloakClientException {
if (tokenURL == null) { if (tokenURL == null) {
throw new KeycloakClientException("Token URL must be not 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"); throw new KeycloakClientException("Authorization must be not null nor empty");
} }
// Constructing request object // Constructing request object
@ -243,9 +285,9 @@ public class DefaultKeycloakClient implements KeycloakClient {
request = GXHTTPStringRequest.newRequest(tokenURL.toString()) request = GXHTTPStringRequest.newRequest(tokenURL.toString())
.header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString); .header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString);
if (authorization != null) { logger.trace("Adding provided headers: {}", headers);
logger.debug("Adding authorization header as: {}", authorization); for (String headerName : headers.keySet()) {
request = request.header("Authorization", authorization); request.header(headerName, headers.get(headerName));
} }
} catch (Exception e) { } catch (Exception e) {
throw new KeycloakClientException("Cannot construct the request object correctly", 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); throw new KeycloakClientException("Cannot construct token response object correctly", e);
} }
} else { } else {
String errorBody = "[empty]";
try {
errorBody = response.getStreamedContentAsString();
} catch (IOException e1) {
// Not interesting case
}
throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(), throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(),
response.getHeaderFields() response.getHeaderFields()
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0), .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); throw new KeycloakClientException("Cannot construct token response object correctly", e);
} }
} else { } else {
throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(), throw KeycloakClientException.create("Unable to refresh token", response.getHTTPCode(),
response.getHeaderFields() response.getHeaderFields()
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0), .getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
response.getMessage()); response.getMessage());

View File

@ -72,6 +72,18 @@ public interface KeycloakClient {
*/ */
TokenResponse queryOIDCToken(String context, String clientId, String clientSecret) throws KeycloakClientException; 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. <code>/gcube</code> 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. * 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; 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. * 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; 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. <code>/gcube</code> 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. * 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; 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), * 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. * in URLEncoded form or not, and optionally a list of permissions.

View File

@ -56,7 +56,7 @@ public class KeycloakClientException extends Exception {
} }
public boolean hasJSONPayload() { public boolean hasJSONPayload() {
return getContentType().endsWith("json"); return getContentType() != null && getContentType().endsWith("json");
} }
public void setResponseString(String responseString) { public void setResponseString(String responseString) {

View File

@ -68,6 +68,11 @@ public class AccessToken extends IDToken {
this.verifyCaller = required; this.verifyCaller = required;
return this; return this;
} }
@Override
public String toString() {
return getRoles() != null ? getRoles().toString() : null;
}
} }
@JsonProperty("trusted-certs") @JsonProperty("trusted-certs")

View File

@ -43,6 +43,7 @@ public class ModelUtils {
public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse, boolean prettyPrint) public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse, boolean prettyPrint)
throws Exception { throws Exception {
return toJSONString(getAccessTokenFrom(tokenResponse, Object.class), prettyPrint); return toJSONString(getAccessTokenFrom(tokenResponse, Object.class), prettyPrint);
} }

View File

@ -4,6 +4,7 @@ public class OIDCConstants {
public static final String PERMISSION_PARAMETER = "permission"; public static final String PERMISSION_PARAMETER = "permission";
public static final String GRANT_TYPE_PARAMETER = "grant_type"; 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 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 UMA_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
public static final String AUDIENCE_PARAMETER = "audience"; public static final String AUDIENCE_PARAMETER = "audience";

View File

@ -71,6 +71,7 @@ public class TestKeycloakClient {
KeycloakClient client = KeycloakClientFactory.newInstance(); KeycloakClient client = KeycloakClientFactory.newInstance();
URL url = client URL url = client
.computeIntrospectionEndpointURL(client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT))); .computeIntrospectionEndpointURL(client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT)));
Assert.assertNotNull(url); Assert.assertNotNull(url);
Assert.assertTrue(url.getProtocol().equals("https")); Assert.assertTrue(url.getProtocol().equals("https"));
Assert.assertEquals(introspectionURL, url); Assert.assertEquals(introspectionURL, url);
@ -92,12 +93,12 @@ public class TestKeycloakClient {
@Test @Test
public void test12aQueryOIDCToken() throws Exception { 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, oidcTR = KeycloakClientFactory.newInstance().queryOIDCToken(tokenURL, CLIENT_ID,
CLIENT_SECRET); CLIENT_SECRET);
logger.info("*** [1.2] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.2a] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.2] OIDC refresh token: {}", oidcTR.getRefreshToken()); logger.info("*** [1.2a] OIDC refresh token: {}", oidcTR.getRefreshToken());
TestModels.checkTokenResponse(oidcTR); TestModels.checkTokenResponse(oidcTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false);
} }
@ -117,12 +118,12 @@ public class TestKeycloakClient {
@Test @Test
public void test24aQueryUMAToken() throws Exception { 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, umaTR = KeycloakClientFactory.newInstance().queryUMAToken(tokenURL, CLIENT_ID, CLIENT_SECRET,
TEST_AUDIENCE, null); TEST_AUDIENCE, null);
logger.info("*** [2.4] UMA access token: {}", umaTR.getAccessToken()); logger.info("*** [2.4a] UMA access token: {}", umaTR.getAccessToken());
logger.info("*** [2.4] UMA refresh token: {}", umaTR.getRefreshToken()); logger.info("*** [2.4a] UMA refresh token: {}", umaTR.getRefreshToken());
TestModels.checkTokenResponse(umaTR); TestModels.checkTokenResponse(umaTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true);
@ -139,7 +140,7 @@ public class TestKeycloakClient {
@Test @Test
public void test302aIntrospectOIDCAccessToken() throws Exception { 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( TokenIntrospectionResponse tir = KeycloakClientFactory.newInstance().introspectAccessToken(
introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()); introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken());
@ -157,7 +158,7 @@ public class TestKeycloakClient {
@Test @Test
public void test304aIntrospectUMAAccessToken() throws Exception { 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( TokenIntrospectionResponse tir = KeycloakClientFactory.newInstance().introspectAccessToken(
introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken()); introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken());
@ -173,7 +174,7 @@ public class TestKeycloakClient {
@Test @Test
public void test306aOIDCAccessTokenVerification() throws Exception { 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( Assert.assertTrue(KeycloakClientFactory.newInstance().isAccessTokenVerified(
introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken())); introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()));
} }
@ -187,7 +188,7 @@ public class TestKeycloakClient {
@Test @Test
public void test307aOIDCAccessTokenNonVerification() throws Exception { 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( Assert.assertFalse(KeycloakClientFactory.newInstance().isAccessTokenVerified(
introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_OIDC_ACCESS_TOKEN)); introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_OIDC_ACCESS_TOKEN));
} }
@ -201,7 +202,7 @@ public class TestKeycloakClient {
@Test @Test
public void test309aUMAAccessTokenVerification() throws Exception { 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( Assert.assertTrue(KeycloakClientFactory.newInstance().isAccessTokenVerified(
introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken())); introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken()));
} }
@ -215,7 +216,7 @@ public class TestKeycloakClient {
@Test @Test
public void test310aUMAAccessTokenNonVerification() throws Exception { 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( Assert.assertFalse(KeycloakClientFactory.newInstance().isAccessTokenVerified(
introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_UMA_ACCESS_TOKEN)); introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_UMA_ACCESS_TOKEN));
} }