diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bd9c46..c9f7f86 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"
+## [v1.3.0-SNAPSHOT]
+- Added functions to introspect and verify access tokens (both OIDC and UMA are supported) (#23326).
+
## [v1.2.0]
- Added OIDC token retrieve for clients [#23076] and UMA token from OIDC token as bearer auth, instead of credentials only (basic auth)
diff --git a/pom.xml b/pom.xml
index 68f600e..6c8584b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
org.gcube.common
keycloak-client
- 1.2.0
+ 1.3.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 f381bd2..a298da2 100644
--- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java
+++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java
@@ -1,8 +1,17 @@
package org.gcube.common.keycloak;
+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.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.TOKEN_PARAMETER;
+import static org.gcube.common.keycloak.model.OIDCConstants.UMA_TOKEN_GRANT_TYPE;
import static org.gcube.resources.discovery.icclient.ICFactory.clientFor;
import static org.gcube.resources.discovery.icclient.ICFactory.queryFor;
-import static org.gcube.common.keycloak.model.OIDCConstants.*;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
@@ -19,6 +28,7 @@ import java.util.stream.Collectors;
import org.gcube.common.gxrest.request.GXHTTPStringRequest;
import org.gcube.common.gxrest.response.inbound.GXInboundResponse;
import org.gcube.common.keycloak.model.ModelUtils;
+import org.gcube.common.keycloak.model.TokenIntrospectionResponse;
import org.gcube.common.keycloak.model.TokenResponse;
import org.gcube.common.resources.gcore.ServiceEndpoint;
import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint;
@@ -84,6 +94,28 @@ public class DefaultKeycloakClient implements KeycloakClient {
}
}
+ @Override
+ public URL computeIntrospectionEndpointURL() throws KeycloakClientException {
+ return computeIntrospectionEndpointURL(findTokenEndpointURL());
+ }
+
+ @Override
+ public URL computeIntrospectionEndpointURL(URL tokenEndpointURL) throws KeycloakClientException {
+ logger.debug("Computing introspection URL starting from token endpoint URL: {}", tokenEndpointURL);
+ try {
+ URL introspectionURL = null;
+ if (tokenEndpointURL.getPath().endsWith("token/")) {
+ introspectionURL = new URL(tokenEndpointURL, "introspect");
+ } else {
+ introspectionURL = new URL(tokenEndpointURL, "token/introspect");
+ }
+ logger.debug("Computed introspection URL is: {}", introspectionURL);
+ return introspectionURL;
+ } catch (MalformedURLException e) {
+ throw new KeycloakClientException("Cannot create introspection URL from token URL: " + tokenEndpointURL, e);
+ }
+ }
+
@Override
public TokenResponse queryOIDCToken(String clientId, String clientSecret) throws KeycloakClientException {
return queryOIDCToken(findTokenEndpointURL(), clientId, clientSecret);
@@ -198,7 +230,6 @@ public class DefaultKeycloakClient implements KeycloakClient {
// Constructing request object
GXHTTPStringRequest request;
try {
-
String queryString = params.entrySet().stream()
.flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v))
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
@@ -334,6 +365,7 @@ public class DefaultKeycloakClient implements KeycloakClient {
if (clientSecret != null && !"".equals(clientSecret)) {
params.put(CLIENT_SECRET_PARAMETER, URLEncoder.encode(clientSecret, "UTF-8"));
}
+
String queryString = params.entrySet().stream()
.map(p -> p.getKey() + "=" + p.getValue())
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
@@ -366,4 +398,82 @@ public class DefaultKeycloakClient implements KeycloakClient {
}
}
+ @Override
+ public TokenIntrospectionResponse introspectAccessToken(String clientId, String clientSecret,
+ String accessTokenJWTString) throws KeycloakClientException {
+
+ return introspectAccessToken(computeIntrospectionEndpointURL(), 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);
+
+ // Constructing request object
+ GXHTTPStringRequest request;
+ try {
+ Map params = new HashMap<>();
+ params.put(TOKEN_PARAMETER, accessTokenJWTString);
+
+ String queryString = params.entrySet().stream()
+ .map(p -> p.getKey() + "=" + p.getValue())
+ .reduce((p1, p2) -> p1 + "&" + p2).orElse("");
+
+ request = GXHTTPStringRequest.newRequest(introspectionURL.toString()).header("Content-Type",
+ "application/x-www-form-urlencoded").withBody(queryString);
+
+ request.isExternalCall(true);
+ request = request.header("Authorization", constructBasicAuthenticationHeader(clientId, clientSecret));
+ } catch (Exception e) {
+ throw new KeycloakClientException("Cannot construct the request object correctly", e);
+ }
+
+ GXInboundResponse response;
+ try {
+ response = request.post();
+ } catch (Exception e) {
+ throw new KeycloakClientException("Cannot send request correctly", e);
+ }
+ if (response.isSuccessResponse()) {
+ try {
+ return response.tryConvertStreamedContentFromJson(TokenIntrospectionResponse.class);
+ } catch (Exception e) {
+ throw new KeycloakClientException("Cannot construct introspection response object correctly", e);
+ }
+ } else {
+ throw KeycloakClientException.create("Unable to get token introspection response", response.getHTTPCode(),
+ response.getHeaderFields()
+ .getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
+ response.getMessage());
+ }
+ }
+
+ @Override
+ public boolean isAccessTokenVerified(String clientId, String clientSecret, String accessTokenJWTString)
+ throws KeycloakClientException {
+
+ return isAccessTokenVerified(computeIntrospectionEndpointURL(), clientId, clientSecret, accessTokenJWTString);
+ }
+
+ @Override
+ public boolean isAccessTokenVerified(URL introspectionURL, String clientId, String clientSecret,
+ String accessTokenJWTString) throws KeycloakClientException {
+
+ return introspectAccessToken(introspectionURL, clientId, clientSecret, accessTokenJWTString).isActive();
+ }
+
}
diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java
index 2e06e01..31b411d 100644
--- a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java
+++ b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java
@@ -3,6 +3,7 @@ package org.gcube.common.keycloak;
import java.net.URL;
import java.util.List;
+import org.gcube.common.keycloak.model.TokenIntrospectionResponse;
import org.gcube.common.keycloak.model.TokenResponse;
import org.gcube.common.scope.api.ScopeProvider;
import org.slf4j.Logger;
@@ -16,16 +17,33 @@ public interface KeycloakClient {
String NAME = "IAM";
String DESCRIPTION = "oidc-token endpoint";
+
/**
- * Finds the keycloak endpoint {@link URL} discovering it in the current scope provided by {@link ScopeProvider}
+ * Finds the keycloak token
endpoint {@link URL} discovering it in the current scope provided by {@link ScopeProvider}
*
- * @return the keycloak endpoint URL in the current scope
+ * @return the keycloak token
endpoint URL in the current scope
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL findTokenEndpointURL() throws KeycloakClientException;
/**
- * Queries an OIDC token from the Keycloak server discovered in the current scope, by using provided clientId and client secret
+ * Compute the keycloak introspection
endpoint {@link URL} starting from the discovered token endpoint it in the current scope provided by {@link ScopeProvider}.
+ *
+ * @return the keycloak introspection
endpoint URL in the current scope
+ * @throws KeycloakClientException if something goes wrong discovering the endpoint URL
+ */
+ URL computeIntrospectionEndpointURL() throws KeycloakClientException;
+
+ /**
+ * Compute the keycloak introspection
endpoint {@link URL} starting from the provided token endpoint.
+ *
+ * @return the keycloak introspection
endpoint URL in the current scope
+ * @throws KeycloakClientException if something goes wrong discovering the endpoint URL
+ */
+ URL computeIntrospectionEndpointURL(URL tokenEndpointURL) 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
@@ -260,4 +278,50 @@ public interface KeycloakClient {
TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, String refreshTokenJWTString)
throws KeycloakClientException;
+ /**
+ * Introspects an access token against the Keycloak server discovered in the current scope.
+ *
+ * @param clientId the requestor client id
+ * @param clientSecret the requestor client secret
+ * @param accessTokenJWTString the access token to verify
+ * @return true
if the token is valid, false
otherwise
+ * @throws KeycloakClientException if something goes wrong performing the verification
+ */
+ TokenIntrospectionResponse introspectAccessToken(String clientId, String clientSecret, String accessTokenJWTString) throws KeycloakClientException;
+
+ /**
+ * Introspects an access token against the Keycloak server.
+ *
+ * @param introspectionURL the introspection endpoint {@link URL} of the Keycloak server
+ * @param clientId the requestor client id
+ * @param clientSecret the requestor client secret
+ * @param accessTokenJWTString the access token to verify
+ * @return a {@link TokenIntrospectionResponse} object with the introspection results; in particular, the active
field represents the token validity
+ * @throws KeycloakClientException if something goes wrong performing the verification
+ */
+ TokenIntrospectionResponse introspectAccessToken(URL introspectionURL, String clientId, String clientSecret, String accessTokenJWTString) throws KeycloakClientException;
+
+ /**
+ * Verifies an access token against the Keycloak server discovered in the current scope.
+ *
+ * @param clientId the requestor client id
+ * @param clientSecret the requestor client secret
+ * @param accessTokenJWTString the access token to verify
+ * @return a {@link TokenIntrospectionResponse} object with the introspection results; in particular, the active
field represents the token validity
+ * @throws KeycloakClientException if something goes wrong performing the verification
+ */
+ boolean isAccessTokenVerified(String clientId, String clientSecret, String accessTokenJWTString) throws KeycloakClientException;
+
+ /**
+ * Verifies an access token against the Keycloak server.
+ *
+ * @param introspectionURL the introspection endpoint {@link URL} of the Keycloak server
+ * @param clientId the requestor client id
+ * @param clientSecret the requestor client secret
+ * @param accessTokenJWTString the access token to verify
+ * @return true
if the token is active, false
otherwise
+ * @throws KeycloakClientException if something goes wrong performing the verification
+ */
+ boolean isAccessTokenVerified(URL introspectionURL, String clientId, String clientSecret, String accessTokenJWTString) throws KeycloakClientException;
+
}
\ No newline at end of file