Added functions to introspect and verify access tokens (both OIDC and UMA are supported) (#23326)
This commit is contained in:
parent
7ab5bd1256
commit
db6f769695
|
@ -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)
|
||||
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
Loading…
Reference in New Issue