Compare commits

...

41 Commits

Author SHA1 Message Date
Mauro Mugnaini 45c52f1d56
Releasing version `2.1.0` 2024-05-07 16:56:05 +02:00
Mauro Mugnaini a4c5de4e9e
Implemented custom deserializer that uses `gcube-jackson-databind` and removes completely the dependency with `jjson-*` deserializers 2024-05-07 16:51:34 +02:00
Mauro Mugnaini e0b165c491
Releasing version `2.1.0` 2024-05-06 18:11:29 +02:00
Mauro Mugnaini 1cbfa034e7
Testing offline token exchange with exception only (scope without specific value) 2024-05-06 18:11:11 +02:00
Mauro Mugnaini 22013667d1
Token exchage for an offline token now raises an IllegalArgumetException if the original token not contains `offline_access` within its scopes, required by the `v24.0.2` version of the Keycloak 2024-05-06 18:10:26 +02:00
Mauro Mugnaini dfb35bad62
Added javadoc for token exchange methods and exchage for an offline token now raises an IllegalArgumetException if the original token not contains `offline_access` within its scopes, required by the `v24.0.2` version of the Keycloak 2024-05-06 18:09:40 +02:00
Mauro Mugnaini eabd708631
Temporary disabling exchange for offline token since with new keycloak original token should be issued with `offline` scope to work and has still to be inroduced the possibility to do it 2024-05-03 12:13:41 +02:00
Mauro Mugnaini 899cf13afd
Temporary disabling exchange for offline token since with new keycloak original token should be issued with `offline` scope to work and has still to be inroduced the possibility to do it 2024-05-03 12:10:07 +02:00
Mauro Mugnaini 49af6590f7
Fixed javadocs 2024-05-03 12:07:42 +02:00
Mauro Mugnaini ca0423cdf2
Changed to `runtime` `jjwt-impl` dependnecy and moved to `jjwt-gson` as `provided` seems to solve the issue #27377 2024-05-03 12:07:19 +02:00
Mauro Mugnaini 027803b7e9
Added also `jackson-core` and `jackson-annotations` to the exclusions of `jjwt-impl` dependency 2024-05-02 18:14:39 +02:00
Mauro Mugnaini 0dbf1c0c95
Added explicit declaration of `jackson-databind `2,)` to `provided`, also added to exclusions of `jjwt-impl` 2024-05-02 11:37:32 +02:00
Mauro Mugnaini 5938bf4af8
Replaced `auth0` lib with `jjwt` by `io.jsonwebtoken` that doesn't require jackson at runtime if not used 2024-04-30 20:13:30 +02:00
Mauro Mugnaini e339be5083
Added support to JWK endpoint and key sets retrieve to take info about configured key algorithms 2024-04-30 18:31:07 +02:00
Mauro Mugnaini 7d98fbaa16
Overloaded methods to disable token expiration, generalized public key generation providing key algorithm and added support of RS384 and RS512 signature algorithms, defaulting to RS256 if not specified 2024-04-30 18:29:21 +02:00
Mauro Mugnaini 8c009b9a8d
Renamed method `isSignatureValid()` to `isValid()` since it tests also other aspects (exipration, not before, etc...). 2024-04-30 12:55:04 +02:00
Mauro Mugnaini 23f387f832
Added JWT digital signature verification by using the RSA public key of the realm on server. Uses `java-jwt` library by Auth0 [#27340] 2024-04-30 11:48:22 +02:00
Mauro Mugnaini 726291ca55
Added custom base URL set via factory (not automatically working cross environments) [#27234]
Better tests for exchange-token features
2024-04-22 17:50:00 +02:00
Mauro Mugnaini 35c913db02
Added `token-exchange` support, also with `offline-token` scope, and methods to add extra headers during the OIDC token requests. 2024-04-19 17:26:40 +02:00
Mauro Mugnaini 86c3887e76 Release of the `v.2.0.0` 2023-07-21 12:45:18 +02:00
Mauro Mugnaini 52d70eda60 Added new `KeycloakClientHelper` class to perform token request for user in one shot and without the need to provide the `clientId` parameter (#25291). Only `context`, `username` and `password` are required. 2023-07-13 13:09:33 +02:00
Mauro Mugnaini 89c5cf04bd Revised code to avoid method invoked twice and double logging 2023-07-13 13:06:24 +02:00
Mauro Mugnaini d5ddbfd067 Added support of password grant flow (corresponding to the now deprecated OAuth2 flow: Resource Owner Password Credentials grant) also for specific context/audience by using the specific D4S mapper. (#25291) 2023-07-11 13:36:23 +02:00
Mauro Mugnaini a71f1725a7 The library now uses with `2.4.0-SNAPSHOT` version of the BOM and safe set `isExternalCall` to `true` with reflection to be compatible with both old and new `gxJRS` APIs 2023-07-10 15:46:48 +02:00
Mauro Mugnaini 00d07f99c6 Added changelog info 2023-03-23 18:39:22 +01:00
Mauro Mugnaini 5f3e02c6e4 Added support for the user of the D4S mapper that maps/shrink the `aud` to the value requested via `X-D4Science-Context` HTTP header 2023-03-23 18:27:24 +01:00
Mauro Mugnaini 168a1d4b35 Added predictive infrastructure URL support based on context (and on context and realm if the target realm is not the default one) and overloaded all methods that take the URL as argument with the context (#23655) 2022-07-15 18:19:35 +02:00
Luca Frosini 9577388a09 Removed uneeded dependency 2022-07-15 16:02:21 +02:00
Luca Frosini 1dba845d31 Merge remote-tracking branch 'origin/smartgears_4'
Conflicts:
	pom.xml
2022-07-15 15:42:12 +02:00
Luca Frosini b689ab8659 Reverted change which has been made in branch 2022-07-15 15:39:35 +02:00
Luca Frosini 4423cf7434 Set the right gcube-bom 2022-07-15 15:28:31 +02:00
Mauro Mugnaini 0adf0f86e5 Fixed typo in `AccessToken` class for `setAccessToken(..)` method (#23654) 2022-07-15 11:21:17 +02:00
Mauro Mugnaini c30cc9e646 Updated to latest BOM 2022-07-15 11:20:53 +02:00
Lucio Lelii 44cc98a724 porting to smartgears 4 2022-06-15 17:57:44 +02:00
Mauro Mugnaini 7ae6a7dcd8 Added methods for `token` and `introspect` endpoint construction starting from realm's base URL and related tests 2022-06-10 13:48:48 +02:00
Mauro Mugnaini 460b080fcd Removed the discovery functionality to be compatible with SmartGears.v4 and moved to the new library `keycloak-client-legacy-is` that will provide the backward compatibility. (#23478) 2022-06-08 19:05:40 +02:00
Mauro Mugnaini d6316c837d Fixed right method use to test `active` field in token introspection response 2022-05-20 10:15:34 +02:00
Mauro Mugnaini 6ebb6c6616 Added tests for new introspect and isVerified functions (both OIDC and UMA are supported) (#23326) 2022-05-20 10:14:34 +02:00
Mauro Mugnaini db6f769695 Added functions to introspect and verify access tokens (both OIDC and UMA are supported) (#23326) 2022-05-19 19:40:09 +02:00
Mauro Mugnaini 7ab5bd1256 New model classes available (from Keycloak's source code) 2022-05-19 19:39:32 +02:00
Mauro Mugnaini 03ea0fae2c Added `token` parameter contants 2022-05-19 19:38:48 +02:00
29 changed files with 2719 additions and 394 deletions

View File

@ -2,6 +2,22 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
# Changelog for "keycloak-client"
## [v2.1.0]
- Added `token-exchange` support, also with `offline-token` scope, and methods to add extra headers during the OIDC token requests (#27099).
- Added custom base URL set via factory (not automatically working cross environments) [#27234].
- Added JWKS server configuration retrieval, realm's info (as `PublishedRealmRepresentation` JSON containing public key) and JWT digital signature verification by using the RSA public key of the realm on server. It uses the `jjwt` library by `io.jsonwebtoken` [#27340]
## [v2.0.0]
- Removed the discovery functionality to be compatible with SmartGears.v4 and moved to the new library `keycloak-client-legacy-is` that will provide the backward compatibility. (#23478).
- Fixed typo in `AccessToken` class for `setAccessToken(..)` method (#23654)
- Added predictive infrastructure URL support based on context (and on context and realm if the target realm is not the default one) and overloaded all methods that take the URL as argument with the context. (#23655)
- Added support for the use of the custom Keycloak's D4S mapper that maps/shrink the `aud` (and optionally also the resource access) to the value requested via `X-D4Science-Context` HTTP header.
- Added support of password grant flow (corresponding to the now deprecated OAuth2 flow: Resource Owner Password Credentials grant) also for specific context/audience by using the specific D4S mapper. (#25291)
- Added new `KeycloakClientHelper` class to perform token request for user in one shot and without the need to provide the `clientId` parameter (#25291). Only `context`, `username` and `password` are required. (#25291)
## [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)

37
pom.xml
View File

@ -7,20 +7,19 @@
<parent>
<artifactId>maven-parent</artifactId>
<groupId>org.gcube.tools</groupId>
<version>1.1.0</version>
<relativePath />
<version>1.2.0</version>
</parent>
<groupId>org.gcube.common</groupId>
<artifactId>keycloak-client</artifactId>
<version>1.2.0</version>
<version>2.1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.gcube.distribution</groupId>
<artifactId>gcube-bom</artifactId>
<version>2.0.1</version>
<version>2.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -28,11 +27,20 @@
</dependencyManagement>
<scm>
<connection>scm:git:https://code-repo.d4science.org/gCubeSystem/${project.artifactId}.git</connection>
<developerConnection>scm:git:https://code-repo.d4science.org/gCubeSystem/${project.artifactId}.git</developerConnection>
<connection>
scm:git:https://code-repo.d4science.org/gCubeSystem/${project.artifactId}.git</connection>
<developerConnection>
scm:git:https://code-repo.d4science.org/gCubeSystem/${project.artifactId}.git</developerConnection>
<url>https://code-repo.d4science.org/gCubeSystem/${project.artifactId}</url>
</scm>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<jjwt.version>0.12.5</jjwt.version>
</properties>
<dependencies>
<dependency>
@ -61,19 +69,16 @@
</dependency>
<dependency>
<groupId>org.gcube.core</groupId>
<artifactId>common-fw-clients</artifactId>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>org.gcube.resources.discovery</groupId>
<artifactId>ic-client</artifactId>
</dependency>
<dependency>
<groupId>org.gcube.core</groupId>
<artifactId>common-scope-maps</artifactId>
<scope>test</scope>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>

View File

@ -1,25 +0,0 @@
package org.gcube.common.keycloak;
import org.gcube.common.clients.fw.plugin.Plugin;
public abstract class AbstractPlugin<S, P> implements Plugin<S, P>, KeycloakClient {
public final String name;
public AbstractPlugin(String name) {
this.name = name;
}
public String serviceClass() {
return CATEGORY;
}
public String serviceName() {
return NAME;
}
public String name() {
return name;
}
}

View File

@ -1,12 +1,34 @@
package org.gcube.common.keycloak;
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 static org.gcube.common.keycloak.model.OIDCConstants.ACCESS_TOKEN_TOKEN_TYPE;
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.OFFLINE_ACCESS_SCOPE;
import static org.gcube.common.keycloak.model.OIDCConstants.PASSWORD_GRANT_TYPE;
import static org.gcube.common.keycloak.model.OIDCConstants.PASSWORD_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.REFRESH_TOKEN_TOKEN_TYPE;
import static org.gcube.common.keycloak.model.OIDCConstants.REQUESTED_TOKEN_TYPE_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.SCOPE_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.SUBJECT_TOKEN_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.SUBJECT_TOKEN_TYPE_PARAMETER;
import static org.gcube.common.keycloak.model.OIDCConstants.TOKEN_EXCHANGE_GRANT_TYPE;
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.common.keycloak.model.OIDCConstants.USERNAME_PARAMETER;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Base64;
@ -16,120 +38,418 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.gcube.common.gxhttp.util.ContentUtils;
import org.gcube.common.gxrest.request.GXHTTPStringRequest;
import org.gcube.common.gxrest.response.inbound.GXInboundResponse;
import org.gcube.common.gxrest.response.inbound.JsonUtils;
import org.gcube.common.keycloak.model.AccessToken;
import org.gcube.common.keycloak.model.JSONWebKeySet;
import org.gcube.common.keycloak.model.ModelUtils;
import org.gcube.common.keycloak.model.PublishedRealmRepresentation;
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;
import org.gcube.common.scope.api.ScopeProvider;
import org.gcube.resources.discovery.client.api.DiscoveryClient;
import org.gcube.resources.discovery.client.queries.api.SimpleQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultKeycloakClient implements KeycloakClient {
protected static Logger logger = LoggerFactory.getLogger(KeycloakClient.class);
protected final static String AUTHORIZATION_HEADER = "Authorization";
public static final String DEFAULT_BASE_URL = "https://url.d4science.org/auth/realms/";
private String customBaseURL = null;
public void setCustomBaseURL(String customBaseURL) {
if (customBaseURL == null || customBaseURL.endsWith("/")) {
this.customBaseURL = customBaseURL;
} else {
this.customBaseURL = customBaseURL += "/";
}
}
public String getCustomBaseURL() {
return customBaseURL;
}
@Override
public URL findTokenEndpointURL() throws KeycloakClientException {
logger.debug("Checking ScopeProvider's scope presence and format");
String originalScope = ScopeProvider.instance.get();
if (originalScope == null || !originalScope.startsWith("/") || originalScope.length() < 2) {
throw new KeycloakClientException(originalScope == null ? "Scope not found in ScopeProvider"
: "Bad scope name found: " + originalScope);
}
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);
public URL getRealmBaseURL(String context) throws KeycloakClientException {
return getRealmBaseURL(context, DEFAULT_REALM);
}
List<AccessPoint> accessPoints = null;
// 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.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("Creating client for AccessPoint");
DiscoveryClient<AccessPoint> 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);
@Override
public URL getRealmBaseURL(String context, String realm) throws KeycloakClientException {
String realmBaseURLString = null;
if (getCustomBaseURL() != null) {
realmBaseURLString = getCustomBaseURL() + realm + "/";
} else {
realmBaseURLString = DEFAULT_BASE_URL + realm + "/";
if (!context.startsWith(PROD_ROOT_SCOPE)) {
String root = checkContext(context).split("/")[1];
realmBaseURLString = realmBaseURLString.replace("url", "url." + root.replaceAll("\\.", "-"));
}
}
if (accessPoints.size() == 0) {
throw new KeycloakClientException("Service endpoint not found");
} else if (accessPoints.size() > 1) {
throw new KeycloakClientException("Found more than one endpoint with query");
}
String address = accessPoints.iterator().next().address();
logger.debug("Found address: {}", address);
try {
return new URL(address);
return new URL(realmBaseURLString);
} catch (MalformedURLException e) {
throw new KeycloakClientException("Cannot create URL from address: " + address, e);
logger.error("Cannot create base URL from string: {}", realmBaseURLString, e);
return null;
}
}
private static String checkContext(String context) {
if (!context.startsWith("/")) {
try {
logger.trace("Context was provided in URL encoded form, decoding it");
return URLDecoder.decode(context, "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.error("Cannot URL decode 'context'", e);
}
}
return context;
}
@Override
public URL getTokenEndpointURL(URL realmBaseURL) throws KeycloakClientException {
logger.debug("Constructing token endpoint URL starting from base URL: {}", realmBaseURL);
try {
URL tokenURL = null;
if (realmBaseURL.getPath().endsWith("/")) {
tokenURL = new URL(realmBaseURL, OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH);
} else {
tokenURL = new URL(realmBaseURL.toString() + "/" + OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH);
}
logger.debug("Constructed token URL is: {}", tokenURL);
return tokenURL;
} catch (MalformedURLException e) {
throw new KeycloakClientException("Cannot constructs token URL from base URL: " + realmBaseURL, e);
}
}
@Override
public TokenResponse queryOIDCToken(String clientId, String clientSecret) throws KeycloakClientException {
return queryOIDCToken(findTokenEndpointURL(), clientId, clientSecret);
public URL getJWKEndpointURL(URL realmBaseURL) throws KeycloakClientException {
logger.debug("Constructing JWK endpoint URL starting from base URL: {}", realmBaseURL);
try {
URL jwkURL = null;
if (realmBaseURL.getPath().endsWith("/")) {
jwkURL = new URL(realmBaseURL, OPEN_ID_URI_PATH + "/" + JWK_URI_PATH);
} else {
jwkURL = new URL(realmBaseURL.toString() + "/" + OPEN_ID_URI_PATH + "/" + JWK_URI_PATH);
}
logger.debug("Constructed JWK URL is: {}", jwkURL);
return jwkURL;
} catch (MalformedURLException e) {
throw new KeycloakClientException("Cannot constructs JWK URL from base URL: " + realmBaseURL, e);
}
}
@Override
public URL getIntrospectionEndpointURL(URL realmBaseURL) throws KeycloakClientException {
logger.debug("Constructing introspection URL starting from base URL: {}", realmBaseURL);
try {
URL tokenURL = null;
if (realmBaseURL.getPath().endsWith("/")) {
tokenURL = new URL(realmBaseURL,
OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH + "/" + TOKEN_INTROSPECT_URI_PATH);
} else {
tokenURL = new URL(realmBaseURL.toString() + "/" + OPEN_ID_URI_PATH + "/" + TOKEN_URI_PATH + "/"
+ TOKEN_INTROSPECT_URI_PATH);
}
logger.debug("Constructed introspection URL is: {}", tokenURL);
return tokenURL;
} catch (MalformedURLException e) {
throw new KeycloakClientException("Cannot constructs introspection URL from base URL: " + realmBaseURL, e);
}
}
@Override
public URL getAvatarEndpointURL(URL realmBaseURL) throws KeycloakClientException {
logger.debug("Constructing token endpoint URL starting from base URL: {}", realmBaseURL);
try {
URL tokenURL = null;
if (realmBaseURL.getPath().endsWith("/")) {
tokenURL = new URL(realmBaseURL, AVATAR_URI_PATH);
} else {
tokenURL = new URL(realmBaseURL.toString() + "/" + AVATAR_URI_PATH);
}
logger.debug("Constructed avatar URL is: {}", tokenURL);
return tokenURL;
} catch (MalformedURLException e) {
throw new KeycloakClientException("Cannot constructs avatar URL from base URL: " + realmBaseURL, e);
}
}
@Override
public URL computeIntrospectionEndpointURL(URL tokenEndpointURL) throws KeycloakClientException {
logger.debug("Computing introspection endpoint URL starting from token endpoint URL: {}", tokenEndpointURL);
try {
URL introspectionURL = null;
if (tokenEndpointURL.getPath().endsWith(TOKEN_URI_PATH + "/")) {
introspectionURL = new URL(tokenEndpointURL, TOKEN_INTROSPECT_URI_PATH);
} else {
introspectionURL = new URL(tokenEndpointURL, TOKEN_URI_PATH + "/" + TOKEN_INTROSPECT_URI_PATH);
}
logger.debug("Computed introspection URL is: {}", introspectionURL);
return introspectionURL;
} catch (MalformedURLException e) {
throw new KeycloakClientException("Cannot compute introspection URL from token URL: " + tokenEndpointURL,
e);
}
}
@Override
public PublishedRealmRepresentation getRealmInfo(URL realmURL) throws KeycloakClientException {
try {
return JsonUtils.fromJson(ContentUtils.toByteArray(realmURL.openStream()),
PublishedRealmRepresentation.class);
} catch (Exception e) {
throw new KeycloakClientException("Getting realm's info", e);
}
}
@Override
public JSONWebKeySet getRealmJSONWebKeySet(URL jwkURL) throws KeycloakClientException {
try {
return JsonUtils.fromJson(ContentUtils.toByteArray(jwkURL.openStream()), JSONWebKeySet.class);
} catch (Exception e) {
throw new KeycloakClientException("Getting realm's JWK", e);
}
}
@Override
public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret)
throws KeycloakClientException {
return queryOIDCToken(context, clientId, clientSecret, null);
}
@Override
public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret,
Map<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, extraHeaders);
}
@Override
public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret)
throws KeycloakClientException {
return queryOIDCToken(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret));
return queryOIDCToken(tokenURL, clientId, clientSecret, null);
}
protected String constructBasicAuthenticationHeader(String clientId, String clientSecret) {
return "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
@Override
public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret,
Map<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCTokenWithContext(tokenURL, clientId, clientSecret, null, extraHeaders);
}
@Override
public TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException {
return queryOIDCTokenWithContext(context, authorization, null);
}
@Override
public TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException {
return queryOIDCToken(tokenURL, authorization, (Map<String, String>) null);
}
@Override
public TokenResponse queryOIDCToken(String context, String authorization, Map<String, String> extraHeaders)
throws KeycloakClientException {
return queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), authorization, extraHeaders);
}
@Override
public TokenResponse queryOIDCToken(URL tokenURL, String authorization, Map<String, String> extraHeaders)
throws KeycloakClientException {
return queryOIDCTokenWithContext(tokenURL, authorization, null, extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username,
String password) throws KeycloakClientException {
return queryOIDCTokenOfUser(context, clientId, clientSecret, username, password, null);
}
@Override
public TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username,
String password, Map<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, null, extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret,
String audience) throws KeycloakClientException {
return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
audience);
}
@Override
public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience)
throws KeycloakClientException {
return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience);
}
@Override
public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret,
String audience) throws KeycloakClientException {
return queryOIDCTokenWithContext(tokenURL, clientId, clientSecret, audience, null);
}
@Override
public TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret,
String audience, Map<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
audience, extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret, String audience,
Map<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCTokenWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret),
audience, extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience,
Map<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience,
extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience)
throws KeycloakClientException {
return queryOIDCTokenWithContext(tokenURL, authorization, audience, (Map<String, String>) null);
}
protected static String constructBasicAuthenticationHeader(String clientId, String clientSecret) {
return "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes());
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret,
String username, String password, String audience) throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, audience,
(Map<String, String>) null);
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret,
String username, String password, String audience, Map<String, String> extraHeaders)
throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
username, password, audience, extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username,
String password, String audience) throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, username,
password, audience);
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username,
String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, username,
password, audience, extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret,
String username, String password, String audience) throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(tokenURL, clientId, clientSecret, username, password, audience,
(Map<String, String>) null);
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret,
String username, String password, String audience, Map<String, String> extraHeaders)
throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret),
username, password, audience, extraHeaders);
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username,
String password, String audience) throws KeycloakClientException {
return queryOIDCTokenOfUserWithContext(tokenURL, authorization, username, password, audience,
(Map<String, String>) null);
}
@Override
public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username,
String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException {
Map<String, List<String>> params = new HashMap<>();
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(PASSWORD_GRANT_TYPE));
params.put(USERNAME_PARAMETER, Arrays.asList(username));
params.put(PASSWORD_PARAMETER, Arrays.asList(password));
// params.put(SCOPE_PARAMETER, Arrays.asList("openid profile " + OFFLINE_ACCESS_SCOPE));
Map<String, String> headers = new HashMap<>();
logger.debug("Adding authorization header as: {}", authorization);
headers.put(AUTHORIZATION_HEADER, authorization);
if (extraHeaders != null) {
logger.debug("Adding provided extra headers: {}", extraHeaders);
headers.putAll(extraHeaders);
}
if (audience != null) {
logger.debug("Adding d4s context header as: {}", audience);
headers.put(D4S_CONTEXT_HEADER_NAME, audience);
}
return performRequest(tokenURL, headers, params);
}
@Override
public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience,
Map<String, String> extraHeaders)
throws KeycloakClientException {
logger.debug("Querying OIDC token from Keycloak server with URL: {}", tokenURL);
Map<String, List<String>> params = new HashMap<>();
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(CLIENT_CREDENTIALS_GRANT_TYPE));
return performRequest(tokenURL, authorization, params);
Map<String, String> headers = new HashMap<>();
logger.debug("Adding authorization header as: {}", authorization);
headers.put(AUTHORIZATION_HEADER, authorization);
if (audience != null) {
logger.debug("Adding d4s context header as: {}", audience);
headers.put(D4S_CONTEXT_HEADER_NAME, audience);
}
return performRequest(tokenURL, headers, params);
}
@Override
public TokenResponse queryUMAToken(String clientId, String clientSecret, List<String> permissions)
throws KeycloakClientException {
return queryUMAToken(clientId, clientSecret, ScopeProvider.instance.get(), permissions);
}
@Override
public TokenResponse queryUMAToken(TokenResponse oidcTokenResponse, String audience, List<String> permissions)
throws KeycloakClientException {
return queryUMAToken(findTokenEndpointURL(), constructBeareAuthenticationHeader(oidcTokenResponse), audience,
permissions);
}
@Override
public TokenResponse queryUMAToken(String clientId, String clientSecret, String audience,
public TokenResponse queryUMAToken(String context, TokenResponse oidcTokenResponse, String audience,
List<String> permissions) throws KeycloakClientException {
return queryUMAToken(findTokenEndpointURL(), clientId, clientSecret, audience, permissions);
return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, audience, permissions);
}
@Override
@ -139,10 +459,18 @@ public class DefaultKeycloakClient implements KeycloakClient {
return queryUMAToken(tokenURL, constructBeareAuthenticationHeader(oidcTokenResponse), audience, permissions);
}
protected String constructBeareAuthenticationHeader(TokenResponse oidcTokenResponse) {
protected static String constructBeareAuthenticationHeader(TokenResponse oidcTokenResponse) {
return "Bearer " + oidcTokenResponse.getAccessToken();
}
@Override
public TokenResponse queryUMAToken(String context, String clientId, String clientSecret, String audience,
List<String> permissions) throws KeycloakClientException {
return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, audience,
permissions);
}
@Override
public TokenResponse queryUMAToken(URL tokenURL, String clientId, String clientSecret, String audience,
List<String> permissions) throws KeycloakClientException {
@ -152,6 +480,13 @@ public class DefaultKeycloakClient implements KeycloakClient {
audience, permissions);
}
@Override
public TokenResponse queryUMAToken(String context, String authorization, String audience,
List<String> permissions) throws KeycloakClientException {
return queryUMAToken(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience, permissions);
}
@Override
public TokenResponse queryUMAToken(URL tokenURL, String authorization, String audience,
List<String> permissions) throws KeycloakClientException {
@ -166,11 +501,17 @@ public class DefaultKeycloakClient implements KeycloakClient {
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(UMA_TOKEN_GRANT_TYPE));
try {
params.put(AUDIENCE_PARAMETER, Arrays.asList(URLEncoder.encode(checkAudience(audience), "UTF-8")));
String audienceToSend = URLEncoder.encode(checkAudience(audience), "UTF-8");
params.put(AUDIENCE_PARAMETER, Arrays.asList(audienceToSend));
logger.trace("audience is {}", audienceToSend);
} catch (UnsupportedEncodingException e) {
logger.error("Can't URL encode audience: {}", audience, e);
}
Map<String, String> headers = new HashMap<>();
logger.debug("Adding authorization header as: {}", authorization);
headers.put(AUTHORIZATION_HEADER, authorization);
if (permissions != null && !permissions.isEmpty()) {
params.put(
PERMISSION_PARAMETER, permissions.stream().map(s -> {
@ -182,34 +523,53 @@ public class DefaultKeycloakClient implements KeycloakClient {
}).collect(Collectors.toList()));
}
return performRequest(tokenURL, authorization, params);
return performRequest(tokenURL, headers, params);
}
protected TokenResponse performRequest(URL tokenURL, String authorization, Map<String, List<String>> params)
protected TokenResponse performRequest(URL tokenURL, Map<String, String> headers, Map<String, List<String>> params)
throws KeycloakClientException {
if (tokenURL == null) {
return performRequest(TokenResponse.class, tokenURL, headers, params);
}
protected <T> T performRequest(Class<T> returnObjectClass, URL url, Map<String, String> headers,
Map<String, List<String>> params)
throws KeycloakClientException {
if (url == null) {
throw new KeycloakClientException("Token URL must be not null");
}
if (authorization == null || "".equals(authorization)) {
throw new KeycloakClientException("Authorization must be not null nor empty");
}
// Constructing request object
GXHTTPStringRequest request;
try {
String queryString = "";
if (params != null) {
queryString = params.entrySet().stream()
.flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v))
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
} else {
if (logger.isDebugEnabled()) {
logger.debug("Params map is null");
}
}
String queryString = params.entrySet().stream()
.flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v))
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
logger.trace("Query string is: {}", queryString);
request = GXHTTPStringRequest.newRequest(tokenURL.toString())
request = GXHTTPStringRequest.newRequest(url.toString())
.header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString);
request.isExternalCall(true);
if (authorization != null) {
logger.debug("Adding authorization header as: {}", authorization);
request = request.header("Authorization", authorization);
safeSetAsExternalCallForOldAPI(request);
if (headers != null) {
logger.trace("Adding provided headers: {}", headers);
for (String headerName : headers.keySet()) {
request.header(headerName, headers.get(headerName));
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("HTTP headers map is null");
}
}
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct the request object correctly", e);
@ -218,20 +578,27 @@ public class DefaultKeycloakClient implements KeycloakClient {
GXInboundResponse response;
try {
response = request.post();
// TODO: Fill a bug ticket for the gxJRS lib for JSON responses in case of not 2XX code (e.g. 403 error with JSON details in this case).
} catch (Exception e) {
throw new KeycloakClientException("Cannot send request correctly", e);
}
if (response.isSuccessResponse()) {
try {
return response.tryConvertStreamedContentFromJson(TokenResponse.class);
return response.tryConvertStreamedContentFromJson(returnObjectClass);
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct token response object correctly", e);
}
} else {
String errorBody = "[empty]";
try {
errorBody = response.getStreamedContentAsString();
} catch (IOException e1) {
// Not interesting case
}
throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(),
response.getHeaderFields()
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
response.getMessage());
errorBody);
}
}
@ -248,8 +615,8 @@ public class DefaultKeycloakClient implements KeycloakClient {
}
@Override
public TokenResponse refreshToken(TokenResponse tokenResponse) throws KeycloakClientException {
return refreshToken((String) null, tokenResponse);
public TokenResponse refreshToken(String context, TokenResponse tokenResponse) throws KeycloakClientException {
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), tokenResponse);
}
@Override
@ -258,15 +625,10 @@ public class DefaultKeycloakClient implements KeycloakClient {
}
@Override
public TokenResponse refreshToken(String clientId, TokenResponse tokenResponse) throws KeycloakClientException {
return refreshToken(clientId, null, tokenResponse);
}
@Override
public TokenResponse refreshToken(String clientId, String clientSecret, TokenResponse tokenResponse)
public TokenResponse refreshToken(String context, String clientId, String clientSecret, TokenResponse tokenResponse)
throws KeycloakClientException {
return refreshToken(findTokenEndpointURL(), clientId, clientSecret, tokenResponse);
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, tokenResponse);
}
@Override
@ -285,25 +647,11 @@ public class DefaultKeycloakClient implements KeycloakClient {
}
@Override
public TokenResponse refreshToken(String refreshTokenJWTString) throws KeycloakClientException {
try {
String clientId = ModelUtils.getClientIdFromToken(ModelUtils.getRefreshTokenFrom(refreshTokenJWTString));
return refreshToken(clientId, refreshTokenJWTString);
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct access token object from token response", e);
}
}
public TokenResponse refreshToken(String context, String clientId, String clientSecret,
String refreshTokenJWTString) throws KeycloakClientException {
@Override
public TokenResponse refreshToken(String clientId, String refreshTokenJWTString) throws KeycloakClientException {
return refreshToken(clientId, null, refreshTokenJWTString);
}
@Override
public TokenResponse refreshToken(String clientId, String clientSecret, String refreshTokenJWTString)
throws KeycloakClientException {
return refreshToken(findTokenEndpointURL(), clientId, clientSecret, refreshTokenJWTString);
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
refreshTokenJWTString);
}
@Override
@ -324,45 +672,224 @@ public class DefaultKeycloakClient implements KeycloakClient {
logger.debug("Refreshing token from Keycloak server with URL: {}", tokenURL);
// Constructing request object
GXHTTPStringRequest request;
try {
Map<String, String> params = new HashMap<>();
params.put(GRANT_TYPE_PARAMETER, REFRESH_TOKEN_GRANT_TYPE);
params.put(REFRESH_TOKEN_PARAMETER, refreshTokenJWTString);
params.put(CLIENT_ID_PARAMETER, URLEncoder.encode(clientId, "UTF-8"));
Map<String, List<String>> params = new HashMap<>();
params.put(GRANT_TYPE_PARAMETER, Collections.singletonList(REFRESH_TOKEN_GRANT_TYPE));
params.put(REFRESH_TOKEN_PARAMETER, Collections.singletonList(refreshTokenJWTString));
params.put(CLIENT_ID_PARAMETER, Collections.singletonList(URLEncoder.encode(clientId, "UTF-8")));
if (clientSecret != null && !"".equals(clientSecret)) {
params.put(CLIENT_SECRET_PARAMETER, URLEncoder.encode(clientSecret, "UTF-8"));
params.put(CLIENT_SECRET_PARAMETER,
Collections.singletonList(URLEncoder.encode(clientSecret, "UTF-8")));
}
String queryString = params.entrySet().stream()
.map(p -> p.getKey() + "=" + p.getValue())
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
request = GXHTTPStringRequest.newRequest(tokenURL.toString()).header("Content-Type",
"application/x-www-form-urlencoded").withBody(queryString);
request.isExternalCall(true);
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct the request object correctly", e);
return performRequest(tokenURL, null, params);
} catch (UnsupportedEncodingException e) {
throw new KeycloakClientException("Cannot encode parameters", e);
}
GXInboundResponse response;
}
@Override
public TokenResponse exchangeTokenForAccessToken(String context, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException {
return exchangeTokenForAccessToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId,
clientSecret, audience);
}
@Override
public TokenResponse exchangeTokenForAccessToken(URL tokenURL, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException {
return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, ACCESS_TOKEN_TOKEN_TYPE,
null);
}
@Override
public TokenResponse exchangeTokenForRefreshToken(String context, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException {
return exchangeTokenForRefreshToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId,
clientSecret, audience);
}
@Override
public TokenResponse exchangeTokenForRefreshToken(URL tokenURL, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException {
return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, REFRESH_TOKEN_TOKEN_TYPE,
null);
}
@Override
public TokenResponse exchangeTokenForOfflineToken(String context, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException {
return exchangeTokenForOfflineToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId,
clientSecret, audience);
}
@Override
public TokenResponse exchangeTokenForOfflineToken(URL tokenURL, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException {
AccessToken at = null;
try {
response = request.post();
at = ModelUtils.getAccessTokenFrom(oidcAccessToken);
} catch (Exception e) {
throw new KeycloakClientException("Cannot send request correctly", e);
throw new IllegalArgumentException("Impossible to parse the access token as JSON", e);
}
if (response.isSuccessResponse()) {
if (at.getScope().indexOf(OFFLINE_ACCESS_SCOPE) < 0) {
logger.info("Token to be exchanged doesn't contain 'offline_token' within scopes");
throw new IllegalArgumentException("Orignal access token doesn't contain the 'offline_token' scope");
}
return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, REFRESH_TOKEN_TOKEN_TYPE,
OFFLINE_ACCESS_SCOPE);
}
/**
* Queries from the OIDC server an exchanged token by using provided access token, for the given audience (context),
* in URLEncoded form or not,
*
* @param tokenURL the token endpoint {@link URL} of the OIDC server
* @param oidcAccessToken the auth token (the access token URLEncoded by the "Bearer " string)
* @param clientId the client id
* @param clientSecret the client secret
* @param audience the audience (context) where to request the issuing of the ticket (URLEncoded)
* @param requestedTokenType the token type (e.g. <code>refresh</code>)
* @param scope the scope, optional can be <code>null</code>
* @return the issued exchanged token
* @throws KeycloakClientException if an error occurs, inspect the exception for details
*/
protected TokenResponse exchangeToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret,
String audience, String requestedTokenType, String scope) throws KeycloakClientException {
if (audience == null || "".equals(audience)) {
throw new KeycloakClientException("Audience must be not null nor empty");
}
logger.debug("Exchanging token from Keycloak server with URL: {}", tokenURL);
Map<String, List<String>> params = new HashMap<>();
params.put(SUBJECT_TOKEN_PARAMETER, Arrays.asList(oidcAccessToken));
params.put(CLIENT_ID_PARAMETER, Arrays.asList(clientId));
params.put(CLIENT_SECRET_PARAMETER, Arrays.asList(clientSecret));
params.put(GRANT_TYPE_PARAMETER, Arrays.asList(TOKEN_EXCHANGE_GRANT_TYPE));
params.put(SUBJECT_TOKEN_TYPE_PARAMETER, Arrays.asList(ACCESS_TOKEN_TOKEN_TYPE));
params.put(REQUESTED_TOKEN_TYPE_PARAMETER, Arrays.asList(requestedTokenType));
if (scope != null) {
params.put(SCOPE_PARAMETER, Arrays.asList(scope));
}
try {
String audienceToSend = URLEncoder.encode(checkAudience(audience), "UTF-8");
params.put(AUDIENCE_PARAMETER, Arrays.asList(audienceToSend));
logger.trace("audience is {}", audienceToSend);
} catch (UnsupportedEncodingException e) {
logger.error("Can't URL encode audience: {}", audience, e);
}
return performRequest(tokenURL, null, params);
}
@Override
public TokenIntrospectionResponse introspectAccessToken(String context, String clientId, String clientSecret,
String accessTokenJWTString) throws KeycloakClientException {
return introspectAccessToken(getIntrospectionEndpointURL(getRealmBaseURL(context)), 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);
return performRequest(TokenIntrospectionResponse.class, introspectionURL,
Collections.singletonMap("Authorization", constructBasicAuthenticationHeader(clientId, clientSecret)),
Collections.singletonMap(TOKEN_PARAMETER, Collections.singletonList(accessTokenJWTString)));
}
@Override
public boolean isAccessTokenVerified(String context, String clientId, String clientSecret,
String accessTokenJWTString) throws KeycloakClientException {
return isAccessTokenVerified(getIntrospectionEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
accessTokenJWTString);
}
@Override
public boolean isAccessTokenVerified(URL introspectionURL, String clientId, String clientSecret,
String accessTokenJWTString) throws KeycloakClientException {
return introspectAccessToken(introspectionURL, clientId, clientSecret, accessTokenJWTString).getActive();
}
protected void safeSetAsExternalCallForOldAPI(GXHTTPStringRequest request) {
try {
logger.trace("Looking for the 'isExternalCall' method in the 'GXHTTPStringRequest' class");
Method isExetnalCallMethod = request.getClass().getMethod("isExternalCall", boolean.class);
logger.trace("Method found, is the old gxJRS API. Invoking it with 'true' argument");
isExetnalCallMethod.invoke(request, true);
} catch (NoSuchMethodException e) {
logger.trace("Method not found, is the new gxJRS API");
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
logger.warn("Cannot invoke 'isExternalCall' method via reflection on 'GXHTTPStringRequest' class", e);
}
}
@Override
public byte[] getAvatarData(String context, TokenResponse tokenResponse) throws KeycloakClientException {
return getAvatarData(getAvatarEndpointURL(getRealmBaseURL(context)), tokenResponse);
}
@Override
public byte[] getAvatarData(URL avatarURL, TokenResponse tokenResponse) throws KeycloakClientException {
logger.debug("Getting user's avatar from URL: {}", avatarURL);
try {
GXHTTPStringRequest request;
try {
return response.tryConvertStreamedContentFromJson(TokenResponse.class);
request = GXHTTPStringRequest.newRequest(avatarURL.toString());
safeSetAsExternalCallForOldAPI(request);
String authorization = constructBeareAuthenticationHeader(tokenResponse);
logger.debug("Adding authorization header as: {}", authorization);
request = request.header("Authorization", authorization);
request = request.header("Accept", "image/png, image/gif");
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct token response object correctly", e);
throw new KeycloakClientException("Cannot construct the request object correctly", e);
}
} else {
throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(),
response.getHeaderFields()
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
response.getMessage());
GXInboundResponse response;
try {
response = request.get();
} catch (Exception e) {
throw new KeycloakClientException("Cannot send request correctly", e);
}
if (response.isSuccessResponse()) {
return response.getStreamedContent();
} else {
throw KeycloakClientException.create("Unable to get avatar image data", response.getHTTPCode(),
response.getHeaderFields()
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
response.getMessage());
}
} catch (IOException e) {
throw new KeycloakClientException("Error getting user's avatar data", e);
}
}

View File

@ -2,37 +2,124 @@ package org.gcube.common.keycloak;
import java.net.URL;
import java.util.List;
import java.util.Map;
import org.gcube.common.keycloak.model.JSONWebKeySet;
import org.gcube.common.keycloak.model.PublishedRealmRepresentation;
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;
import org.slf4j.LoggerFactory;
public interface KeycloakClient {
Logger logger = LoggerFactory.getLogger(KeycloakClient.class);
public static final String PROD_ROOT_SCOPE = "/d4science.research-infrastructures.eu";
public static final String OPEN_ID_URI_PATH = "protocol/openid-connect";
public static final String TOKEN_URI_PATH = "token";
public static final String JWK_URI_PATH = "certs";
public static final String TOKEN_INTROSPECT_URI_PATH = "introspect";
public static final String AVATAR_URI_PATH = "account-avatar";
public final static String D4S_CONTEXT_HEADER_NAME = "X-D4Science-Context";
public static String DEFAULT_REALM = "d4science";
String CATEGORY = "Auth";
String NAME = "IAM";
String DESCRIPTION = "oidc-token endpoint";
/**
* Finds the keycloak endpoint {@link URL} discovering it in the current scope provided by {@link ScopeProvider}
* Returns the Keycloak base {@link URL} for the given context and the default realm (<code>d4science</code>)
*
* @return the keycloak endpoint URL in the current scope
* @param context the context where the endpoint is needed (e.g. <code>/gcube</code> for DEV)
* @return the Keycloak <code>token</code> endpoint URL
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL findTokenEndpointURL() throws KeycloakClientException;
URL getRealmBaseURL(String context) throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server discovered in the current scope, by using provided clientId and client secret
* Returns the Keycloak base {@link URL} for the given context and in the given realm.
*
* @param context the context where the endpoint is needed (e.g. <code>/gcube</code> for DEV)
* @param realm the realm to use to construct the base URL
* @return the Keycloak <code>token</code> endpoint URL
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL getRealmBaseURL(String context, String realm) throws KeycloakClientException;
/**
* Constructs the Keycloak <code>token</code> endpoint {@link URL} from the realm's base URL.
*
* @param realmBaseURL the realm's base URL to use
* @return the Keycloak <code>token</code> endpoint URL
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL getTokenEndpointURL(URL realmBaseURL) throws KeycloakClientException;
/**
* Constructs the Keycloak <code>JWK</code> endpoint {@link URL} from the realm's base URL.
*
* @param realmBaseURL the realm's base URL to use
* @return the Keycloak <code>JWK</code> endpoint URL
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL getJWKEndpointURL(URL realmBaseURL) throws KeycloakClientException;
/**
* Constructs the Keycloak <code>introspection</code> endpoint {@link URL} from the realm's base URL.
*
* @param realmBaseURL the realm's base URL to use
* @return the Keycloak <code>introspection</code> endpoint URL
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL getIntrospectionEndpointURL(URL realmBaseURL) throws KeycloakClientException;
/**
* Compute the keycloak <code>introspection</code> endpoint {@link URL} starting from the provided token endpoint.
*
* @param tokenEndpointURL the token endpoint to use in the compute
* @return the keycloak <code>introspection</code> endpoint URL
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL computeIntrospectionEndpointURL(URL tokenEndpointURL) throws KeycloakClientException;
/**
* Constructs the Keycloak <code>avatar</code> endpoint {@link URL} from the realm's base URL.
*
* @param realmBaseURL the realm's base URL to use
* @return the Keycloak <code>avatar</code> endpoint URL
* @throws KeycloakClientException if something goes wrong discovering the endpoint URL
*/
URL getAvatarEndpointURL(URL realmBaseURL) throws KeycloakClientException;
/**
* Gets the realm info setup (RSA <code>public_key</code>, <code>token-service</code> URL,
* <code>account-service</code> URL and <code>tokens-not-before</code> setting)
*
* @param realmURL the realm URL
* @return the configured realm info
* @throws KeycloakClientException if something goes wrong getting realm info
*/
PublishedRealmRepresentation getRealmInfo(URL realmURL) throws KeycloakClientException;
JSONWebKeySet getRealmJSONWebKeySet(URL jwkURL) throws KeycloakClientException;
/**
* Queries an OIDC token from the context's Keycloak server, by using provided clientId and client secret.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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;
TokenResponse queryOIDCToken(String context, String clientId, String clientSecret) throws KeycloakClientException;
/**
* Queries an OIDC token from the context's Keycloak server, by using provided clientId and client secret.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCToken(String context, String clientId, String clientSecret, Map<String, String> extraHeaders) throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided clientId and client secret.
@ -45,21 +132,358 @@ public interface KeycloakClient {
*/
TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret) throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided clientId and client secret.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @param tokenURL the token endpoint {@link URL} of the Keycloak server
* @param clientId the client id
* @param clientSecret the client secret
* @param extraHeaders extra HTTP headers to add to the request
* @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, Map<String, String> extraHeaders) 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 context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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(String context, String authorization) throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided authorization.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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)
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCToken(String context, String authorization, Map<String, String> extraHeaders) 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 OIDC token from the Keycloak server, by using provided authorization.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @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)
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCToken(URL tokenURL, String authorization, Map<String, String> extraHeaders) throws KeycloakClientException;
/**
* Queries an OIDC token from the context's Keycloak server, by using provided clientId and client secret, reducing the audience to the requested one.
*
* The implementation uses the custom <code>X-D4Science-Context</code> HTTP header that the proper mapper on Keycloak uses to reduce the audience
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret, String audience)
throws KeycloakClientException;
/**
* Queries an OIDC token from the context's Keycloak server, by using provided clientId and client secret, reducing the audience to the requested one.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* The implementation uses the custom <code>X-D4Science-Context</code> HTTP header that the proper mapper on Keycloak uses to reduce the audience
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret, String audience, Map<String, String> extraHeaders)
throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided clientId and client secret, reducing the audience to the requested one.
*
* The implementation uses the custom <code>X-D4Science-Context</code> HTTP header that the proper mapper on Keycloak uses to reduce the audience
*
* @param tokenURL the token endpoint {@link URL} of the Keycloak server
* @param clientId the client id
* @param clientSecret the client secret
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret, String audience)
throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided clientId and client secret, reducing the audience to the requested one.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* The implementation uses the custom <code>X-D4Science-Context</code> HTTP header that the proper mapper on Keycloak uses to reduce the audience
*
* @param tokenURL the token endpoint {@link URL} of the Keycloak server
* @param clientId the client id
* @param clientSecret the client secret
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret, String audience, Map<String, String> extraHeaders)
throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided authorization, reducing the audience to the requested one.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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)
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience)
throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided authorization, reducing the audience to the requested one.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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)
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience, Map<String, String> extraHeaders)
throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided authorization, reducing the audience to the requested one.
*
* @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)
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience)
throws KeycloakClientException;
/**
* Queries an OIDC token from the Keycloak server, by using provided authorization, reducing the audience to the requested one.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @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)
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience, Map<String, String> extraHeaders)
throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param username the user's username
* @param password the user's password
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username,
String password) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param username the user's username
* @param password the user's password
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username,
String password, Map<String, String> extraHeaders) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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)
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username,
String password, String audience) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the Keycloak server, by using provided clientId and client secret and user's username and password, reducing the audience to the requested one.
*
* The implementation uses the custom <code>X-D4Science-Context</code> HTTP header that the proper mapper on Keycloak uses to reduce the audience
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret, String username,
String password, String audience) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the Keycloak server, by using provided clientId and client secret and user's username and password, reducing the audience to the requested one.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* The implementation uses the custom <code>X-D4Science-Context</code> HTTP header that the proper mapper on Keycloak uses to reduce the audience
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret,
String username, String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password, reducing the audience to the requested one.
*
* The implementation uses the custom <code>X-D4Science-Context</code> HTTP header that the proper mapper on Keycloak uses to reduce the audience
*
* @param tokenURL the token endpoint {@link URL} of the Keycloak server
* @param clientId the client id
* @param clientSecret the client secret
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret, String username,
String password, String audience) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password, , reducing the audience to the requested one.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @param tokenURL the token endpoint {@link URL} of the Keycloak server
* @param clientId the client id
* @param clientSecret the client secret
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret,
String username, String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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)
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username,
String password, String audience, Map<String, String> extraHeaders) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password.
*
* @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)
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username, String password,
String audience) throws KeycloakClientException;
/**
* Queries an OIDC token for a specific user from the context's Keycloak server, by using provided clientId and client secret and user's username and password.
* Optionally extra HTTP headers can be provided to be used in the call.
*
* @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)
* @param username the user's username
* @param password the user's password
* @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak
* @param extraHeaders extra HTTP headers to add to the request
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username,
String password, String audience, Map<String, String> extraHeaders) 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.
*
* @param tokenUrl the token endpoint {@link URL} of the OIDC server
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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)
* @param audience the audience (context) where to request the issuing of the ticket (URLEncoded)
* @param permissions a list of permissions, can be <code>null</code>
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryUMAToken(String context, String authorization, String audience, List<String> permissions)
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.
*
* @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)
* @param audience the audience (context) where to request the issuing of the ticket (URLEncoded)
* @param permissions a list of permissions, can be <code>null</code>
@ -73,6 +497,35 @@ public interface KeycloakClient {
* 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 context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param oidcTokenResponse the previously issued token as {@link TokenResponse} object
* @param audience the audience (context) where to request the issuing of the ticket
* @param permissions a list of permissions, can be <code>null</code>
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryUMAToken(String context, TokenResponse oidcTokenResponse, String audience,
List<String> 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 tokenURL the token endpoint {@link URL} of the OIDC server
* @param oidcTokenResponse the previously issued token as {@link TokenResponse} object
* @param audience the audience (context) where to request the issuing of the ticket
* @param permissions a list of permissions, can be <code>null</code>
* @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<String> 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.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the client id
* @param clientSecret the client secret
* @param audience the audience (context) where to request the issuing of the ticket
@ -80,7 +533,7 @@ public interface KeycloakClient {
* @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,
TokenResponse queryUMAToken(String context, String clientId, String clientSecret, String audience,
List<String> permissions) throws KeycloakClientException;
/**
@ -99,58 +552,18 @@ public interface KeycloakClient {
List<String> 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 <code>null</code>
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryUMAToken(TokenResponse oidcTokenResponse, String audience, List<String> permissions)
throws KeycloakClientException;
/**
* Queries an UMA token from the Keycloak server discovered in the current scope, by using provided clientId and client secret
* 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 <code>null</code>
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryUMAToken(String clientId, String clientSecret, String audience, List<String> permissions)
throws KeycloakClientException;
/**
* Queries an UMA token from the Keycloak server discovered in the current scope, by using provided clientId and client secret
* 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
* @param permissions a list of permissions, can be <code>null</code>
* @return the issued token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryUMAToken(String clientId, String clientSecret, List<String> permissions)
throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server discovered in the current scope using the refresh
* token JWT encoded string in the token response object.
* Refreshes a previously issued token from the Keycloak server using the refresh token JWT encoded string in the
* token response object.
*
* Client id will be read from "issued for" access token's claim and client secret will be not sent.
* <br><b>NOTE</b>: For <code>public</code> clients types only.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param tokenResponse the previously issued token as {@link TokenResponse} object
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse refreshToken(TokenResponse tokenResponse) throws KeycloakClientException;
TokenResponse refreshToken(String context, TokenResponse tokenResponse) throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server using the refresh token JWT encoded string in the
@ -159,7 +572,7 @@ public interface KeycloakClient {
* Client id will be read from "issued for" access token's claim and client secret will be not sent.
* <br><b>NOTE</b>: For <code>public</code> clients types only.
*
* @param tokenUrl the token endpoint {@link URL} of the OIDC server
* @param tokenURL the token endpoint {@link URL} of the OIDC server
* @param tokenResponse the previously issued token as {@link TokenResponse} object
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
@ -167,36 +580,24 @@ public interface KeycloakClient {
TokenResponse refreshToken(URL tokenURL, TokenResponse tokenResponse) throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server discovered in the current scope using the refresh
* token JWT encoded string in the token response object and the provided client id.
*
* Client secret will be not sent.
* <br><b>NOTE</b>: For <code>public</code> clients types only.
*
* @param clientId the requestor client id, may be <code>null</code> and in this case will be take from the access token "issued for" claim
* @param tokenResponse the previously issued token as {@link TokenResponse} object
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse refreshToken(String clientId, TokenResponse tokenResponse) throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server discovered in the current scope using the refresh
* token JWT encoded string in the token response object and the provided client id and secret.
* Refreshes a previously issued token from the Keycloak server using the refresh token JWT encoded string in the
* token response object and the provided client id and secret.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the requestor client id, may be <code>null</code> and in this case will be take from the access token "issued for" claim
* @param clientSecret the requestor client secret, may be <code>null</code> for non-confidential clients
* @param tokenResponse the previously issued token as {@link TokenResponse} object
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse refreshToken(String clientId, String clientSecret, TokenResponse tokenResponse)
TokenResponse refreshToken(String context, String clientId, String clientSecret, TokenResponse tokenResponse)
throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server using the refresh token JWT encoded string in the
* token response object and the provided client id and secret.
*
* @param tokenURL the token endpoint {@link URL} of the OIDC server
* @param clientId the requestor client id, may be <code>null</code> and in this case will be take from the access token "issued for" claim
* @param clientSecret the requestor client secret, may be <code>null</code> for non-confidential clients
* @param tokenResponse the previously issued token as {@link TokenResponse} object
@ -207,50 +608,24 @@ public interface KeycloakClient {
throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server discovered in the current scope using the the refresh token JWT encoded string obtained with the access token in the previous token response.
*
* Client id will be read from "issued for" refresh token's claim and client secret will be not sent.
* <br><b>NOTE</b>: For <code>public</code> clients types only.
*
* @param refreshTokenJWTString the previously issued refresh token JWT string taken from the same token response of the access token parameter
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse refreshToken(String refreshTokenJWTString) throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server discovered in the current scope using the provided
* client id and the refresh token JWT encoded string obtained with the access token in the previous token response.
*
* Client secret will be not used.
* <br><b>NOTE</b>: For <code>public</code> clients types only.
*
* @param clientId the requestor client id
* @param refreshTokenJWTString the previously issued refresh token JWT string taken from the same token response of the access token parameter
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse refreshToken(String clientId, String refreshTokenJWTString) throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server discovered in the current scope using the provided
* client id and secret and the refresh token JWT encoded string obtained with the access token in the previous
* token response.
*
* Refreshes a previously issued token from the Keycloak server by using the client id and secret
* and the refresh token JWT encoded string obtained with the access token in the previous token response.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param clientId the requestor client id
* @param clientSecret the requestor client secret, may be <code>null</code> for non-confidential clients
* @param refreshTokenJWTString the previously issued refresh token JWT string taken from the same token response of the access token parameter
* @param refreshTokenJWTString the previously issued refresh token JWT string
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse refreshToken(String clientId, String clientSecret, String refreshTokenJWTString)
TokenResponse refreshToken(String context, String clientId, String clientSecret, String refreshTokenJWTString)
throws KeycloakClientException;
/**
* Refreshes a previously issued token from the Keycloak server by using the client id and secret
* and the refresh token JWT encoded string obtained with the access token in the previous token response.
*
* @param tokenUrl the token endpoint {@link URL} of the OIDC server
* @param tokenURL the token endpoint {@link URL} of the OIDC server
* @param clientId the requestor client id
* @param clientSecret the requestor client secret, may be <code>null</code> for non-confidential clients
* @param refreshTokenJWTString the previously issued refresh token JWT string
@ -260,4 +635,148 @@ public interface KeycloakClient {
TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, String refreshTokenJWTString)
throws KeycloakClientException;
/**
* Exchanges a token for another access token for a specific client and a specific audience
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param oidcAccessToken the original access token to exchange
* @param clientId the authorized client's id
* @param clientSecret the authorized client's secret
* @param audience the requested token audience
* @return the exchanged token response
* @throws KeycloakClientException if an error occurs during the exchange
*/
TokenResponse exchangeTokenForAccessToken(String context, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException;
/**
* Exchanges a token for another access token for a specific client and a specific audience
*
* @param tokenURL the token endpoint URL
* @param oidcAccessToken the original access token to exchange
* @param clientId the authorized client's id
* @param clientSecret the authorized client's secret
* @param audience the requested token audience
* @return the exchanged token response
* @throws KeycloakClientException if an error occurs during the exchange
*/
TokenResponse exchangeTokenForAccessToken(URL tokenURL, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException;
/**
* Exchanges a token for another access and a refresh tokens for a specific client and a specific audience
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @param oidcAccessToken the original access token to exchange
* @param clientId the authorized client's id
* @param clientSecret the authorized client's secret
* @param audience the requested token audience
* @return the exchanged token response
* @throws KeycloakClientException if an error occurs during the exchange
*/
TokenResponse exchangeTokenForRefreshToken(String context, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException;
/**
* Exchanges a token for another access and a refresh tokens for a specific client and a specific audience
*
* @param tokenURL the token endpoint URL
* @param oidcAccessToken the original access token to exchange
* @param clientId the authorized client's id
* @param clientSecret the authorized client's secret
* @param audience the requested token audience
* @return the exchanged token response
* @throws KeycloakClientException if an error occurs during the exchange
*/
TokenResponse exchangeTokenForRefreshToken(URL tokenURL, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws KeycloakClientException;
/**
* Exchanges a token for another access and an offline refresh tokens for a specific client and a specific audience
* The refresh token will be of the offline type only if the original token has the <code>offline_access</code> within its scopes
*
* @param tokenURL the token endpoint URL
* @param oidcAccessToken the original access token to exchange
* @param clientId the authorized client's id
* @param clientSecret the authorized client's secret
* @param audience the requested token audience
* @return the exchanged token response
* @throws IllegalArgumentException if the original token does'nt contains the <code>offline_access</code> scope within its scopes or if is impossible to parse the access token as JSON
* @throws KeycloakClientException if an error occurs during the exchange
*/
TokenResponse exchangeTokenForOfflineToken(String context, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException;
/**
* Exchanges a token for another access and an offline refresh tokens for a specific client and a specific audience
* The refresh token will be of the offline type only if the original token has the scope <code>offline_access</code> within its scopes
*
* @param tokenURL the token endpoint URL
* @param oidcAccessToken the original access token to exchange
* @param clientId the authorized client's id
* @param clientSecret the authorized client's secret
* @param audience the requested token audience
* @return the exchanged token response
* @throws IllegalArgumentException if the original token does'nt contains the <code>offline_access</code> scope within its scopes or if is impossible to parse the access token as JSON
* @throws KeycloakClientException if an error occurs during the exchange
*/
TokenResponse exchangeTokenForOfflineToken(URL tokenURL, String oidcAccessToken, String clientId,
String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException;
/**
* Introspects an access token against the Keycloak server.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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(String context, 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.
*
* @param context the context where the Keycloak's is needed (e.g. <code>/gcube</code> for DEV)
* @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(String context, 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;
byte[] getAvatarData(String context, TokenResponse tokenResponse) throws KeycloakClientException;
byte[] getAvatarData(URL avatarURL, TokenResponse tokenResponse) throws KeycloakClientException;
}

View File

@ -56,7 +56,7 @@ public class KeycloakClientException extends Exception {
}
public boolean hasJSONPayload() {
return getContentType().endsWith("json");
return getContentType() != null && getContentType().endsWith("json");
}
public void setResponseString(String responseString) {

View File

@ -7,9 +7,32 @@ public class KeycloakClientFactory {
protected static final Logger logger = LoggerFactory.getLogger(KeycloakClientFactory.class);
protected static String CUSTOM_BASE_URL;
public static void setCustomBaseURL(String customBaseURL) {
if (customBaseURL != null) {
logger.info("Setting custom base URL static value to {}", customBaseURL);
} else {
logger.info("Removing custom base URL static value");
}
if (customBaseURL == null || customBaseURL.endsWith("/")) {
CUSTOM_BASE_URL = customBaseURL;
} else {
CUSTOM_BASE_URL = customBaseURL += "/";
}
}
public static String getCustomBaseURL() {
return CUSTOM_BASE_URL;
}
public static KeycloakClient newInstance() {
logger.debug("Instantiating a new keycloak client instance");
return new DefaultKeycloakClient();
DefaultKeycloakClient newInstance = new DefaultKeycloakClient();
if (getCustomBaseURL() != null) {
newInstance.setCustomBaseURL(CUSTOM_BASE_URL);
}
return newInstance;
}
}

View File

@ -0,0 +1,48 @@
package org.gcube.common.keycloak;
import org.gcube.common.keycloak.model.AccessToken;
import org.gcube.common.keycloak.model.RefreshToken;
import org.gcube.common.keycloak.model.TokenResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class KeycloakClientHelper {
protected static Logger logger = LoggerFactory.getLogger(KeycloakClientHelper.class);
/**
* The public GW used to obtain the token, is not declared final since it can be changed if necessary at runtime
*/
private static String GATEWAY_CLIENT_ID = "d4science-internal-gateway";
/**
* Sets the new default GW <code>clientId</code> used for all the queries to the Keycloak server.
* Note: The operation will logged as WARN to be visible.
* @param gatewayClientId the new GW <code>clientId</code>
*/
public static void setDefaultGWClientID(String gatewayClientId) {
logger.warn("The default GW clientId will be changed to: {}", gatewayClientId);
GATEWAY_CLIENT_ID = gatewayClientId;
}
/**
* Gets a new {@link TokenResponse}, containing the {@link AccessToken} and the {@link RefreshToken} from the Keycloak server in the environment of the context represented by the <code>context</code> parameter.
* The <code>context</code> parameter is also used as audience for the token.
*
* @param context the context ot be used for discovery and for audience
* @param username the user's username
* @param password the user's password
* @return the token response from the Keycloak server
* @throws KeycloakClientException if an error occurs during the process
*/
public static TokenResponse getTokenForUser(String context, String username, String password) throws KeycloakClientException {
logger.debug("Getting new token for user '{}' in context '{}'", username, context);
logger.trace("Getting OIDC token for user '{}' using configured default GW clientId '{}'", username, GATEWAY_CLIENT_ID);
TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUser(context, GATEWAY_CLIENT_ID,
"", username, password);
logger.trace("Getting UMA token in the '{}' context", context);
return KeycloakClientFactory.newInstance().queryUMAToken(context, oidcTR, context, null);
}
}

View File

@ -1,3 +1,19 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import java.io.Serializable;
@ -10,6 +26,9 @@ import java.util.Set;
import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
*/
public class AccessToken extends IDToken {
private static final long serialVersionUID = 6364784008775737335L;
@ -68,6 +87,11 @@ public class AccessToken extends IDToken {
this.verifyCaller = required;
return this;
}
@Override
public String toString() {
return getRoles() != null ? getRoles().toString() : null;
}
}
@JsonProperty("trusted-certs")
@ -151,4 +175,12 @@ public class AccessToken extends IDToken {
this.trustedCertificates = trustedCertificates;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
}

View File

@ -1,7 +1,26 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
*/
public class AddressClaimSet {
public static final String FORMATTED = "formatted";

View File

@ -1,7 +1,26 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
*/
public class IDToken extends JsonWebToken {
private static final long serialVersionUID = 8406175387651749097L;

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class JSONWebKeySet {
@JsonProperty("keys")
private JWK[] keys;
public JWK[] getKeys() {
return keys;
}
public void setKeys(JWK[] keys) {
this.keys = keys;
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import java.util.HashMap;
import java.util.Map;
import org.gcube.com.fasterxml.jackson.annotation.JsonAnyGetter;
import org.gcube.com.fasterxml.jackson.annotation.JsonAnySetter;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class JWK {
public static final String KEY_ID = "kid";
public static final String KEY_TYPE = "kty";
public static final String ALGORITHM = "alg";
public static final String PUBLIC_KEY_USE = "use";
public enum Use {
SIG("sig"),
ENCRYPTION("enc");
private String str;
Use(String str) {
this.str = str;
}
public String asString() {
return str;
}
}
@JsonProperty(KEY_ID)
private String keyId;
@JsonProperty(KEY_TYPE)
private String keyType;
@JsonProperty(ALGORITHM)
private String algorithm;
@JsonProperty(PUBLIC_KEY_USE)
private String publicKeyUse;
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
public String getKeyId() {
return keyId;
}
public void setKeyId(String keyId) {
this.keyId = keyId;
}
public String getKeyType() {
return keyType;
}
public void setKeyType(String keyType) {
this.keyType = keyType;
}
public String getAlgorithm() {
return algorithm;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public String getPublicKeyUse() {
return publicKeyUse;
}
public void setPublicKeyUse(String publicKeyUse) {
this.publicKeyUse = publicKeyUse;
}
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}
@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}
}

View File

@ -15,6 +15,9 @@ import org.gcube.common.keycloak.model.util.StringOrArrayDeserializer;
import org.gcube.common.keycloak.model.util.StringOrArraySerializer;
import org.gcube.common.keycloak.model.util.Time;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
*/
public class JsonWebToken implements Serializable {
private static final long serialVersionUID = -8136409077130940942L;
@ -80,7 +83,7 @@ public class JsonWebToken implements Serializable {
/**
* Tests that the token is not expired and is not-before.
*
* @return
* @return <code>true</code> if is not expired and is not-before
*/
@JsonIgnore
public boolean isActive() {
@ -98,6 +101,7 @@ public class JsonWebToken implements Serializable {
/**
* Set issuedAt to the current time
* @return the token itself
*/
@JsonIgnore
public JsonWebToken issuedNow() {
@ -183,7 +187,7 @@ public class JsonWebToken implements Serializable {
/**
* OAuth client the token was issued for.
*
* @return
* @return the issued for vale
*/
public String getIssuedFor() {
return issuedFor;
@ -197,7 +201,7 @@ public class JsonWebToken implements Serializable {
/**
* This is a map of any other claims and data that might be in the IDToken. Could be custom claims set up by the auth server
*
* @return
* @return the object's other claims
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {

View File

@ -1,16 +1,36 @@
package org.gcube.common.keycloak.model;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.gcube.com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.gcube.com.fasterxml.jackson.core.JsonProcessingException;
import org.gcube.com.fasterxml.jackson.core.type.TypeReference;
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
import org.gcube.com.fasterxml.jackson.databind.ObjectWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
/**
* @author <a href="mailto:mauro.mugnaini@nubisware.com">Mauro Mugnaini</a>
*/
public class ModelUtils {
protected static final Logger logger = LoggerFactory.getLogger(ModelUtils.class);
@ -37,12 +57,87 @@ public class ModelUtils {
}
}
/**
* Creates a {@link RSAPublicKey} instance from its string PEM representation
*
* @param publicKeyPem the public key PEM string
* @return the RSA public key
* @throws Exception if it's not possible to create the RSA public key from the PEM string
*/
public static RSAPublicKey createRSAPublicKey(String publicKeyPem) throws Exception {
return (RSAPublicKey) createPublicKey(publicKeyPem, "RSA");
}
/**
* Creates a {@link PublicKey} instance from its string PEM representation
*
* @param publicKeyPem the public key PEM string
* @param algorithm the key type (e.g. RSA)
* @return the public key
* @throws Exception if it's not possible to create the public key from the PEM string
*/
public static PublicKey createPublicKey(String publicKeyPem, String algorithm) throws Exception {
try {
String publicKey = publicKeyPem.replaceFirst("-----BEGIN (.*)-----\n", "");
publicKey = publicKey.replaceFirst("-----END (.*)-----", "");
publicKey = publicKey.replaceAll("\r\n", "");
publicKey = publicKey.replaceAll("\n", "");
byte[] encoded = Base64.getDecoder().decode(publicKey);
KeyFactory kf = KeyFactory.getInstance(algorithm);
return kf.generatePublic(new X509EncodedKeySpec(encoded));
} catch (Exception e) {
throw new RuntimeException("Cannot create public key from PEM string", e);
}
}
/**
* Verifies the token validity
*
* @param token the base64 JWT token string
* @param publicKey the realm's public key on server
* @return <code>true</code> if the token is valid, <code>false</code> otherwise
* @throws Exception if an error occurs constructing the verifier
*/
public static boolean isValid(String token, PublicKey publicKey) throws Exception {
return isValid(token, publicKey, true);
}
/**
* Verifies the token validity
*
* @param token the base64 JWT token string
* @param publicKey the public key to use for verification
* @param checkExpiration if <code>false</code> token expiration check is disabled
* @return <code>true</code> if the token is valid, <code>false</code> otherwise
* @throws Exception if an error occurs constructing the verifier
*/
public static boolean isValid(String token, PublicKey publicKey, boolean checkExpiration) throws Exception {
JwtParser jwtParser = Jwts.parser().json(new GcubeJacksonDeserializer()).verifyWith(publicKey).build();
try {
jwtParser.parse(token);
return true;
} catch (ExpiredJwtException e) {
// This is OK because expiration check is after the signature validation in the implementation
if (logger.isDebugEnabled()) {
logger.debug("JWT is expired: {}", e.getMessage());
}
return !checkExpiration;
} catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("JWT is not verified: {}", e.getMessage());
}
return false;
}
}
public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse) throws Exception {
return getAccessTokenPayloadJSONStringFrom(tokenResponse, true);
}
public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse, boolean prettyPrint)
throws Exception {
return toJSONString(getAccessTokenFrom(tokenResponse, Object.class), prettyPrint);
}
@ -145,4 +240,23 @@ public class ModelUtils {
return "";
}
}
}
public static class GcubeJacksonDeserializer implements Deserializer<Map<String,?>> {
@Override
public Map<String, ?> deserialize(byte[] bytes) throws DeserializationException {
return deserialize(new InputStreamReader(new ByteArrayInputStream(bytes)));
}
@Override
public Map<String, ?> deserialize(Reader reader) throws DeserializationException {
try {
return new ObjectMapper().readValue(reader, new TypeReference<HashMap<String,Object>>() {});
} catch (IOException e) {
throw new DeserializationException("Cannot deserialize JSON with GCube Jackson", e);
}
}
}
}

View File

@ -1,15 +1,30 @@
package org.gcube.common.keycloak.model;
/**
* @author <a href="mailto:mauro.mugnaini@nubisware.com">Mauro Mugnaini</a>
*/
public class OIDCConstants {
public static final String PERMISSION_PARAMETER = "permission";
public static final String GRANT_TYPE_PARAMETER = "grant_type";
public static final String SCOPE_PARAMETER = "scope";
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 TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
public static final String AUDIENCE_PARAMETER = "audience";
public static final String REFRESH_TOKEN_GRANT_TYPE = "refresh_token";
public static final String REFRESH_TOKEN_PARAMETER = "refresh_token";
public static final String CLIENT_ID_PARAMETER = "client_id";
public static final String CLIENT_SECRET_PARAMETER = "client_secret";
public static final String TOKEN_PARAMETER = "token";
public static final String PASSWORD_GRANT_TYPE = "password";
public static final String USERNAME_PARAMETER = "username";
public static final String PASSWORD_PARAMETER = "password";
public static final String SUBJECT_TOKEN_PARAMETER = "subject_token";
public static final String SUBJECT_TOKEN_TYPE_PARAMETER = "subject_token_type";
public static final String REQUESTED_TOKEN_TYPE_PARAMETER = "requested_token_type";
public static final String ACCESS_TOKEN_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
public static final String REFRESH_TOKEN_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:refresh_token";
public static final String OFFLINE_ACCESS_SCOPE = "offline_access";
}

View File

@ -0,0 +1,107 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import java.security.interfaces.RSAPublicKey;
import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author (modified by) <a href="mailto:mauro.mugnaini@nubisware.com">Mauro Mugnaini</a>
*/
public class PublishedRealmRepresentation {
protected String realm;
@JsonProperty("public_key")
protected String publicKeyPem;
@JsonProperty("token-service")
protected String tokenServiceUrl;
@JsonProperty("account-service")
protected String accountServiceUrl;
@JsonProperty("tokens-not-before")
protected int notBefore;
@JsonIgnore
protected volatile transient RSAPublicKey publicKey;
public String getRealm() {
return realm;
}
public void setRealm(String realm) {
this.realm = realm;
}
public String getPublicKeyPem() {
return publicKeyPem;
}
public void setPublicKeyPem(String publicKeyPem) {
this.publicKeyPem = publicKeyPem;
this.publicKey = null;
}
@JsonIgnore
public RSAPublicKey getPublicKey() {
if (publicKey != null)
return publicKey;
if (publicKeyPem != null) {
try {
publicKey = ModelUtils.createRSAPublicKey(publicKeyPem);
} catch (Exception e) {
e.printStackTrace();
}
}
return publicKey;
}
@JsonIgnore
public void setPublicKey(RSAPublicKey publicKey) {
this.publicKey = publicKey;
// this.publicKeyPem = PemUtils.encodeKey(publicKey);
}
public String getTokenServiceUrl() {
return tokenServiceUrl;
}
public void setTokenServiceUrl(String tokenServiceUrl) {
this.tokenServiceUrl = tokenServiceUrl;
}
public String getAccountServiceUrl() {
return accountServiceUrl;
}
public void setAccountServiceUrl(String accountServiceUrl) {
this.accountServiceUrl = accountServiceUrl;
}
public int getNotBefore() {
return notBefore;
}
public void setNotBefore(int notBefore) {
this.notBefore = notBefore;
}
}

View File

@ -1,5 +1,24 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
*/
public class RefreshToken extends AccessToken {
private static final long serialVersionUID = 2646534143077862960L;

View File

@ -0,0 +1,43 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import java.util.List;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
import org.gcube.common.keycloak.model.idm.authorization.Permission;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class TokenIntrospectionResponse extends JsonWebToken {
private static final long serialVersionUID = -3105799239959636906L;
@JsonProperty
private Boolean active;
private List<Permission> permissions;
public Boolean getActive() {
return this.active;
}
public List<Permission> getPermissions() {
return this.permissions;
}
}

View File

@ -10,6 +10,9 @@ import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author <a href="mailto:mauro.mugnaini@nubisware.com">Mauro Mugnaini</a>
*/
public class TokenResponse implements Serializable {
protected static Logger logger = LoggerFactory.getLogger(TokenResponse.class);
@ -60,7 +63,7 @@ public class TokenResponse implements Serializable {
return accessToken;
}
public void setSccessToken(String accessToken) {
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}

View File

@ -1,3 +1,19 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model;
import java.util.HashMap;
@ -295,7 +311,7 @@ public class UserInfo {
/**
* This is a map of any other claims and data that might be in the UserInfo. Could be custom claims set up by the auth server
*
* @return
* @return the object's other claims
*/
@JsonAnyGetter
public Map<String, Object> getOtherClaims() {

View File

@ -0,0 +1,138 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model.idm.authorization;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.gcube.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.gcube.com.fasterxml.jackson.annotation.JsonInclude;
import org.gcube.com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class Permission {
@JsonProperty("rsid")
private String resourceId;
@JsonProperty("rsname")
private String resourceName;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Set<String> scopes;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map<String, Set<String>> claims;
public Permission() {
this(null, null, null, null);
}
public Permission(final String resourceId, final Set<String> scopes) {
this(resourceId, null, scopes, null);
}
public Permission(final String resourceId, String resourceName, final Set<String> scopes, Map<String, Set<String>> claims) {
this.resourceId = resourceId;
this.resourceName = resourceName;
this.scopes = scopes;
this.claims = claims;
}
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public String getResourceId() {
if (resourceId == null || "".equals(resourceId.trim())) {
return null;
}
return this.resourceId;
}
public void setResourceName(String resourceName) {
this.resourceName = resourceName;
}
public String getResourceName() {
return this.resourceName;
}
public Set<String> getScopes() {
if (this.scopes == null) {
this.scopes = new HashSet<>();
}
return this.scopes;
}
public Map<String, Set<String>> getClaims() {
return claims;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !getClass().isAssignableFrom(o.getClass())) return false;
Permission that = (Permission) o;
if (getResourceId() != null || getResourceName() != null) {
if (!getResourceId().equals(that.resourceId)) {
return false;
}
if (getScopes().isEmpty() && that.getScopes().isEmpty()) {
return true;
}
} else if (that.resourceId != null) {
return false;
}
for (String scope : that.getScopes()) {
if (getScopes().contains(scope)) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(resourceId);
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Permission {").append("id=").append(resourceId).append(", name=").append(resourceName)
.append(", scopes=").append(scopes).append("}");
return builder.toString();
}
public void setScopes(Set<String> scopes) {
this.scopes = scopes;
}
}

View File

@ -1,3 +1,19 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model.util;
import java.io.IOException;

View File

@ -1,3 +1,19 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model.util;
import java.io.IOException;

View File

@ -1,3 +1,19 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model.util;
import java.io.IOException;

View File

@ -1,13 +1,32 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gcube.common.keycloak.model.util;
import java.util.Date;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Time {
private static int offset;
/**
* Returns current time in seconds adjusted by adding {@link #offset) seconds.
* Returns current time in seconds adjusted by adding {@link #offset} seconds.
* @return see description
*/
public static int currentTime() {
@ -15,7 +34,7 @@ public class Time {
}
/**
* Returns current time in milliseconds adjusted by adding {@link #offset) seconds.
* Returns current time in milliseconds adjusted by adding {@link #offset} seconds.
* @return see description
*/
public static long currentTimeMillis() {

View File

@ -1,10 +1,15 @@
package org.gcube.common.keycloak;
import java.net.URL;
import static org.junit.Assert.assertTrue;
import java.net.URL;
import java.util.Collections;
import org.gcube.common.keycloak.model.JSONWebKeySet;
import org.gcube.common.keycloak.model.ModelUtils;
import org.gcube.common.keycloak.model.PublishedRealmRepresentation;
import org.gcube.common.keycloak.model.TokenIntrospectionResponse;
import org.gcube.common.keycloak.model.TokenResponse;
import org.gcube.common.scope.api.ScopeProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -19,109 +24,522 @@ public class TestKeycloakClient {
protected static final Logger logger = LoggerFactory.getLogger(TestKeycloakClient.class);
private static final String DEV_ENDPOINT = "https://accounts.dev.d4science.org/auth/realms/d4science/protocol/openid-connect/token";
private static final String CLIENT_ID = "keycloak-client";
private static final String CLIENT_SECRET = "38f76152-2b7c-418f-9b67-66f4cc2f401e";
private static final String TEST_AUDIENCE = "conductor-server";
protected static final String DEV_ROOT_CONTEXT = "/gcube";
protected static final String DEV_BASE_URL = "https://url.gcube.d4science.org/auth/realms/d4science";
protected static final String DEV_TOKEN_ENDPOINT = DEV_BASE_URL + "/protocol/openid-connect/token";
protected static final String DEV_INTROSPECTION_ENDPOINT = DEV_TOKEN_ENDPOINT + "/introspect";
protected static final String DEV_AVATAR_ENDPOINT = DEV_BASE_URL + "/account-avatar";
private static TokenResponse oidcTR = null;
private static TokenResponse umaTR = null;
protected static final String ACCOUNTS_DEV_INTROSPECTION_ENDPOINT = "https://accounts.dev.d4science.org/auth/realms/d4science/protocol/openid-connect/token/introspect";
protected static final String TOKEN_RESTRICTION_VRE_CONTEXT = "%2Fgcube%2Fdevsec%2FCCP";
protected static final String CLIENT_ID = "keycloak-client-unit-test";
protected static final String CLIENT_SECRET = "ebf6f82e-9511-408e-8321-203081e472d8";
protected static final String TEST_AUDIENCE = "conductor-server";
protected static final String TEST_USER_USERNAME = "testuser";
protected static final String TEST_USER_PASSWORD = "t35tp455";
protected static final String OLD_OIDC_ACCESS_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSSklZNEpoNF9qdDdvNmREY0NlUDFfS1l0akcxVExXVW9oMkQ2Tzk1bFNBIn0.eyJleHAiOjE2NTI5Nzk4NDUsImlhdCI6MTY1Mjk3OTU0NSwianRpIjoiMzQ2MjgwMWItODg4NS00YTM4LWJkNDUtNWExM2U1MGE5MGU5IiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5kZXYuZDRzY2llbmNlLm9yZy9hdXRoL3JlYWxtcy9kNHNjaWVuY2UiLCJhdWQiOlsiJTJGZ2N1YmUiLCIlMkZnY3ViZSUyRmRldnNlYyUyRmRldlZSRSIsImFjY291bnQiXSwic3ViIjoiYTQ3ZGZlMTYtYjRlZC00NGVkLWExZDktOTdlY2Q1MDQzNjBjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoia2V5Y2xvYWstY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImQ4MDk3MDBmLWEyNDUtNDI3Zi1hYzhjLTQxYjFkZDNkYTQ3MCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsIkluZnJhc3RydWN0dXJlLUNsaWVudCIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiJTJGZ2N1YmUiOnsicm9sZXMiOlsiTWVtYmVyIl19LCJrZXljbG9hay1jbGllbnQiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sIiUyRmdjdWJlJTJGZGV2c2VjJTJGZGV2VlJFIjp7InJvbGVzIjpbIk1lbWJlciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJrZXljbG9hay1jbGllbnQiLCJjbGllbnRIb3N0IjoiOTMuNjYuMTg1Ljc1IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQta2V5Y2xvYWstY2xpZW50IiwiY2xpZW50QWRkcmVzcyI6IjkzLjY2LjE4NS43NSJ9.FQu4ox2HWeqeaY7nHYVGeJVpkJOcASfOb8tbOUeG-GB6sMjRB2S8PjLLaw63r_c42yxKszP04XdxGqIWqXTtoD9QCiUHTT5yJTkIpio4tMMGHth9Fbx-9dwk0yy_IFi1_OsCvZFmOQRdjMuUkj1lSqslCzAw-2E5q1Zt415-au5pEVJYNTFqIsG72ChJwh6eq1Dh1XBy8krb7YVPQyIwxO_awgAYO5hbsdvXYlRfCrnB38kk2V6-CQ-XYoL1m7xIB-gjhKCiFvDmmntQSRCZFgb0qi8eOmh9FdzPxZgx7yPJwAAj17dS4B_gz9FpZBVciNzpA6Lf4P2bqvoD9-R6ow";
protected static final String OLD_UMA_ACCESS_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSSklZNEpoNF9qdDdvNmREY0NlUDFfS1l0akcxVExXVW9oMkQ2Tzk1bFNBIn0.eyJleHAiOjE2NTI5ODA0NzgsImlhdCI6MTY1Mjk4MDE3OCwianRpIjoiNjBkNzU3MGMtZmQxOC00NGQ1LTg1MzUtODhlMmFmOGQ1ZTgwIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5kZXYuZDRzY2llbmNlLm9yZy9hdXRoL3JlYWxtcy9kNHNjaWVuY2UiLCJhdWQiOiJjb25kdWN0b3Itc2VydmVyIiwic3ViIjoiYTQ3ZGZlMTYtYjRlZC00NGVkLWExZDktOTdlY2Q1MDQzNjBjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoia2V5Y2xvYWstY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjI3NDUyN2M5LWNkZjMtNGM2Yi1iNTUxLTFmMTRkZGE5ZGVlZiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiSW5mcmFzdHJ1Y3R1cmUtQ2xpZW50Il19LCJhdXRob3JpemF0aW9uIjp7InBlcm1pc3Npb25zIjpbeyJzY29wZXMiOlsiZ2V0Il0sInJzaWQiOiIyNDlmZDQ2OS03OWM1LTRiODUtYjE5NS1mMjliM2ViNjAzNDUiLCJyc25hbWUiOiJtZXRhZGF0YSJ9LHsic2NvcGVzIjpbImdldCIsInN0YXJ0IiwidGVybWluYXRlIl0sInJzaWQiOiJhNmYzZWFkZS03NDA0LTRlNWQtOTA3MC04MDBhZGI1YWFjNGUiLCJyc25hbWUiOiJ3b3JrZmxvdyJ9LHsicnNpZCI6IjFiNmMwMGI3LTkxMzktNGVhYS1hYWM3LTIwMjMxZmVlMDVhNSIsInJzbmFtZSI6IkRlZmF1bHQgUmVzb3VyY2UifV19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJjbGllbnRJZCI6ImtleWNsb2FrLWNsaWVudCIsImNsaWVudEhvc3QiOiI5My42Ni4xODUuNzUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1rZXljbG9hay1jbGllbnQiLCJjbGllbnRBZGRyZXNzIjoiOTMuNjYuMTg1Ljc1In0.Hh62E56R-amHwoDPFQEylMvrvmNzWnC_4bDI7_iQYAPJ5YzCNH9d7zcdGaQ96kRmps_JRc2Giv_1W9kYorOhlXl-5QLDrSoqrqFxrNpEGG5r5jpNJbusbu4wNUKiCt_GMnM1UmztgXiQeuggNGkmeBIjotj0eubnmIbUV9ukHj3v7Z5PwNKKX3BCpsghd1u8lg6Nfqk_Oho4GXUfdaFY_AR3SNqzVI_9YLhND_a03MNNWlnfOvj8T4nDCKBZIs91tVyiu98d2TjnQt8PdlVwokMP3LA58m0Khy2cmUm1KF2k0zlzP8MxV9wTxNrpovMr-PnbtEPZ_IlVQIzHwjHfwQ";
@Before
public void setUp() throws Exception {
ScopeProvider.instance.set("/gcube");
}
@After
public void tearDown() throws Exception {
// Assure to reset the factory's default base URL
KeycloakClientFactory.setCustomBaseURL(null);
}
@Test
public void test01EndpointDiscovery() throws Exception {
logger.info("*** [0.1] Start testing Keycloak endpoint discovery...");
URL url = KeycloakClientFactory.newInstance().findTokenEndpointURL();
Assert.assertNotNull(url);
Assert.assertTrue(url.getProtocol().equals("https"));
Assert.assertEquals(new URL(DEV_ENDPOINT), url);
logger.info("Discovered URL is: {}", url);
public void test00TokenEndpointConstruction() throws Exception {
logger.info(
"*** [0.0] Start testing Keycloak token endpoint construction from base URL computed from context...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
logger.info("*** [0.0] Constructed token URL is: {}", tokenURL);
URL devTokenURL = new URL(DEV_TOKEN_ENDPOINT);
logger.info("*** [0.0] DEV token URL is: {}", devTokenURL);
Assert.assertNotNull(tokenURL);
Assert.assertEquals(tokenURL.getProtocol(), "https");
Assert.assertEquals(devTokenURL.toString(), tokenURL.toString());
}
@Test
public void test11QueryOIDCTokenWithDiscoveryInCurrentScope() throws Exception {
logger.info("*** [1.1] Start testing query OIDC token from Keycloak with endpoint discovery and current scope...");
oidcTR = KeycloakClientFactory.newInstance().queryOIDCToken(CLIENT_ID, CLIENT_SECRET);
TestModels.checkTokenResponse(oidcTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID);
public void test01IntrospectionEndpointConstruction() throws Exception {
logger.info("*** [0.1] Start testing Keycloak introspection endpoint construction from base URL...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
logger.info("*** [0.1] Constructed introspection URL is: {}", introspectionURL);
URL devIntrospectionURL = new URL(DEV_INTROSPECTION_ENDPOINT);
logger.info("*** [0.1] DEV introspection URL is: {}", devIntrospectionURL);
Assert.assertNotNull(introspectionURL);
Assert.assertEquals(introspectionURL.getProtocol(), "https");
Assert.assertEquals(devIntrospectionURL.toString(), introspectionURL.toString());
logger.info("Constructed URL is: {}", introspectionURL);
}
@Test
public void test02IntrospectEndpointCompute() throws Exception {
logger.info("*** [0.2] Start testing Keycloak userinfo endpoint computed from provided URL string...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
logger.info("*** [0.2] Constructed introspection URL is: {}", introspectionURL);
URL computedIntrospectionURL = client
.computeIntrospectionEndpointURL(client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT)));
logger.info("*** [0.2] Computed introspection URL is: {}", computedIntrospectionURL);
URL devIntrospectionURL = new URL(DEV_INTROSPECTION_ENDPOINT);
logger.info("*** [0.2] DEV introspection URL is: {}", devIntrospectionURL);
Assert.assertNotNull(computedIntrospectionURL);
Assert.assertEquals(computedIntrospectionURL.getProtocol(), "https");
Assert.assertEquals(introspectionURL.toString(), computedIntrospectionURL.toString());
Assert.assertEquals(devIntrospectionURL.toString(), computedIntrospectionURL.toString());
}
@Test
public void test03AvatarEndpointConstruction() throws Exception {
logger.info("*** [0.3] Start testing Keycloak avatar endpoint construction from base URL...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL avatarURL = client.getAvatarEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
logger.info("*** [0.3] Constructed avatar URL is: {}", avatarURL);
URL devAvatarURL = new URL(DEV_AVATAR_ENDPOINT);
logger.info("*** [0.3] DEV avatar URL is: {}", devAvatarURL);
Assert.assertNotNull(avatarURL);
Assert.assertEquals(avatarURL.getProtocol(), "https");
Assert.assertEquals(devAvatarURL.toString(), avatarURL.toString());
}
@Test
public void test04CustomEndpointTest() throws Exception {
String customBase = "https://accounts.cloud.dev.d4science.org/auth/realms/";
logger.info("*** [0.4] Start testing Keycloak token endpoint construction from custom base URL {}...",
customBase);
KeycloakClientFactory.setCustomBaseURL(customBase);
KeycloakClient client = KeycloakClientFactory.newInstance();
URL customBaseURL = client.getRealmBaseURL(DEV_ROOT_CONTEXT);
logger.info("*** [0.4] Constructed token URL is: {}", customBaseURL);
URL devTokenURL = new URL(DEV_TOKEN_ENDPOINT);
logger.info("*** [0.4] DEV token URL is: {}", devTokenURL);
Assert.assertNotNull(customBaseURL);
Assert.assertEquals(customBaseURL.getProtocol(), "https");
Assert.assertEquals(customBase + KeycloakClient.DEFAULT_REALM + "/", customBaseURL.toString());
}
@Test
public void test10aQueryRealmInfo() throws Exception {
logger.info("*** [1.0a] Start testing query realm info...");
KeycloakClient client = KeycloakClientFactory.newInstance();
PublishedRealmRepresentation realmInfo = client.getRealmInfo(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
logger.info("*** [1.0a] Realm info public key PEM: {}", realmInfo.getPublicKeyPem());
logger.info("*** [1.0a] Realm info public key: {}", realmInfo.getPublicKey());
}
@Test
public void test10bQueryRealmJWK() throws Exception {
logger.info("*** [1.0b] Start testing query realm JWK...");
KeycloakClient client = KeycloakClientFactory.newInstance();
JSONWebKeySet jsonWebKeySet = client
.getRealmJSONWebKeySet(client.getJWKEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT)));
for (int i = 0; i < jsonWebKeySet.getKeys().length; i++) {
logger.info("*** [1.0b] Realm JWK public key {} algorithm: {}", i,
jsonWebKeySet.getKeys()[i].getAlgorithm());
}
}
@Test
public void test11TestAccessTokenJWTSignature() throws Exception {
logger.info("*** [1.0] Start testing access token JTW signature with model utils...");
KeycloakClient client = KeycloakClientFactory.newInstance();
PublishedRealmRepresentation realmInfo = client.getRealmInfo(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
logger.info("*** [1.0] Realm info public key PEM: {}", realmInfo.getPublicKeyPem());
logger.info("*** [1.0] Realm info public key: {}", realmInfo.getPublicKey());
TokenResponse oidcTR = client.queryOIDCToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET);
logger.info("*** [1.0] OIDC access token: {}", oidcTR.getAccessToken());
Assert.assertTrue("Access token is not valid",
ModelUtils.isValid(oidcTR.getAccessToken(), realmInfo.getPublicKey()));
}
@Test
public void test12QueryOIDCToken() throws Exception {
logger.info("*** [1.2] Start testing query OIDC token from Keycloak with URL...");
oidcTR = KeycloakClientFactory.newInstance().queryOIDCToken(new URL(DEV_ENDPOINT), CLIENT_ID, CLIENT_SECRET);
TestModels.checkTokenResponse(oidcTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID);
logger.info("*** [1.2] Start testing query OIDC token from Keycloak with context...");
TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCToken(DEV_ROOT_CONTEXT, CLIENT_ID,
CLIENT_SECRET);
logger.info("*** [1.2] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.2] OIDC refresh token: {}", oidcTR.getRefreshToken());
TestModelUtils.checkTokenResponse(oidcTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false);
}
@Test
public void test13RefreshOIDCTokenWithDiscovery() throws Exception {
logger.info("*** [1.3] Start testing refresh OIDC token from Keycloak with endpoint discovery...");
TokenResponse refreshedTR = KeycloakClientFactory.newInstance().refreshToken(CLIENT_ID, CLIENT_SECRET, oidcTR);
TestModels.checkTokenResponse(refreshedTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(refreshedTR), "service-account-" + CLIENT_ID);
TestModels.checkRefreshToken(ModelUtils.getRefreshTokenFrom(refreshedTR));
public void test12aQueryOIDCToken() throws Exception {
logger.info("*** [1.2a] Start testing query OIDC token from Keycloak with URL...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenResponse oidcTR = client.queryOIDCToken(tokenURL, CLIENT_ID, CLIENT_SECRET);
logger.info("*** [1.2a] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.2a] OIDC refresh token: {}", oidcTR.getRefreshToken());
TestModelUtils.checkTokenResponse(oidcTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false);
}
@Test
public void test21QueryUMATokenWithDiscoveryInCurrentScope() throws Exception {
public void test13QueryOIDCTokenOfUser() throws Exception {
logger.info("*** [1.3] Start testing query OIDC token from Keycloak with context for user...");
TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID,
CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD);
logger.info("*** [1.3] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.3] OIDC refresh token: {}", oidcTR.getRefreshToken());
TestModelUtils.checkTokenResponse(oidcTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, false);
}
@Test
public void test13aQueryOIDCTokenOfUserWithContext() throws Exception {
logger.info("*** [1.3a] Start testing query OIDC token for audience from Keycloak with context for user...");
TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUser(DEV_ROOT_CONTEXT,
CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD);
logger.info("*** [1.3a] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.3a] OIDC refresh token: {}", oidcTR.getRefreshToken());
TestModelUtils.checkTokenResponse(oidcTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true);
TokenResponse oidcRestrictedTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUserWithContext(
DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD,
TOKEN_RESTRICTION_VRE_CONTEXT);
logger.info("*** [1.3a] OIDC restricted access token: {}", oidcRestrictedTR.getAccessToken());
logger.info("*** [1.3a] OIDC restricted refresh token: {}", oidcRestrictedTR.getRefreshToken());
TestModelUtils.checkTokenResponse(oidcTR);
TestModelUtils.checkTokenResponse(oidcRestrictedTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcRestrictedTR), TEST_USER_USERNAME, true);
assertTrue(ModelUtils.getAccessTokenFrom(oidcTR).getAudience().length > 1);
assertTrue(ModelUtils.getAccessTokenFrom(oidcRestrictedTR).getAudience().length == 1);
assertTrue(
TOKEN_RESTRICTION_VRE_CONTEXT.equals(ModelUtils.getAccessTokenFrom(oidcRestrictedTR).getAudience()[0]));
}
@Test
public void test13bQueryOIDCTokenAndUMAOfUser() throws Exception {
logger.info("*** [1.3b] Start testing query OIDC and UMA tokens from Keycloak with context for user...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
TEST_USER_USERNAME, TEST_USER_PASSWORD);
logger.info("*** [1.3b] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.3b] OIDC refresh token: {}", oidcTR.getRefreshToken());
TestModelUtils.checkTokenResponse(oidcTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, false);
TokenResponse umaTR = client.queryUMAToken(DEV_ROOT_CONTEXT, oidcTR, TEST_AUDIENCE, null);
logger.info("*** [1.3b] UMA access token: {}", umaTR.getAccessToken());
logger.info("*** [1.3b] UMA refresh token: {}", umaTR.getRefreshToken());
TestModelUtils.checkTokenResponse(umaTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), TEST_USER_USERNAME, true);
}
@Test
public void test13aQueryOIDCTokenOfUserWithContextAndCustomHeader() throws Exception {
logger.info(
"*** [2.1] Start testing query UMA token from Keycloak with endpoint discovery and current scope as audience...");
"*** [1.3c] Start testing query OIDC token for audience from Keycloak with context and custom headers for user...");
TokenResponse oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUserWithContext(DEV_ROOT_CONTEXT,
CLIENT_ID, CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD, TOKEN_RESTRICTION_VRE_CONTEXT,
Collections.singletonMap("X_A_CUSTOM_HEADER", "HEADER_VALUE"));
umaTR = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, null);
TestModels.checkTokenResponse(umaTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID);
}
logger.info("*** [1.3c] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.3c] OIDC refresh token: {}", oidcTR.getRefreshToken());
@Test
public void test22QueryUMATokenWithDiscovery() throws Exception {
logger.info("*** [2.2] Start testing query UMA token from Keycloak with endpoint discovery...");
umaTR = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null);
TestModels.checkTokenResponse(umaTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID);
}
@Test
public void test23QueryUMATokenWithDiscoveryWithOIDCAuthorization() throws Exception {
logger.info(
"*** [2.3] Start testing query UMA token from Keycloak with endpoint discovery and OIDC access token for authorization...");
umaTR = KeycloakClientFactory.newInstance().queryUMAToken(oidcTR, TEST_AUDIENCE, null);
TestModels.checkTokenResponse(umaTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID);
TestModelUtils.checkTokenResponse(oidcTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true);
assertTrue(ModelUtils.getAccessTokenFrom(oidcTR).getAudience().length == 1);
// It is not possible to check programmatically if the header has been added to the call, See the logs if the Header is present.
}
@Test
public void test24QueryUMAToken() throws Exception {
logger.info("*** [2.4] Start testing query UMA token from Keycloak with URL...");
umaTR = KeycloakClientFactory.newInstance().queryUMAToken(new URL(DEV_ENDPOINT), CLIENT_ID, CLIENT_SECRET,
TEST_AUDIENCE, null);
logger.info("*** [2.4] Start testing query UMA token from Keycloak with context...");
TokenResponse umaTR = KeycloakClientFactory.newInstance().queryUMAToken(DEV_ROOT_CONTEXT, CLIENT_ID,
CLIENT_SECRET, TEST_AUDIENCE, null);
TestModels.checkTokenResponse(umaTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID);
logger.info("*** [2.4] UMA access token: {}", umaTR.getAccessToken());
logger.info("*** [2.4] UMA refresh token: {}", umaTR.getRefreshToken());
TestModelUtils.checkTokenResponse(umaTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true);
}
@Test
public void test25RefreshUMATokenWithDiscovery() throws Exception {
logger.info("*** [2.5] Start testing refresh UMA token from Keycloak with endpoint discovery...");
TokenResponse refreshedTR = KeycloakClientFactory.newInstance().refreshToken(CLIENT_ID, CLIENT_SECRET, umaTR);
TestModels.checkTokenResponse(refreshedTR);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(refreshedTR), "service-account-" + CLIENT_ID);
TestModels.checkRefreshToken(ModelUtils.getRefreshTokenFrom(refreshedTR));
public void test24aQueryUMAToken() throws Exception {
logger.info("*** [2.4a] Start testing query UMA token from Keycloak with URL...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenResponse umaTR = client.queryUMAToken(tokenURL, CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null);
logger.info("*** [2.4a] UMA access token: {}", umaTR.getAccessToken());
logger.info("*** [2.4a] UMA refresh token: {}", umaTR.getRefreshToken());
TestModelUtils.checkTokenResponse(umaTR);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true);
}
@Test(expected = KeycloakClientException.class)
public void test26RefreshTokenWithDiscoveryAndClientIdFromRefreshToken() throws Exception {
logger.info("*** [2.6] Start testing refresh UMA token *with error* since is not a public client...");
KeycloakClientFactory.newInstance().refreshToken(umaTR.getRefreshToken());
@Test
public void test302IntrospectOIDCAccessToken() throws Exception {
logger.info("*** [3.2] Start testing introspect OIDC access token...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenResponse oidcTR = client.queryOIDCToken(tokenURL, CLIENT_ID, CLIENT_SECRET);
TokenIntrospectionResponse tir = client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
oidcTR.getAccessToken());
TestModelUtils.checkTokenIntrospectionResponse(tir);
}
@Test
public void test302aIntrospectOIDCAccessToken() throws Exception {
logger.info("*** [3.2a] Start testing introspect OIDC access token...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenResponse oidcTR = client.queryOIDCToken(tokenURL, CLIENT_ID, CLIENT_SECRET);
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenIntrospectionResponse tir = client.introspectAccessToken(introspectionURL, CLIENT_ID, CLIENT_SECRET,
oidcTR.getAccessToken());
TestModelUtils.checkTokenIntrospectionResponse(tir);
}
@Test
public void test302bIntrospectOIDCAccessTokenFromDifferentServer() throws Exception {
logger.info("*** [3.2b] Start testing introspect OIDC access token with different base URLs...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenResponse oidcTR = client.queryOIDCToken(tokenURL, CLIENT_ID, CLIENT_SECRET);
logger.info("*** [3.2b] OIDC access token: {}", oidcTR.getAccessToken());
URL introspectionURL = new URL(ACCOUNTS_DEV_INTROSPECTION_ENDPOINT);
TokenIntrospectionResponse tir = client.introspectAccessToken(introspectionURL, CLIENT_ID, CLIENT_SECRET,
oidcTR.getAccessToken());
TestModelUtils.checkTokenIntrospectionResponse(tir, true);
}
@Test
public void test304IntrospectUMAAccessToken() throws Exception {
logger.info("*** [3.4] Start testing introspect UMA access token...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse umaTR = client.queryUMAToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null);
TokenIntrospectionResponse tir = client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
umaTR.getAccessToken());
TestModelUtils.checkTokenIntrospectionResponse(tir);
}
@Test
public void test304aIntrospectUMAAccessToken() throws Exception {
logger.info("*** [3.4a] Start testing introspect UMA access token...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse umaTR = client.queryUMAToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null);
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenIntrospectionResponse tir = client.introspectAccessToken(introspectionURL, CLIENT_ID, CLIENT_SECRET,
umaTR.getAccessToken());
TestModelUtils.checkTokenIntrospectionResponse(tir);
}
@Test
public void test306OIDCAccessTokenVerification() throws Exception {
logger.info("*** [3.6] Start OIDC access token verification...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenResponse oidcTR = client.queryOIDCToken(tokenURL, CLIENT_ID, CLIENT_SECRET);
Assert.assertTrue(
client.isAccessTokenVerified(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()));
}
@Test
public void test306aOIDCAccessTokenVerification() throws Exception {
logger.info("*** [3.6a] Start OIDC access token verification...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL tokenURL = client.getTokenEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
TokenResponse oidcTR = client.queryOIDCToken(tokenURL, CLIENT_ID, CLIENT_SECRET);
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
Assert.assertTrue(
client.isAccessTokenVerified(introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()));
}
@Test
public void test307OIDCAccessTokenNonVerification() throws Exception {
logger.info("*** [3.7] Start OIDC access token NON verification...");
Assert.assertFalse(KeycloakClientFactory.newInstance().isAccessTokenVerified(
DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, OLD_OIDC_ACCESS_TOKEN));
}
@Test
public void test307aOIDCAccessTokenNonVerification() throws Exception {
logger.info("*** [3.7a] Start OIDC access token NON verification...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
Assert.assertFalse(
client.isAccessTokenVerified(introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_OIDC_ACCESS_TOKEN));
}
@Test
public void test309UMAAccessTokenVerification() throws Exception {
logger.info("*** [3.9] Start UMA access token verification...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse umaTR = client.queryUMAToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null);
Assert.assertTrue(
client.isAccessTokenVerified(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken()));
}
@Test
public void test309aUMAAccessTokenVerification() throws Exception {
logger.info("*** [3.9a] Start UMA access token verification...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse umaTR = client.queryUMAToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null);
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
Assert.assertTrue(
client.isAccessTokenVerified(introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken()));
}
@Test
public void test310UMAAccessTokenNonVerification() throws Exception {
logger.info("*** [3.10] Start UMA access token NON verification...");
Assert.assertFalse(KeycloakClientFactory.newInstance().isAccessTokenVerified(
DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, OLD_UMA_ACCESS_TOKEN));
}
@Test
public void test310aUMAAccessTokenNonVerification() throws Exception {
logger.info("*** [3.10a] Start UMA access token NON verification...");
KeycloakClient client = KeycloakClientFactory.newInstance();
URL introspectionURL = client.getIntrospectionEndpointURL(client.getRealmBaseURL(DEV_ROOT_CONTEXT));
Assert.assertFalse(
client.isAccessTokenVerified(introspectionURL, CLIENT_ID, CLIENT_SECRET, OLD_UMA_ACCESS_TOKEN));
}
@Test
public void test4GetTokenOfUser() throws Exception {
logger.info("*** [4] Start testing query token from Keycloak for user...");
TokenResponse tokenResponse = KeycloakClientHelper.getTokenForUser(DEV_ROOT_CONTEXT, TEST_USER_USERNAME,
TEST_USER_PASSWORD);
logger.info("*** [4] UMA access token: {}", tokenResponse.getAccessToken());
logger.info("*** [4] UMA refresh token: {}", tokenResponse.getRefreshToken());
TestModelUtils.checkTokenResponse(tokenResponse);
TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(tokenResponse), TEST_USER_USERNAME, true);
}
@Test
public void test51ExchangeToken4Access() throws Exception {
logger.info("*** [5.1] Start testing token exchange for access token from Keycloak...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
TEST_USER_USERNAME, TEST_USER_PASSWORD);
logger.info("*** [5.1] OIDC access token: {}", oidcTR.getAccessToken());
TokenResponse exchangedTR = client.exchangeTokenForAccessToken(DEV_ROOT_CONTEXT, oidcTR.getAccessToken(),
CLIENT_ID, CLIENT_SECRET, CLIENT_ID);
logger.info("*** [5.1] Exchanged access token: {}", exchangedTR.getAccessToken());
TestModelUtils.checkTokenResponse(exchangedTR, false);
Assert.assertNull(exchangedTR.getRefreshToken());
TestModelUtils.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID,
CLIENT_SECRET, exchangedTR.getAccessToken()));
}
@Test
public void test51aExchangeUMAToken4Access() throws Exception {
logger.info("*** [5.1a] Start testing UMA token exchange for access token from Keycloak...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
TEST_USER_USERNAME, TEST_USER_PASSWORD);
logger.info("*** [5.1a] OIDC access token: {}", oidcTR.getAccessToken());
TokenResponse umaTR = client.queryUMAToken(DEV_ROOT_CONTEXT, oidcTR, TOKEN_RESTRICTION_VRE_CONTEXT, null);
logger.info("*** [5.1a] UMA access token: {}", umaTR.getAccessToken());
TokenResponse exchangedTR = client.exchangeTokenForAccessToken(DEV_ROOT_CONTEXT, umaTR.getAccessToken(),
CLIENT_ID, CLIENT_SECRET, CLIENT_ID);
logger.info("*** [5.1a] Exchanged access token: {}", exchangedTR.getAccessToken());
TestModelUtils.checkTokenResponse(exchangedTR, false);
Assert.assertNull(exchangedTR.getRefreshToken());
TestModelUtils.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID,
CLIENT_SECRET, exchangedTR.getAccessToken()));
}
@Test
public void test52ExchangeToken4refresh() throws Exception {
logger.info("*** [5.2] Start testing token exchange for refresh token from Keycloak...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
TEST_USER_USERNAME, TEST_USER_PASSWORD);
logger.info("*** [5.2] OIDC access token: {}", oidcTR.getAccessToken());
TokenResponse exchangedTR = client.exchangeTokenForRefreshToken(DEV_ROOT_CONTEXT, oidcTR.getAccessToken(),
CLIENT_ID, CLIENT_SECRET, CLIENT_ID);
logger.info("*** [5.2] Exchanged access token: {}", exchangedTR.getAccessToken());
logger.info("*** [5.2] Exchanged refresh token: {}", exchangedTR.getRefreshToken());
TestModelUtils.checkTokenResponse(exchangedTR);
TestModelUtils.checkTokenIntrospectionResponse(
client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, exchangedTR.getAccessToken()));
}
@Test(expected = IllegalArgumentException.class)
public void test53ExchangeToken4Offline() throws Exception {
logger.info("*** [5.3] Start testing token exchange for offline token from Keycloak...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
TEST_USER_USERNAME, TEST_USER_PASSWORD);
logger.info("*** [5.3] OIDC access token: {}", oidcTR.getAccessToken());
// TokenResponse exchangedTR =
client.exchangeTokenForOfflineToken(DEV_ROOT_CONTEXT, oidcTR.getAccessToken(),
CLIENT_ID, CLIENT_SECRET, CLIENT_ID);
// For the moment this part is not covered by tests
// logger.info("*** [5.3] Exchanged access token: {}", exchangedTR.getAccessToken());
// logger.info("*** [5.3] Exchanged refresh token: {}", exchangedTR.getRefreshToken());
// TestModelUtils.checkTokenResponse(exchangedTR, true);
// TestModelUtils.checkOfflineToken(exchangedTR);
//
// TestModelUtils.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID,
// CLIENT_SECRET, exchangedTR.getAccessToken()));
}
@Test
public void test6GetAvatar() throws Exception {
logger.info("*** [6] Start testing get user's avatar...");
KeycloakClient client = KeycloakClientFactory.newInstance();
TokenResponse oidcTR = client.queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET,
TEST_USER_USERNAME, TEST_USER_PASSWORD);
byte[] avatarData = client.getAvatarData(DEV_ROOT_CONTEXT, oidcTR);
Assert.assertNotNull("Avatar data is null", avatarData);
logger.info("*** [6] Avatar image of user is {} bytes", avatarData.length);
}
}

View File

@ -1,11 +1,15 @@
package org.gcube.common.keycloak;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.interfaces.RSAPublicKey;
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
import org.gcube.common.keycloak.model.AccessToken;
import org.gcube.common.keycloak.model.ModelUtils;
import org.gcube.common.keycloak.model.RefreshToken;
import org.gcube.common.keycloak.model.TokenIntrospectionResponse;
import org.gcube.common.keycloak.model.TokenResponse;
import org.junit.After;
import org.junit.Assert;
@ -17,9 +21,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestModels {
public class TestModelUtils {
protected static final Logger logger = LoggerFactory.getLogger(TestModels.class);
protected static final Logger logger = LoggerFactory.getLogger(TestModelUtils.class);
@Before
public void setUp() throws Exception {
@ -29,6 +33,22 @@ public class TestModels {
public void tearDown() throws Exception {
}
@Test
public void testTokenValidity() throws Exception {
logger.info("Start testing access token valdity...");
TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/oidc-token-response.json"),
TokenResponse.class);
RSAPublicKey publicKey = ModelUtils.createRSAPublicKey(
new String(Files.readAllBytes(Paths.get("src/test/resources/rsa-public-key.pem"))));
// Valid signature
Assert.assertFalse("Token is valid", ModelUtils.isValid(tr.getAccessToken(), publicKey));
Assert.assertFalse("Token is not expired", ModelUtils.isValid(tr.getAccessToken(), publicKey, true));
Assert.assertTrue("Token is valid", ModelUtils.isValid(tr.getAccessToken(), publicKey, false));
Assert.assertFalse("Token signature is valid", ModelUtils.isValid(tr.getAccessToken().replace("ZV9hY2Nlc3", "ZV9hY2Nlcc"), publicKey));
}
@Test
public void testTokenResponseForOIDC() throws Exception {
logger.info("Start testing OIDC token response object binding...");
@ -37,7 +57,7 @@ public class TestModels {
logger.debug("OIDC token response:\n{}", ModelUtils.toJSONString(tr, true));
checkTokenResponse(tr);
}
@Test
@ -56,7 +76,7 @@ public class TestModels {
AccessToken at = new ObjectMapper().readValue(new File("src/test/resources/uma-access-token.json"),
AccessToken.class);
checkAccessToken(at, null);
checkAccessToken(at, null, true);
}
@Test
@ -68,9 +88,9 @@ public class TestModels {
AccessToken at2 = ModelUtils.getAccessTokenFrom("Bearer " + tr.getAccessToken());
AccessToken at3 = ModelUtils.getAccessTokenFrom("bearer " + tr.getAccessToken());
checkAccessToken(at1, null);
checkAccessToken(at2, null);
checkAccessToken(at3, null);
checkAccessToken(at1, null, true);
checkAccessToken(at2, null, true);
checkAccessToken(at3, null, true);
Assert.assertEquals(ModelUtils.toJSONString(at1), ModelUtils.toJSONString(at2));
Assert.assertEquals(ModelUtils.toJSONString(at2), ModelUtils.toJSONString(at3));
}
@ -85,25 +105,52 @@ public class TestModels {
}
public static void checkTokenResponse(TokenResponse tr) throws Exception {
Assert.assertNotNull(tr);
Assert.assertEquals("bearer", tr.getTokenType().toLowerCase());
Assert.assertNotNull(tr.getAccessToken());
Assert.assertNotNull(tr.getRefreshToken());
checkTokenResponse(tr, true);
}
public static void checkAccessToken(AccessToken at, String preferredUsername) {
public static void checkTokenResponse(TokenResponse tr, boolean checkAlsoRefreshToken) throws Exception {
Assert.assertNotNull(tr);
Assert.assertEquals("bearer", tr.getTokenType().toLowerCase());
Assert.assertNotNull("Access token is null", tr.getAccessToken());
if (checkAlsoRefreshToken) {
Assert.assertNotNull("Refresh token is null", tr.getRefreshToken());
}
}
public static void checkAccessToken(AccessToken at, String preferredUsername, boolean checkAudience) {
logger.debug("Access token:\n{}", ModelUtils.toJSONString(at, true));
Assert.assertNotNull(at.getPreferredUsername());
if (preferredUsername != null) {
Assert.assertEquals(preferredUsername, at.getPreferredUsername());
}
Assert.assertNotNull(at.getAudience());
if (checkAudience) {
Assert.assertNotNull("Audience is null", at.getAudience());
}
}
public static void checkRefreshToken(RefreshToken rt) {
logger.debug("Refresh token:\n{}", ModelUtils.toJSONString(rt, true));
Assert.assertNotNull(rt.getOtherClaims());
Assert.assertNotNull(rt.getAudience());
Assert.assertNotNull("Other claims are null", rt.getOtherClaims());
Assert.assertNotNull("Audience is null", rt.getAudience());
}
public static void checkOfflineToken(TokenResponse tr) throws Exception {
RefreshToken rt = ModelUtils.getRefreshTokenFrom(tr.getRefreshToken());
Assert.assertEquals("Offline", rt.getType());
Assert.assertNull("Expiration is not null", rt.getExp());
}
public static void checkTokenIntrospectionResponse(TokenIntrospectionResponse tir) {
checkTokenIntrospectionResponse(tir, true);
}
public static void checkTokenIntrospectionResponse(TokenIntrospectionResponse tir, boolean mustBeActive) {
logger.debug("Token introspection response :\n{}", ModelUtils.toJSONString(tir, true));
if (mustBeActive) {
Assert.assertTrue("Token is not active", tir.getActive());
} else {
Assert.assertFalse("Token is active", tir.getActive());
}
}
}

View File

@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjyJAmPx5K2eGYjvRmiBC8as3nF/jsYSUgBnlul9TNEdWSPuxTzntTb37xDGPmVRkNyOCJmRnBcI8GbrWz8SHJ643JTKp8yx4zDQCgLD72crb9ah/Tfu8KpDz3+FRuYLE4EvvRCGBnsFO2vSM02iTAp7nSToOCX4jCCrDMBUUJkIzuZIQUBTx8lvWl/M6LtQAqS7Gw3wsZSklRcvsR9qlCUxJW3cvhALt9wWrejSJ3LaR6TMaNa8k6Ojk6bJ3/5c6OifxYde0YjXIOaeMkkgnfoQvs4cyhvNxCXLx65+VKV4Dlts7cTgJvuodV0w/UhJGfUxgA9V6IQowmwHNBnYAMwIDAQAB