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.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<String, List<String>> 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<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
@ -207,6 +245,10 @@ public class DefaultKeycloakClient implements KeycloakClient {
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 -> {
@ -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<String, List<String>> 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");
}
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());

View File

@ -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. <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.
*
@ -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. <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.
*
@ -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.

View File

@ -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) {

View File

@ -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")

View File

@ -43,6 +43,7 @@ public class ModelUtils {
public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse, boolean prettyPrint)
throws Exception {
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 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";

View File

@ -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));
}