Added functions to introspect and verify access tokens (both OIDC and UMA are supported) (#23326)

This commit is contained in:
Mauro Mugnaini 2022-05-19 19:40:09 +02:00
parent 7ab5bd1256
commit db6f769695
4 changed files with 183 additions and 6 deletions

View File

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

View File

@ -13,7 +13,7 @@
<groupId>org.gcube.common</groupId>
<artifactId>keycloak-client</artifactId>
<version>1.2.0</version>
<version>1.3.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>

View File

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

View File

@ -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 <code>token</code> 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 <code>token</code> 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 <code>introspection</code> endpoint {@link URL} starting from the discovered token endpoint it in the current scope provided by {@link ScopeProvider}.
*
* @return the keycloak <code>introspection</code> endpoint URL in the current scope
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL computeIntrospectionEndpointURL() throws KeycloakClientException;
/**
* Compute the keycloak <code>introspection</code> endpoint {@link URL} starting from the provided token endpoint.
*
* @return the keycloak <code>introspection</code> 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 <code>true</code> if the token is valid, <code>false</code> 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 <code>active</code> 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 <code>active</code> 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 <code>true</code> if the token is active, <code>false</code> otherwise
* @throws KeycloakClientException if something goes wrong performing the verification
*/
boolean isAccessTokenVerified(URL introspectionURL, String clientId, String clientSecret, String accessTokenJWTString) throws KeycloakClientException;
}