Compare commits

...

52 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
Mauro Mugnaini f7e5c29c54 Releasing v.1.2.0 2022-04-04 13:25:41 +02:00
Mauro Mugnaini f5bbec3f3a Typo in version title fixed 2022-03-30 12:05:47 +02:00
Mauro Mugnaini 49586563e2 Added tests for new OIDC token retrieve methods and UMA from OIDC token method 2022-03-30 12:02:23 +02:00
Mauro Mugnaini 4c769f329f Added OIDC token retrieve for clients [#23076] and UMA token from OIDC token instead for credentials 2022-03-30 12:01:42 +02:00
Mauro Mugnaini 7d3f508954 Releasing version `1.1.0` 2022-01-13 15:06:38 +01:00
Mauro Mugnaini 4ab2c18440 Test with error (unhautorized) for the refresh token with only the refresh token, since a `confidential` client is used for testing. 2021-12-17 17:59:31 +01:00
Mauro Mugnaini defc33913f Helper function to crete a `RefreshToken` object from its base64 encoded JWT string added 2021-12-17 17:58:22 +01:00
Mauro Mugnaini f7ab942a5c Added refresh token from the encoded JWT form only of the `refresh_token` function; for public client types only. 2021-12-17 17:56:56 +01:00
Mauro Mugnaini 6ed1591974 Added OIDC access code example 2021-12-17 17:55:07 +01:00
Mauro Mugnaini 3e2bbeb3e2 [#22515] Added refresh token methods 2021-12-09 15:05:47 +01:00
Mauro Mugnaini f5ef1d2c92 [#22515] Added refresh token methods 2021-12-09 15:05:26 +01:00
32 changed files with 3164 additions and 288 deletions

View File

@ -2,5 +2,27 @@ 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)
## [v1.1.0]
- Added refresh token facilities for expired tokens (#22515) and some helper methods added.
## [v1.0.1]
- First release (#21389 #22155) provides the basic helper classes for Keycloak tokens retrieve and functions for the gCube framework integration (automatic service discovery).

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.0.1</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,11 +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.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;
@ -15,77 +38,437 @@ 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 {
private static final String PERMISSION_PARAMETER = "permission";
private static final String GRANT_TYPE_PARAMETER = "grant_type";
private static final String UMA_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
private static final String AUDIENCE_PARAMETER = "audience";
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);
public URL getRealmBaseURL(String context) throws KeycloakClientException {
return getRealmBaseURL(context, DEFAULT_REALM);
}
@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("\\.", "-"));
}
}
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);
ScopeProvider.instance.set(rootVOScope);
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);
List<AccessPoint> accessPoints = client.submit(query);
logger.debug("Restting scope into provider to the original value: {}", originalScope);
ScopeProvider.instance.set(originalScope);
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 queryUMAToken(String clientId, String clientSecret, List<String> permissions)
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 queryUMAToken(clientId, clientSecret, ScopeProvider.instance.get(), permissions);
return queryOIDCToken(context, clientId, clientSecret, null);
}
@Override
public TokenResponse queryUMAToken(String clientId, String clientSecret, String audience,
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, clientId, clientSecret, null);
}
@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));
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 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
public TokenResponse queryUMAToken(URL tokenURL, TokenResponse oidcTokenResponse, String audience,
List<String> permissions) throws KeycloakClientException {
return queryUMAToken(tokenURL, constructBeareAuthenticationHeader(oidcTokenResponse), audience, permissions);
}
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
@ -93,36 +476,42 @@ public class DefaultKeycloakClient implements KeycloakClient {
List<String> permissions) throws KeycloakClientException {
return queryUMAToken(tokenURL,
"Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes()),
constructBasicAuthenticationHeader(clientId, clientSecret),
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 {
if (tokenURL == null) {
throw new KeycloakClientException("'tokenURL' parameter must be not null");
}
if (authorization == null || "".equals(authorization)) {
throw new KeycloakClientException("'authorization' parameter must be not null nor empty");
}
if (audience == null || "".equals(audience)) {
throw new KeycloakClientException("'audience' parameter must be not null nor empty");
throw new KeycloakClientException("Audience must be not null nor empty");
}
logger.debug("Querying token from Keycloak server with URL: {}", tokenURL);
logger.debug("Querying UMA token from Keycloak server with URL: {}", tokenURL);
Map<String, List<String>> params = new HashMap<>();
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("Cannot URL encode 'audience'", 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 -> {
@ -134,20 +523,53 @@ public class DefaultKeycloakClient implements KeycloakClient {
}).collect(Collectors.toList()));
}
return performRequest(tokenURL, headers, params);
}
protected TokenResponse performRequest(URL tokenURL, Map<String, String> headers, Map<String, List<String>> params)
throws KeycloakClientException {
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");
}
// Constructing request object
GXHTTPStringRequest request;
try {
String queryString = params.entrySet().stream()
.flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v))
.reduce((p1, p2) -> p1 + "&" + p2).orElse("");
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");
}
}
request = GXHTTPStringRequest.newRequest(tokenURL.toString())
logger.trace("Query string is: {}", queryString);
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);
@ -156,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());
.getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0),
errorBody);
}
}
@ -185,4 +614,283 @@ public class DefaultKeycloakClient implements KeycloakClient {
return audience;
}
@Override
public TokenResponse refreshToken(String context, TokenResponse tokenResponse) throws KeycloakClientException {
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), tokenResponse);
}
@Override
public TokenResponse refreshToken(URL tokenURL, TokenResponse tokenResponse) throws KeycloakClientException {
return refreshToken(tokenURL, null, null, tokenResponse);
}
@Override
public TokenResponse refreshToken(String context, String clientId, String clientSecret, TokenResponse tokenResponse)
throws KeycloakClientException {
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, tokenResponse);
}
@Override
public TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, TokenResponse tokenResponse)
throws KeycloakClientException {
if (clientId == null) {
logger.debug("Client id not set, trying to get it from access token info");
try {
clientId = ModelUtils.getClientIdFromToken(ModelUtils.getAccessTokenFrom(tokenResponse));
} catch (Exception e) {
throw new KeycloakClientException("Cannot construct access token object from token response", e);
}
}
return refreshToken(tokenURL, clientId, clientSecret, tokenResponse.getRefreshToken());
}
@Override
public TokenResponse refreshToken(String context, String clientId, String clientSecret,
String refreshTokenJWTString) throws KeycloakClientException {
return refreshToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret,
refreshTokenJWTString);
}
@Override
public TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, String refreshTokenJWTString)
throws KeycloakClientException {
if (tokenURL == null) {
throw new KeycloakClientException("Token URL must be not null");
}
if (clientId == null || "".equals(clientId)) {
throw new KeycloakClientException("Client id must be not null nor empty");
}
if (refreshTokenJWTString == null || "".equals(clientId)) {
throw new KeycloakClientException("Refresh token JWT encoded string must be not null nor empty");
}
logger.debug("Refreshing token from Keycloak server with URL: {}", tokenURL);
try {
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,
Collections.singletonList(URLEncoder.encode(clientSecret, "UTF-8")));
}
return performRequest(tokenURL, null, params);
} catch (UnsupportedEncodingException e) {
throw new KeycloakClientException("Cannot encode parameters", e);
}
}
@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 {
at = ModelUtils.getAccessTokenFrom(oidcAccessToken);
} catch (Exception e) {
throw new IllegalArgumentException("Impossible to parse the access token as JSON", e);
}
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 {
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 the request object correctly", e);
}
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,32 +2,488 @@ 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}
* @return the keycloak endpoint URL in the current scope
* Returns the Keycloak base {@link URL} for the given context and the default realm (<code>d4science</code>)
*
* @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;
/**
* 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 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.
*
* @param tokenURL the token endpoint {@link URL} of the Keycloak server
* @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(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 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>
@ -37,6 +493,49 @@ public interface KeycloakClient {
TokenResponse queryUMAToken(URL tokenURL, String authorization, 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 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
* @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 clientId, String clientSecret, 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.
@ -50,33 +549,234 @@ public interface KeycloakClient {
* @throws KeycloakClientException if something goes wrong performing the query
*/
TokenResponse queryUMAToken(URL tokenURL, String clientId, String clientSecret, String audience,
List<String> permissions)
List<String> permissions) throws KeycloakClientException;
/**
* 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(String context, 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.
*
* 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 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(URL tokenURL, 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 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 context, String clientId, String clientSecret, TokenResponse tokenResponse)
throws KeycloakClientException;
/**
* Queries an UMA token from the discovered Keycloak server 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.
* 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 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
* @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
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse queryUMAToken(String clientId, String clientSecret, String audience, List<String> permissions)
TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, TokenResponse tokenResponse)
throws KeycloakClientException;
/**
* Queries an UMA token from the discovered Keycloak server in the current scope, by using provided clientId and client secret
* for the current scope 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
* 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
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
TokenResponse queryUMAToken(String clientId, String clientSecret, List<String> permissions)
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 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
* @return the refreshed token as {@link TokenResponse} object
* @throws KeycloakClientException if something goes wrong performing the refresh query
*/
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

@ -35,7 +35,7 @@ public class KeycloakClientException extends Exception {
super(message);
}
public KeycloakClientException(String message, Exception cause) {
public KeycloakClientException(String message, Throwable cause) {
super(message, cause);
}
@ -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,18 +1,42 @@
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);
private static final String ACCOUNT_AUDIENCE_RESOURCE = "account";
private static final ObjectMapper mapper = new ObjectMapper();
static {
@ -33,36 +57,124 @@ public class ModelUtils {
}
}
private static byte[] getDecodedPayload(String value) {
return getBase64Decoded(getEncodedPayload(value));
/**
* 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");
}
public static String getAccessTokenPayloadStringFrom(TokenResponse tokenResponse) throws Exception {
return getAccessTokenPayloadStringFrom(tokenResponse, true);
/**
* 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);
}
}
public static String getAccessTokenPayloadStringFrom(TokenResponse tokenResponse, boolean prettyPrint) throws Exception {
/**
* 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);
}
public static AccessToken getAccessTokenFrom(TokenResponse tokenResponse) throws Exception {
return getAccessTokenFrom(tokenResponse, RefreshToken.class);
return getAccessTokenFrom(tokenResponse, AccessToken.class);
}
public static AccessToken getAccessTokenFrom(String authorizationHeaderOrBase64EncodedJWT) throws Exception {
return getAccessTokenFrom(authorizationHeaderOrBase64EncodedJWT.matches("[b|B]earer ")
? authorizationHeaderOrBase64EncodedJWT.substring("bearer ".length())
: authorizationHeaderOrBase64EncodedJWT, AccessToken.class);
}
private static <T> T getAccessTokenFrom(TokenResponse tokenResponse, Class<T> clazz) throws Exception {
return mapper.readValue(getDecodedPayload(tokenResponse.getAccessToken()), clazz);
return getAccessTokenFrom(tokenResponse.getAccessToken(), clazz);
}
private static <T> T getAccessTokenFrom(String accessToken, Class<T> clazz) throws Exception {
return mapper.readValue(getDecodedPayload(accessToken), clazz);
}
public static String getRefreshTokenPayloadStringFrom(TokenResponse tokenResponse) throws Exception {
return getRefreshTokenPayloadStringFrom(tokenResponse, true);
}
public static String getRefreshTokenPayloadStringFrom(TokenResponse tokenResponse, boolean prettyPrint) throws Exception {
public static String getRefreshTokenPayloadStringFrom(TokenResponse tokenResponse, boolean prettyPrint)
throws Exception {
return toJSONString(getRefreshTokenFrom(tokenResponse, Object.class), prettyPrint);
}
public static RefreshToken getRefreshTokenFrom(TokenResponse tokenResponse) throws Exception {
return getRefreshTokenFrom(tokenResponse, RefreshToken.class);
return getRefreshTokenFrom(tokenResponse.getRefreshToken());
}
public static RefreshToken getRefreshTokenFrom(String base64EncodedJWT) throws Exception {
return mapper.readValue(getDecodedPayload(base64EncodedJWT), RefreshToken.class);
}
private static <T> T getRefreshTokenFrom(TokenResponse tokenResponse, Class<T> clazz) throws Exception {
@ -82,16 +194,69 @@ public class ModelUtils {
}
}
public static byte[] getDecodedHeader(String value) {
return getBase64Decoded(getEncodedHeader(value));
}
public static String getEncodedHeader(String encodedJWT) {
return splitAndGet(encodedJWT, 0);
}
public static byte[] getDecodedPayload(String value) {
return getBase64Decoded(getEncodedPayload(value));
}
public static String getEncodedPayload(String encodedJWT) {
return splitAndGet(encodedJWT, 1);
}
public static byte[] getDecodedSignature(String value) {
return getBase64Decoded(getEncodedSignature(value));
}
public static String getEncodedSignature(String encodedJWT) {
return splitAndGet(encodedJWT, 2);
}
}
public static String getClientIdFromToken(AccessToken accessToken) {
String clientId;
logger.debug("Client id not provided, using authorized party field (azp)");
clientId = accessToken.getIssuedFor();
if (clientId == null) {
logger.warn("Issued for field (azp) not present, getting first of the audience field (aud)");
clientId = getFirstAudienceNoAccount(accessToken);
}
return clientId;
}
private static String getFirstAudienceNoAccount(AccessToken accessToken) {
// Trying to get it from the token's audience ('aud' field), getting the first except the 'account'
List<String> tokenAud = Arrays.asList(accessToken.getAudience());
tokenAud.remove(ACCOUNT_AUDIENCE_RESOURCE);
if (tokenAud.size() > 0) {
return tokenAud.iterator().next();
} else {
// Setting it to empty string to avoid NPE in encoding
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

@ -0,0 +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,70 +1,545 @@
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;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestKeycloakClient {
protected static final Logger logger = LoggerFactory.getLogger(TestKeycloakClient.class);
private static final String DEV_ENDPOINT = "http://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";
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 testEndpointDiscovery() throws Exception {
logger.info("Start testing Keycloak endpoint discovery...");
URL url = KeycloakClientFactory.newInstance().findTokenEndpointURL();
Assert.assertNotNull(url);
Assert.assertTrue(url.getProtocol().equals("https"));
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 testQueryUMATokenWithDiscoveryInCurrentScope() throws Exception {
logger.info("Start testing query UMA token from Keycloak with endpoint discovery and current scope...");
TokenResponse tr = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, null);
TestModels.checkTokenResponse(tr);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tr), "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 testQueryUMATokenWithDiscovery() throws Exception {
logger.info("Start testing query UMA token from Keycloak with endpoint discovery...");
TokenResponse tr = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE,
null);
TestModels.checkTokenResponse(tr);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tr), "service-account-" + CLIENT_ID);
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 testQueryUMAToken() throws Exception {
logger.info("Start testing query UMA token from Keycloak with URL...");
TokenResponse tr = KeycloakClientFactory.newInstance()
.queryUMAToken(new URL(DEV_ENDPOINT), CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null);
TestModels.checkTokenResponse(tr);
TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tr), "service-account-" + CLIENT_ID);
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 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 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 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(
"*** [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"));
logger.info("*** [1.3c] OIDC access token: {}", oidcTR.getAccessToken());
logger.info("*** [1.3c] OIDC refresh token: {}", oidcTR.getRefreshToken());
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 context...");
TokenResponse umaTR = KeycloakClientFactory.newInstance().queryUMAToken(DEV_ROOT_CONTEXT, CLIENT_ID,
CLIENT_SECRET, TEST_AUDIENCE, null);
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 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
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

@ -0,0 +1,156 @@
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;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestModelUtils {
protected static final Logger logger = LoggerFactory.getLogger(TestModelUtils.class);
@Before
public void setUp() throws Exception {
}
@After
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...");
TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/oidc-token-response.json"),
TokenResponse.class);
logger.debug("OIDC token response:\n{}", ModelUtils.toJSONString(tr, true));
checkTokenResponse(tr);
}
@Test
public void testTokenResponseForUMA() throws Exception {
logger.info("Start testing UMA token response object binding...");
TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/uma-token-response.json"),
TokenResponse.class);
logger.debug("UMA token response:\n{}", ModelUtils.toJSONString(tr, true));
checkTokenResponse(tr);
}
@Test
public void testUMAAccessToken() throws Exception {
logger.info("Start testing access token object binding...");
AccessToken at = new ObjectMapper().readValue(new File("src/test/resources/uma-access-token.json"),
AccessToken.class);
checkAccessToken(at, null, true);
}
@Test
public void testRemoveBearerPrefixInHeader() throws Exception {
TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/oidc-token-response.json"),
TokenResponse.class);
AccessToken at1 = ModelUtils.getAccessTokenFrom(tr.getAccessToken());
AccessToken at2 = ModelUtils.getAccessTokenFrom("Bearer " + tr.getAccessToken());
AccessToken at3 = ModelUtils.getAccessTokenFrom("bearer " + tr.getAccessToken());
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));
}
@Test
public void testUMARefreshToken() throws Exception {
logger.info("Start testing refresh token object binding...");
RefreshToken rt = new ObjectMapper().readValue(new File("src/test/resources/uma-refresh-token.json"),
RefreshToken.class);
checkRefreshToken(rt);
}
public static void checkTokenResponse(TokenResponse tr) throws Exception {
checkTokenResponse(tr, true);
}
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());
}
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("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

@ -1,89 +0,0 @@
package org.gcube.common.keycloak;
import java.io.File;
import org.gcube.com.fasterxml.jackson.databind.ObjectMapper;
import org.gcube.common.keycloak.model.TokenResponse;
import org.gcube.common.keycloak.model.AccessToken;
import org.gcube.common.keycloak.model.ModelUtils;
import org.gcube.common.keycloak.model.RefreshToken;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestModels {
protected static final Logger logger = LoggerFactory.getLogger(TestModels.class);
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testTokenResponseForOIDC() throws Exception {
logger.info("Start testing OIDC token response object binding...");
TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/oidc-token-response.json"),
TokenResponse.class);
logger.debug("OIDC token response:\n{}", ModelUtils.toJSONString(tr, true));
checkTokenResponse(tr);
}
@Test
public void testTokenResponseForUMA() throws Exception {
logger.info("Start testing UMA token response object binding...");
TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/uma-token-response.json"),
TokenResponse.class);
logger.debug("UMA token response:\n{}", ModelUtils.toJSONString(tr, true));
checkTokenResponse(tr);
}
@Test
public void testUMAAccessToken() throws Exception {
logger.info("Start testing access token object binding...");
AccessToken at = new ObjectMapper().readValue(new File("src/test/resources/uma-access-token.json"),
AccessToken.class);
checkAccessToken(at, null);
}
@Test
public void testUMARefreshToken() throws Exception {
logger.info("Start testing refresh token object binding...");
RefreshToken rt = new ObjectMapper().readValue(new File("src/test/resources/uma-refresh-token.json"),
RefreshToken.class);
checkRefreshToken(rt);
}
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());
}
public static void checkAccessToken(AccessToken at, String preferredUsername) {
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());
}
public static void checkRefreshToken(RefreshToken rt) {
logger.debug("Refresh token:\n{}", ModelUtils.toJSONString(rt, true));
Assert.assertNotNull(rt.getOtherClaims());
Assert.assertNotNull(rt.getAudience());
}
}

View File

@ -12,7 +12,7 @@
</layout>
</appender>
<logger name="org.gcube" additivity="false">
<logger name="org.gcube.common.keycloak" additivity="false">
<level value="TRACE" />
<appender-ref ref="console" />
</logger>

View File

@ -0,0 +1,40 @@
{
"exp": 1622216161,
"iat": 1622215861,
"jti": "d391d814-a1f6-4a40-9aa3-3f51ec59988c",
"iss": "https://accounts.dev.d4science.org/auth/realms/d4science",
"aud": "account",
"sub": "1c84108a-201d-4e20-8ad2-d72b08d58f8a",
"typ": "Bearer",
"azp": "lr62_portal",
"session_state": "1a054bb7-4d87-44a9-ad1f-1746ef2dd522",
"acr": "1",
"allowed-origins": [
"https://dev.d4science.org"
],
"realm_access": {
"roles": [
"offline_access",
"Infrastructure-Client",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "email profile",
"email_verified": true,
"name": "Gino Stilla",
"groups": [],
"preferred_username": "gino.stilla",
"given_name": "Gino",
"locale": "it",
"family_name": "Stilla",
"email": "gino@stilla.com"
}

View File

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