From db6f769695f4dffc75db97672ccabe81d6fb5e92 Mon Sep 17 00:00:00 2001 From: Mauro Mugnaini Date: Thu, 19 May 2022 19:40:09 +0200 Subject: [PATCH] Added functions to introspect and verify access tokens (both OIDC and UMA are supported) (#23326) --- CHANGELOG.md | 3 + pom.xml | 2 +- .../keycloak/DefaultKeycloakClient.java | 114 +++++++++++++++++- .../gcube/common/keycloak/KeycloakClient.java | 70 ++++++++++- 4 files changed, 183 insertions(+), 6 deletions(-) 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