diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c4b787..2317f79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
# Changelog for "keycloak-client"
+## [1.2.0-SNAPSHOT]
+- Added OIDC token retrieve for clients [#23076] and UMA token from OIDC token instead for credentials
+
## [v1.1.0]
- Added refresh token facilities for expired tokens (#22515) and some helper methods added.
diff --git a/pom.xml b/pom.xml
index 03320d3..59e477a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
org.gcube.common
keycloak-client
- 1.1.0
+ 1.2.0-SNAPSHOT
diff --git a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java
index cb06ca1..f381bd2 100644
--- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java
+++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java
@@ -39,23 +39,36 @@ public class DefaultKeycloakClient implements KeycloakClient {
logger.debug("Assuring use the rootVO to query the endpoint simple query. Actual scope is: {}", originalScope);
String rootVOScope = "/" + originalScope.split("/")[1];
logger.debug("Setting rootVO scope into provider as: {}", rootVOScope);
- ScopeProvider.instance.set(rootVOScope);
- logger.debug("Creating simple query");
- SimpleQuery query = queryFor(ServiceEndpoint.class);
- query.addCondition(
- String.format("$resource/Profile/Category/text() eq '%s'", CATEGORY))
- .addCondition(String.format("$resource/Profile/Name/text() eq '%s'", NAME))
- .setResult(String.format("$resource/Profile/AccessPoint[Description/text() eq '%s']", DESCRIPTION));
+ List accessPoints = null;
- logger.debug("Creating client for AccessPoint");
- DiscoveryClient client = clientFor(AccessPoint.class);
+ // trying to be thread safe at least for these calls
+ synchronized (ScopeProvider.instance) {
+ boolean scopeModified = false;
+ if (!ScopeProvider.instance.get().equals(rootVOScope)) {
+ logger.debug("Overriding scope in the provider with rootVO scope : {}", rootVOScope);
+ ScopeProvider.instance.set(rootVOScope);
+ scopeModified = true;
+ }
- logger.trace("Submitting query: {}", query);
- List accessPoints = client.submit(query);
+ logger.debug("Creating simple query");
+ SimpleQuery query = queryFor(ServiceEndpoint.class);
+ query.addCondition(
+ String.format("$resource/Profile/Category/text() eq '%s'", CATEGORY))
+ .addCondition(String.format("$resource/Profile/Name/text() eq '%s'", NAME))
+ .setResult(String.format("$resource/Profile/AccessPoint[Description/text() eq '%s']", DESCRIPTION));
- logger.debug("Restting scope into provider to the original value: {}", originalScope);
- ScopeProvider.instance.set(originalScope);
+ logger.debug("Creating client for AccessPoint");
+ DiscoveryClient client = clientFor(AccessPoint.class);
+
+ logger.trace("Submitting query: {}", query);
+ accessPoints = client.submit(query);
+
+ if (scopeModified) {
+ logger.debug("Resetting scope into provider to the original value: {}", originalScope);
+ ScopeProvider.instance.set(originalScope);
+ }
+ }
if (accessPoints.size() == 0) {
throw new KeycloakClientException("Service endpoint not found");
@@ -71,6 +84,32 @@ public class DefaultKeycloakClient implements KeycloakClient {
}
}
+ @Override
+ public TokenResponse queryOIDCToken(String clientId, String clientSecret) throws KeycloakClientException {
+ return queryOIDCToken(findTokenEndpointURL(), clientId, clientSecret);
+ }
+
+ @Override
+ public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret)
+ throws KeycloakClientException {
+
+ return queryOIDCToken(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret));
+ }
+
+ protected String constructBasicAuthenticationHeader(String clientId, String clientSecret) {
+ return "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
+ }
+
+ @Override
+ public TokenResponse queryOIDCToken(URL tokenURL, String authorization) 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));
+
+ return performRequest(tokenURL, authorization, params);
+ }
+
@Override
public TokenResponse queryUMAToken(String clientId, String clientSecret, List permissions)
throws KeycloakClientException {
@@ -78,6 +117,14 @@ public class DefaultKeycloakClient implements KeycloakClient {
return queryUMAToken(clientId, clientSecret, ScopeProvider.instance.get(), permissions);
}
+ @Override
+ public TokenResponse queryUMAToken(TokenResponse oidcTokenResponse, String audience, List permissions)
+ throws KeycloakClientException {
+
+ return queryUMAToken(findTokenEndpointURL(), constructBeareAuthenticationHeader(oidcTokenResponse), audience,
+ permissions);
+ }
+
@Override
public TokenResponse queryUMAToken(String clientId, String clientSecret, String audience,
List permissions) throws KeycloakClientException {
@@ -85,12 +132,23 @@ public class DefaultKeycloakClient implements KeycloakClient {
return queryUMAToken(findTokenEndpointURL(), clientId, clientSecret, audience, permissions);
}
+ @Override
+ public TokenResponse queryUMAToken(URL tokenURL, TokenResponse oidcTokenResponse, String audience,
+ List permissions) throws KeycloakClientException {
+
+ return queryUMAToken(tokenURL, constructBeareAuthenticationHeader(oidcTokenResponse), audience, permissions);
+ }
+
+ protected String constructBeareAuthenticationHeader(TokenResponse oidcTokenResponse) {
+ return "Bearer " + oidcTokenResponse.getAccessToken();
+ }
+
@Override
public TokenResponse queryUMAToken(URL tokenURL, String clientId, String clientSecret, String audience,
List permissions) throws KeycloakClientException {
return queryUMAToken(tokenURL,
- "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes()),
+ constructBasicAuthenticationHeader(clientId, clientSecret),
audience, permissions);
}
@@ -98,6 +156,38 @@ public class DefaultKeycloakClient implements KeycloakClient {
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 {
+ params.put(AUDIENCE_PARAMETER, Arrays.asList(URLEncoder.encode(checkAudience(audience), "UTF-8")));
+ } catch (UnsupportedEncodingException e) {
+ logger.error("Can't URL encode audience: {}", audience, e);
+ }
+
+ 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, authorization, params);
+ }
+
+ protected TokenResponse performRequest(URL tokenURL, String authorization, Map> params)
+ throws KeycloakClientException {
+
if (tokenURL == null) {
throw new KeycloakClientException("Token URL must be not null");
}
@@ -105,31 +195,9 @@ public class DefaultKeycloakClient implements KeycloakClient {
if (authorization == null || "".equals(authorization)) {
throw new KeycloakClientException("Authorization must be not null nor empty");
}
-
- if (audience == null || "".equals(audience)) {
- throw new KeycloakClientException("Audience must be not null nor empty");
- }
-
- logger.debug("Querying token from Keycloak server with URL: {}", tokenURL);
-
// Constructing request object
GXHTTPStringRequest request;
try {
- Map> params = new HashMap<>();
- params.put(GRANT_TYPE_PARAMETER, Arrays.asList(UMA_TOKEN_GRANT_TYPE));
-
- params.put(AUDIENCE_PARAMETER, Arrays.asList(URLEncoder.encode(checkAudience(audience), "UTF-8")));
-
- 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()));
- }
String queryString = params.entrySet().stream()
.flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v))
diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java
index ba8a3e2..2e06e01 100644
--- a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java
+++ b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java
@@ -24,6 +24,37 @@ public interface KeycloakClient {
*/
URL findTokenEndpointURL() throws KeycloakClientException;
+ /**
+ * Queries an OIDC token from the Keycloak server discovered in the current scope, by using provided clientId and client secret
+ *
+ * @param clientId the client id
+ * @param clientSecret the client secret
+ * @return the issued token as {@link TokenResponse} object
+ * @throws KeycloakClientException if something goes wrong performing the query
+ */
+ TokenResponse queryOIDCToken(String clientId, String clientSecret) throws KeycloakClientException;
+
+ /**
+ * Queries an OIDC token from the Keycloak server, by using provided clientId and client secret.
+ *
+ * @param tokenURL the token endpoint {@link URL} of the Keycloak server
+ * @param clientId the client id
+ * @param clientSecret the client secret
+ * @return the issued token as {@link TokenResponse} object
+ * @throws KeycloakClientException if something goes wrong performing the query
+ */
+ TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret) throws KeycloakClientException;
+
+ /**
+ * Queries an OIDC token from the Keycloak server, by using provided authorization.
+ *
+ * @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)
+ * @return the issued token as {@link TokenResponse} object
+ * @throws KeycloakClientException if something goes wrong performing the query
+ */
+ TokenResponse queryOIDCToken(URL tokenURL, String authorization) 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.
@@ -38,6 +69,20 @@ public interface KeycloakClient {
TokenResponse queryUMAToken(URL tokenURL, String authorization, String audience, List permissions)
throws KeycloakClientException;
+ /**
+ * Queries an UMA token from the Keycloak server, by using access-token provided by the {@link TokenResponse} object
+ * for the given audience (context), in URLEncoded form or not, and optionally a list of permissions.
+ *
+ * @param clientId the client id
+ * @param clientSecret the client secret
+ * @param audience the audience (context) where to request the issuing of the ticket
+ * @param permissions a list of permissions, can be null
+ * @return the issued token as {@link TokenResponse} object
+ * @throws KeycloakClientException if something goes wrong performing the query
+ */
+ TokenResponse queryUMAToken(URL tokenURL, TokenResponse oidcTokenResponse, String audience,
+ List permissions) throws KeycloakClientException;
+
/**
* Queries an UMA token from the Keycloak server, by using provided clientId and client secret for the given audience
* (context), in URLEncoded form or not, and optionally a list of permissions.
@@ -51,7 +96,20 @@ public interface KeycloakClient {
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryUMAToken(URL tokenURL, String clientId, String clientSecret, String audience,
- List permissions)
+ List permissions) throws KeycloakClientException;
+
+ /**
+ * Queries an UMA token from the Keycloak server discovered in the current scope, by using access-token provided by the {@link TokenResponse} object
+ * for the given audience (context), in URLEncoded form or not, and optionally a list of permissions.
+ *
+ * @param clientId the client id
+ * @param clientSecret the client secret
+ * @param audience the audience (context) where to request the issuing of the ticket
+ * @param permissions a list of permissions, can be null
+ * @return the issued token as {@link TokenResponse} object
+ * @throws KeycloakClientException if something goes wrong performing the query
+ */
+ TokenResponse queryUMAToken(TokenResponse oidcTokenResponse, String audience, List permissions)
throws KeycloakClientException;
/**
@@ -70,7 +128,7 @@ public interface KeycloakClient {
/**
* Queries an UMA token from the Keycloak server discovered in the current scope, by using provided clientId and client secret
- * for the current scope audience (context), in URLEncoded form or not, and optionally a list of permissions.
+ * for the current scope as audience (context), in URLEncoded form or not, and optionally a list of permissions.
*
* @param clientId the client id
* @param clientSecret the client secret
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 45212dd..bd73a12 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 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";
public static final String REFRESH_TOKEN_GRANT_TYPE = "refresh_token";