From 23f387f832ed768f0b615c2b93ebcd76c460492a Mon Sep 17 00:00:00 2001 From: Mauro Mugnaini Date: Tue, 30 Apr 2024 11:48:22 +0200 Subject: [PATCH] Added JWT digital signature verification by using the RSA public key of the realm on server. Uses `java-jwt` library by Auth0 [#27340] --- CHANGELOG.md | 1 + pom.xml | 15 +- .../keycloak/DefaultKeycloakClient.java | 340 +++++++------- .../gcube/common/keycloak/KeycloakClient.java | 417 +++++++++++++----- .../common/keycloak/model/AccessToken.java | 27 ++ .../keycloak/model/AddressClaimSet.java | 19 + .../gcube/common/keycloak/model/IDToken.java | 19 + .../common/keycloak/model/JsonWebToken.java | 3 + .../common/keycloak/model/ModelUtils.java | 51 +++ .../common/keycloak/model/OIDCConstants.java | 3 + .../model/PublishedRealmRepresentation.java | 102 +++++ .../common/keycloak/model/RefreshToken.java | 19 + .../model/TokenIntrospectionResponse.java | 19 + .../common/keycloak/model/TokenResponse.java | 3 + .../gcube/common/keycloak/model/UserInfo.java | 16 + .../model/idm/authorization/Permission.java | 19 + .../model/util/StringListMapDeserializer.java | 16 + .../model/util/StringOrArrayDeserializer.java | 16 + .../model/util/StringOrArraySerializer.java | 16 + .../common/keycloak/model/util/Time.java | 19 + .../common/keycloak/TestKeycloakClient.java | 147 +++--- .../{TestModels.java => TestModelUtils.java} | 25 +- src/test/resources/rsa-public-key.pem | 1 + 23 files changed, 991 insertions(+), 322 deletions(-) create mode 100644 src/main/java/org/gcube/common/keycloak/model/PublishedRealmRepresentation.java rename src/test/java/org/gcube/common/keycloak/{TestModels.java => TestModelUtils.java} (86%) create mode 100644 src/test/resources/rsa-public-key.pem diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f71847..af34bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [v2.1.0-SNAPSHOT] - Added `token-exchange` support, also with `offline-token` scope, and methods to add extra headers during the OIDC token requests. - Added custom base URL set via factory (not automatically working cross environments) [#27234]. +- Added JWT digital signature verification by using the RSA public key of the realm on server. Uses `java-jwt` library by Auth0 [#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). diff --git a/pom.xml b/pom.xml index 10f7ed4..9b9a9d4 100644 --- a/pom.xml +++ b/pom.xml @@ -7,8 +7,7 @@ maven-parent org.gcube.tools - 1.1.0 - + 1.2.0 org.gcube.common @@ -33,6 +32,12 @@ https://code-repo.d4science.org/gCubeSystem/${project.artifactId} + + 1.8 + ${java.version} + ${java.version} + + @@ -60,6 +65,12 @@ gxJRS + + com.auth0 + java-jwt + 4.4.0 + + org.slf4j slf4j-log4j12 diff --git a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java index e592f89..e065d7a 100644 --- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java @@ -38,9 +38,12 @@ 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.ModelUtils; +import org.gcube.common.keycloak.model.PublishedRealmRepresentation; import org.gcube.common.keycloak.model.TokenIntrospectionResponse; import org.gcube.common.keycloak.model.TokenResponse; import org.slf4j.Logger; @@ -176,11 +179,80 @@ public class DefaultKeycloakClient implements KeycloakClient { } } + @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 TokenResponse queryOIDCToken(String context, String clientId, String clientSecret) throws KeycloakClientException { - return queryOIDCTokenWithContext(context, clientId, clientSecret, null); + return queryOIDCToken(context, clientId, clientSecret, null); + } + + @Override + public TokenResponse queryOIDCToken(String context, String clientId, String clientSecret, + Map 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 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) null); + } + + @Override + public TokenResponse queryOIDCToken(String context, String authorization, Map extraHeaders) + throws KeycloakClientException { + + return queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), authorization, extraHeaders); + } + + @Override + public TokenResponse queryOIDCToken(URL tokenURL, String authorization, Map 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 extraHeaders) throws KeycloakClientException { + + return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, null, extraHeaders); } @Override @@ -192,46 +264,70 @@ public class DefaultKeycloakClient implements KeycloakClient { } @Override - public TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username, - String password) throws KeycloakClientException { - - return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, (String) null); - } - - @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) null); - } - - @Override - public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret, - String username, String password, String audience, Map extraHeaders) throws KeycloakClientException { - - return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, - username, password, audience, extraHeaders); - } - - @Override - public TokenResponse queryOIDCToken(URL tokenURL, String clientId, String clientSecret) + public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience) throws KeycloakClientException { - - return queryOIDCTokenWithContext(tokenURL, clientId, clientSecret, null); + 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 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 extraHeaders) throws KeycloakClientException { + return queryOIDCTokenWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret), - audience); + audience, extraHeaders); + } + + @Override + public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience, + Map 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) 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) null); + } + + @Override + public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret, + String username, String password, String audience, Map 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 { @@ -248,27 +344,18 @@ public class DefaultKeycloakClient implements KeycloakClient { password, audience, extraHeaders); } - @Override - public TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException { - return queryOIDCTokenWithContext(context, authorization, null); - } - - @Override - public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience) - throws KeycloakClientException { - return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, audience); - } - @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) null); + return queryOIDCTokenOfUserWithContext(tokenURL, clientId, clientSecret, username, password, audience, + (Map) null); } @Override public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret, - String username, String password, String audience, Map extraHeaders) throws KeycloakClientException { + String username, String password, String audience, Map extraHeaders) + throws KeycloakClientException { return queryOIDCTokenOfUserWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret), username, password, audience, extraHeaders); @@ -278,7 +365,8 @@ public class DefaultKeycloakClient implements KeycloakClient { public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username, String password, String audience) throws KeycloakClientException { - return queryOIDCTokenOfUserWithContext(tokenURL, authorization, username, password, audience, (Map) null); + return queryOIDCTokenOfUserWithContext(tokenURL, authorization, username, password, audience, + (Map) null); } @Override @@ -289,6 +377,7 @@ public class DefaultKeycloakClient implements KeycloakClient { 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 headers = new HashMap<>(); logger.debug("Adding authorization header as: {}", authorization); @@ -307,12 +396,8 @@ public class DefaultKeycloakClient implements KeycloakClient { } @Override - public TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException { - return queryOIDCTokenWithContext(tokenURL, authorization, null); - } - - @Override - public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience) + public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience, + Map extraHeaders) throws KeycloakClientException { logger.debug("Querying OIDC token from Keycloak server with URL: {}", tokenURL); @@ -415,27 +500,47 @@ public class DefaultKeycloakClient implements KeycloakClient { protected TokenResponse performRequest(URL tokenURL, Map headers, Map> params) throws KeycloakClientException { - if (tokenURL == null) { + return performRequest(TokenResponse.class, tokenURL, headers, params); + } + + protected T performRequest(Class returnObjectClass, URL url, Map headers, + Map> 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"); + } + } logger.trace("Query string is: {}", queryString); - request = GXHTTPStringRequest.newRequest(tokenURL.toString()) + request = GXHTTPStringRequest.newRequest(url.toString()) .header("Content-Type", "application/x-www-form-urlencoded").withBody(queryString); safeSetAsExternalCallForOldAPI(request); - logger.trace("Adding provided headers: {}", headers); - for (String headerName : headers.keySet()) { - request.header(headerName, headers.get(headerName)); + 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); @@ -450,7 +555,7 @@ public class DefaultKeycloakClient implements KeycloakClient { } 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); } @@ -538,99 +643,73 @@ public class DefaultKeycloakClient implements KeycloakClient { logger.debug("Refreshing token from Keycloak server with URL: {}", tokenURL); - // Constructing request object - GXHTTPStringRequest request; try { - Map params = new HashMap<>(); - params.put(GRANT_TYPE_PARAMETER, REFRESH_TOKEN_GRANT_TYPE); - params.put(REFRESH_TOKEN_PARAMETER, refreshTokenJWTString); - params.put(CLIENT_ID_PARAMETER, URLEncoder.encode(clientId, "UTF-8")); + Map> params = new HashMap<>(); + params.put(GRANT_TYPE_PARAMETER, Collections.singletonList(REFRESH_TOKEN_GRANT_TYPE)); + params.put(REFRESH_TOKEN_PARAMETER, Collections.singletonList(refreshTokenJWTString)); + params.put(CLIENT_ID_PARAMETER, Collections.singletonList(URLEncoder.encode(clientId, "UTF-8"))); if (clientSecret != null && !"".equals(clientSecret)) { - params.put(CLIENT_SECRET_PARAMETER, URLEncoder.encode(clientSecret, "UTF-8")); + params.put(CLIENT_SECRET_PARAMETER, + Collections.singletonList(URLEncoder.encode(clientSecret, "UTF-8"))); } - String queryString = params.entrySet().stream() - .map(p -> p.getKey() + "=" + p.getValue()) - .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); - - request = GXHTTPStringRequest.newRequest(tokenURL.toString()).header("Content-Type", - "application/x-www-form-urlencoded").withBody(queryString); - - safeSetAsExternalCallForOldAPI(request); - } catch (Exception e) { - throw new KeycloakClientException("Cannot construct the request object correctly", e); + return performRequest(tokenURL, null, params); + } catch (UnsupportedEncodingException e) { + throw new KeycloakClientException("Cannot encode parameters", e); } - GXInboundResponse response; - try { - response = request.post(); - } catch (Exception e) { - throw new KeycloakClientException("Cannot send request correctly", e); - } - if (response.isSuccessResponse()) { - try { - return response.tryConvertStreamedContentFromJson(TokenResponse.class); - } catch (Exception e) { - throw new KeycloakClientException("Cannot construct token response object correctly", e); - } - } else { - throw KeycloakClientException.create("Unable to refresh token", response.getHTTPCode(), - response.getHeaderFields() - .getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0), - response.getMessage()); - } } @Override - public TokenResponse exchangeTokenForAccessToken(String context, TokenResponse oidcTokenResponse, String clientId, + public TokenResponse exchangeTokenForAccessToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { - return exchangeTokenForAccessToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, clientId, + return exchangeTokenForAccessToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId, clientSecret, audience); } @Override - public TokenResponse exchangeTokenForAccessToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId, + public TokenResponse exchangeTokenForAccessToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { - return exchangeToken(tokenURL, oidcTokenResponse.getAccessToken(), clientId, clientSecret, audience, - ACCESS_TOKEN_TOKEN_TYPE, null); + return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, ACCESS_TOKEN_TOKEN_TYPE, + null); } @Override - public TokenResponse exchangeTokenForRefreshToken(String context, TokenResponse oidcTokenResponse, String clientId, + public TokenResponse exchangeTokenForRefreshToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { - return exchangeTokenForRefreshToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, clientId, + return exchangeTokenForRefreshToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId, clientSecret, audience); } @Override - public TokenResponse exchangeTokenForRefreshToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId, + public TokenResponse exchangeTokenForRefreshToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { - return exchangeToken(tokenURL, oidcTokenResponse.getAccessToken(), clientId, clientSecret, audience, - REFRESH_TOKEN_TOKEN_TYPE, null); + return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, REFRESH_TOKEN_TOKEN_TYPE, + null); } @Override - public TokenResponse exchangeTokenForOfflineToken(String context, TokenResponse oidcTokenResponse, String clientId, + public TokenResponse exchangeTokenForOfflineToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException { - return exchangeTokenForOfflineToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcTokenResponse, clientId, + return exchangeTokenForOfflineToken(getTokenEndpointURL(getRealmBaseURL(context)), oidcAccessToken, clientId, clientSecret, audience); } @Override - public TokenResponse exchangeTokenForOfflineToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId, - String clientSecret, String audience) throws KeycloakClientException { + public TokenResponse exchangeTokenForOfflineToken(URL tokenURL, String oidcAccessToken, String clientId, + String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException { - return exchangeToken(tokenURL, oidcTokenResponse.getAccessToken(), clientId, clientSecret, audience, - REFRESH_TOKEN_TOKEN_TYPE, OFFLINE_ACCESS_SCOPE); + // ModelUtils.getAccessTokenFrom(oidcTokenResponse).getScope(). + return exchangeToken(tokenURL, oidcAccessToken, clientId, clientSecret, audience, REFRESH_TOKEN_TOKEN_TYPE, + OFFLINE_ACCESS_SCOPE); } - @Override - public TokenResponse exchangeToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, + protected TokenResponse exchangeToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, String audience, String requestedTokenType, String scope) throws KeycloakClientException { if (audience == null || "".equals(audience)) { @@ -658,7 +737,7 @@ public class DefaultKeycloakClient implements KeycloakClient { logger.error("Can't URL encode audience: {}", audience, e); } - return performRequest(tokenURL, Collections.emptyMap(), params); + return performRequest(tokenURL, null, params); } /** @@ -699,44 +778,9 @@ public class DefaultKeycloakClient implements KeycloakClient { logger.debug("Verifying access token against Keycloak server with URL: {}", introspectionURL); - // Constructing request object - GXHTTPStringRequest request; - try { - Map params = new HashMap<>(); - params.put(TOKEN_PARAMETER, accessTokenJWTString); - - String queryString = params.entrySet().stream() - .map(p -> p.getKey() + "=" + p.getValue()) - .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); - - request = GXHTTPStringRequest.newRequest(introspectionURL.toString()).header("Content-Type", - "application/x-www-form-urlencoded").withBody(queryString); - - safeSetAsExternalCallForOldAPI(request); - - request = request.header("Authorization", constructBasicAuthenticationHeader(clientId, clientSecret)); - } catch (Exception e) { - throw new KeycloakClientException("Cannot construct the request object correctly", e); - } - - GXInboundResponse response; - try { - response = request.post(); - } catch (Exception e) { - throw new KeycloakClientException("Cannot send request correctly", e); - } - if (response.isSuccessResponse()) { - try { - return response.tryConvertStreamedContentFromJson(TokenIntrospectionResponse.class); - } catch (Exception e) { - throw new KeycloakClientException("Cannot construct introspection response object correctly", e); - } - } else { - throw KeycloakClientException.create("Unable to get token introspection response", response.getHTTPCode(), - response.getHeaderFields() - .getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0), - response.getMessage()); - } + return performRequest(TokenIntrospectionResponse.class, introspectionURL, + Collections.singletonMap("Authorization", constructBasicAuthenticationHeader(clientId, clientSecret)), + Collections.singletonMap(TOKEN_PARAMETER, Collections.singletonList(accessTokenJWTString))); } @Override diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java index 8db0c32..a3aad44 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java @@ -4,6 +4,7 @@ import java.net.URL; import java.util.List; import java.util.Map; +import org.gcube.common.keycloak.model.PublishedRealmRepresentation; import org.gcube.common.keycloak.model.TokenIntrospectionResponse; import org.gcube.common.keycloak.model.TokenResponse; @@ -72,7 +73,16 @@ public interface KeycloakClient { * @return the Keycloak avatar endpoint URL * @throws KeycloakClientException if something goes wrong discovering the endpoint URL */ - public URL getAvatarEndpointURL(URL realmBaseURL) throws KeycloakClientException; + URL getAvatarEndpointURL(URL realmBaseURL) throws KeycloakClientException; + + /** + * Get the realm info setup + * + * @param realmURL the realm URL + * @return the configured realm info + * @throws KeycloakClientException + */ + PublishedRealmRepresentation getRealmInfo(URL realmURL) throws KeycloakClientException; /** * Queries an OIDC token from the context's Keycloak server, by using provided clientId and client secret. @@ -85,9 +95,93 @@ public interface KeycloakClient { */ 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. /gcube 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 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 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. /gcube for DEV) + * @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(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. /gcube for DEV) + * @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(String context, String authorization, Map 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 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 X-D4Science-Context 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. /gcube for DEV) * @param clientId the client id * @param clientSecret the client secret @@ -98,6 +192,109 @@ public interface KeycloakClient { 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 X-D4Science-Context 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. /gcube 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 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 X-D4Science-Context 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 X-D4Science-Context 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 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. /gcube for DEV) + * @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(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. /gcube for DEV) + * @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(String context, String authorization, String audience, Map 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 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. * @@ -113,87 +310,20 @@ public interface KeycloakClient { String password) 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. + * 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 Keycloak server + * @param context the context where the Keycloak's is needed (e.g. /gcube 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) throws KeycloakClientException; - - public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret, - String username, String password, String audience, Map 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, reducing the audience to the requested one. - * - * @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 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. - * - * @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 - * @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; - - public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String clientId, String clientSecret, - String username, String password, String audience, Map 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. /gcube for DEV) - * @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(String context, String authorization) 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. /gcube for DEV) - * @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(String context, String authorization, String audience) - throws KeycloakClientException; + TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username, + String password, Map 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. @@ -209,30 +339,91 @@ public interface KeycloakClient { TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username, String password, String audience) throws KeycloakClientException; - public TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username, - String password, String audience, Map extraHeaders) throws KeycloakClientException; - /** - * Queries an OIDC token from the Keycloak server, by using provided authorization. + * 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. * - * @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, reducing the audience to the requested one. + * The implementation uses the custom X-D4Science-Context HTTP header that the proper mapper on Keycloak uses to reduce the audience * - * @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 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 queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience) - throws KeycloakClientException; + 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 X-D4Science-Context 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 + * @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 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 X-D4Science-Context 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 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. /gcube 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 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. @@ -248,7 +439,20 @@ public interface KeycloakClient { TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username, String password, String audience) throws KeycloakClientException; - public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username, + /** + * 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 extraHeaders) throws KeycloakClientException; /** @@ -420,27 +624,24 @@ public interface KeycloakClient { */ TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, String refreshTokenJWTString) throws KeycloakClientException; - - TokenResponse exchangeTokenForOfflineToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId, + + TokenResponse exchangeTokenForAccessToken(URL tokenURL, String oidcAccessToken, String clientId, + String clientSecret, String audience) throws KeycloakClientException; + + TokenResponse exchangeTokenForAccessToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException; - TokenResponse exchangeTokenForOfflineToken(String context, TokenResponse oidcTokenResponse, String clientId, + TokenResponse exchangeTokenForRefreshToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException; - TokenResponse exchangeTokenForRefreshToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId, + TokenResponse exchangeTokenForRefreshToken(String context, String oidcAccessToken, String clientId, String clientSecret, String audience) throws KeycloakClientException; - TokenResponse exchangeTokenForRefreshToken(String context, TokenResponse oidcTokenResponse, String clientId, - String clientSecret, String audience) throws KeycloakClientException; + TokenResponse exchangeTokenForOfflineToken(URL tokenURL, String oidcAccessToken, String clientId, + String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException; - TokenResponse exchangeTokenForAccessToken(URL tokenURL, TokenResponse oidcTokenResponse, String clientId, - String clientSecret, String audience) throws KeycloakClientException; - - TokenResponse exchangeTokenForAccessToken(String context, TokenResponse oidcTokenResponse, String clientId, - String clientSecret, String audience) throws KeycloakClientException; - - TokenResponse exchangeToken(URL tokenURL, String oidcAccessToken, String clientId, String clientSecret, - String audience, String requestedTokenType, String scope) throws KeycloakClientException; + TokenResponse exchangeTokenForOfflineToken(String context, String oidcAccessToken, String clientId, + String clientSecret, String audience) throws IllegalArgumentException, KeycloakClientException; /** * Introspects an access token against the Keycloak server. @@ -494,8 +695,8 @@ public interface KeycloakClient { boolean isAccessTokenVerified(URL introspectionURL, String clientId, String clientSecret, String accessTokenJWTString) throws KeycloakClientException; - public byte[] getAvatarData(String context, TokenResponse tokenResponse) throws KeycloakClientException; + byte[] getAvatarData(String context, TokenResponse tokenResponse) throws KeycloakClientException; - public byte[] getAvatarData(URL avatarURL, TokenResponse tokenResponse) throws KeycloakClientException; + byte[] getAvatarData(URL avatarURL, TokenResponse tokenResponse) throws KeycloakClientException; } \ No newline at end of file diff --git a/src/main/java/org/gcube/common/keycloak/model/AccessToken.java b/src/main/java/org/gcube/common/keycloak/model/AccessToken.java index bcab683..d2715d6 100644 --- a/src/main/java/org/gcube/common/keycloak/model/AccessToken.java +++ b/src/main/java/org/gcube/common/keycloak/model/AccessToken.java @@ -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 Bill Burke + */ public class AccessToken extends IDToken { private static final long serialVersionUID = 6364784008775737335L; @@ -156,4 +175,12 @@ public class AccessToken extends IDToken { this.trustedCertificates = trustedCertificates; } + public String getScope() { + return scope; + } + + public void setScope(String scope) { + this.scope = scope; + } + } diff --git a/src/main/java/org/gcube/common/keycloak/model/AddressClaimSet.java b/src/main/java/org/gcube/common/keycloak/model/AddressClaimSet.java index ffe077d..2bf1fe6 100644 --- a/src/main/java/org/gcube/common/keycloak/model/AddressClaimSet.java +++ b/src/main/java/org/gcube/common/keycloak/model/AddressClaimSet.java @@ -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 Bill Burke + */ public class AddressClaimSet { public static final String FORMATTED = "formatted"; diff --git a/src/main/java/org/gcube/common/keycloak/model/IDToken.java b/src/main/java/org/gcube/common/keycloak/model/IDToken.java index 85256cc..ed06935 100644 --- a/src/main/java/org/gcube/common/keycloak/model/IDToken.java +++ b/src/main/java/org/gcube/common/keycloak/model/IDToken.java @@ -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 Bill Burke + */ public class IDToken extends JsonWebToken { private static final long serialVersionUID = 8406175387651749097L; diff --git a/src/main/java/org/gcube/common/keycloak/model/JsonWebToken.java b/src/main/java/org/gcube/common/keycloak/model/JsonWebToken.java index 2b1458a..cdda20b 100644 --- a/src/main/java/org/gcube/common/keycloak/model/JsonWebToken.java +++ b/src/main/java/org/gcube/common/keycloak/model/JsonWebToken.java @@ -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 Bill Burke + */ public class JsonWebToken implements Serializable { private static final long serialVersionUID = -8136409077130940942L; diff --git a/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java b/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java index 188729e..6d66904 100644 --- a/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java +++ b/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java @@ -1,5 +1,8 @@ package org.gcube.common.keycloak.model; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.Base64; import java.util.List; @@ -11,6 +14,14 @@ import org.gcube.com.fasterxml.jackson.databind.ObjectWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.JWTVerifier; + +/** + * @author Mauro Mugnaini + */ public class ModelUtils { protected static final Logger logger = LoggerFactory.getLogger(ModelUtils.class); @@ -37,6 +48,46 @@ public class ModelUtils { } } + public static RSAPublicKey createRSAPublicKey(String publicKeyPem) { + try { + String publicKey = publicKeyPem.replaceFirst("-----BEGIN .+-----\n", ""); + publicKey = publicKey.replaceFirst("-----END .+-----", ""); + + byte[] encoded = Base64.getDecoder().decode(publicKey); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded)); + } catch (Exception e) { + throw new RuntimeException("Cant' create RSA public key from PEM string", e); + } + } + + /** + * Verifies the token's digital signature + * + * @param token the base64 JWT token string + * @param publicKey the realm's public key on server + * @return true if the signature is verified, false otherwise + * @throws RuntimeException if an error occurs constructing the digital signature verifier + */ + public static boolean isSignatureValid(String token, RSAPublicKey publicKey) throws RuntimeException { + JWTVerifier verifier = null; + try { + Algorithm algorithm = Algorithm.RSA256(publicKey, null); + verifier = JWT.require(algorithm).build(); + } catch (Exception e) { + throw new RuntimeException("Cannot construct the JWT digital signature verifier", e); + } + try { + verifier.verify(token); + return true; + } catch (JWTVerificationException e) { + if (logger.isDebugEnabled()) { + logger.debug("JWT digital signature is not verified", e); + } + return false; + } + } + public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse) throws Exception { return getAccessTokenPayloadJSONStringFrom(tokenResponse, true); } diff --git a/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java b/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java index 87298b1..079d548 100644 --- a/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java +++ b/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java @@ -1,5 +1,8 @@ package org.gcube.common.keycloak.model; +/** + * @author Mauro Mugnaini + */ public class OIDCConstants { public static final String PERMISSION_PARAMETER = "permission"; diff --git a/src/main/java/org/gcube/common/keycloak/model/PublishedRealmRepresentation.java b/src/main/java/org/gcube/common/keycloak/model/PublishedRealmRepresentation.java new file mode 100644 index 0000000..c86b862 --- /dev/null +++ b/src/main/java/org/gcube/common/keycloak/model/PublishedRealmRepresentation.java @@ -0,0 +1,102 @@ +/* + * 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 Bill Burke + * @author (modified by) Mauro Mugnaini + */ +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) { + publicKey = ModelUtils.createRSAPublicKey(publicKeyPem); + } + 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; + } +} diff --git a/src/main/java/org/gcube/common/keycloak/model/RefreshToken.java b/src/main/java/org/gcube/common/keycloak/model/RefreshToken.java index 2ee8115..f6b21cc 100644 --- a/src/main/java/org/gcube/common/keycloak/model/RefreshToken.java +++ b/src/main/java/org/gcube/common/keycloak/model/RefreshToken.java @@ -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 Bill Burke + */ public class RefreshToken extends AccessToken { private static final long serialVersionUID = 2646534143077862960L; diff --git a/src/main/java/org/gcube/common/keycloak/model/TokenIntrospectionResponse.java b/src/main/java/org/gcube/common/keycloak/model/TokenIntrospectionResponse.java index 814f837..eb72d00 100644 --- a/src/main/java/org/gcube/common/keycloak/model/TokenIntrospectionResponse.java +++ b/src/main/java/org/gcube/common/keycloak/model/TokenIntrospectionResponse.java @@ -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.List; @@ -5,6 +21,9 @@ import java.util.List; import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; import org.gcube.common.keycloak.model.idm.authorization.Permission; +/** + * @author Pedro Igor + */ public class TokenIntrospectionResponse extends JsonWebToken { private static final long serialVersionUID = -3105799239959636906L; diff --git a/src/main/java/org/gcube/common/keycloak/model/TokenResponse.java b/src/main/java/org/gcube/common/keycloak/model/TokenResponse.java index bf502fd..8d5fe97 100644 --- a/src/main/java/org/gcube/common/keycloak/model/TokenResponse.java +++ b/src/main/java/org/gcube/common/keycloak/model/TokenResponse.java @@ -10,6 +10,9 @@ import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * @author Mauro Mugnaini + */ public class TokenResponse implements Serializable { protected static Logger logger = LoggerFactory.getLogger(TokenResponse.class); diff --git a/src/main/java/org/gcube/common/keycloak/model/UserInfo.java b/src/main/java/org/gcube/common/keycloak/model/UserInfo.java index b3bfb9d..4f1ea83 100644 --- a/src/main/java/org/gcube/common/keycloak/model/UserInfo.java +++ b/src/main/java/org/gcube/common/keycloak/model/UserInfo.java @@ -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; diff --git a/src/main/java/org/gcube/common/keycloak/model/idm/authorization/Permission.java b/src/main/java/org/gcube/common/keycloak/model/idm/authorization/Permission.java index ef7a7ae..dcc91d9 100644 --- a/src/main/java/org/gcube/common/keycloak/model/idm/authorization/Permission.java +++ b/src/main/java/org/gcube/common/keycloak/model/idm/authorization/Permission.java @@ -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.idm.authorization; import java.util.HashSet; @@ -9,6 +25,9 @@ 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 Pedro Igor + */ @JsonIgnoreProperties(ignoreUnknown = true) public class Permission { diff --git a/src/main/java/org/gcube/common/keycloak/model/util/StringListMapDeserializer.java b/src/main/java/org/gcube/common/keycloak/model/util/StringListMapDeserializer.java index d399eff..751c12e 100644 --- a/src/main/java/org/gcube/common/keycloak/model/util/StringListMapDeserializer.java +++ b/src/main/java/org/gcube/common/keycloak/model/util/StringListMapDeserializer.java @@ -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; diff --git a/src/main/java/org/gcube/common/keycloak/model/util/StringOrArrayDeserializer.java b/src/main/java/org/gcube/common/keycloak/model/util/StringOrArrayDeserializer.java index 6eb1450..ec1e642 100644 --- a/src/main/java/org/gcube/common/keycloak/model/util/StringOrArrayDeserializer.java +++ b/src/main/java/org/gcube/common/keycloak/model/util/StringOrArrayDeserializer.java @@ -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; diff --git a/src/main/java/org/gcube/common/keycloak/model/util/StringOrArraySerializer.java b/src/main/java/org/gcube/common/keycloak/model/util/StringOrArraySerializer.java index 386ec5d..0950367 100644 --- a/src/main/java/org/gcube/common/keycloak/model/util/StringOrArraySerializer.java +++ b/src/main/java/org/gcube/common/keycloak/model/util/StringOrArraySerializer.java @@ -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; diff --git a/src/main/java/org/gcube/common/keycloak/model/util/Time.java b/src/main/java/org/gcube/common/keycloak/model/util/Time.java index 1101d1a..71b266e 100644 --- a/src/main/java/org/gcube/common/keycloak/model/util/Time.java +++ b/src/main/java/org/gcube/common/keycloak/model/util/Time.java @@ -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.util; import java.util.Date; +/** + * @author Stian Thorgersen + */ public class Time { private static int offset; diff --git a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java index 4d31847..dbf7145 100644 --- a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java +++ b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java @@ -6,6 +6,7 @@ import java.net.URL; import java.util.Collections; 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.junit.After; @@ -124,6 +125,34 @@ public class TestKeycloakClient { Assert.assertEquals(customBase + KeycloakClient.DEFAULT_REALM + "/", customBaseURL.toString()); } + @Test + public void test10QueryRealmInfo() throws Exception { + logger.info("*** [1.0] Start testing query realm info..."); + 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()); + // TestModels.checkTokenResponse(oidcTR); + // TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); + } + + @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 digital signature is not valid", + ModelUtils.isSignatureValid(oidcTR.getAccessToken(), realmInfo.getPublicKey())); + } + @Test public void test12QueryOIDCToken() throws Exception { logger.info("*** [1.2] Start testing query OIDC token from Keycloak with context..."); @@ -132,8 +161,8 @@ public class TestKeycloakClient { logger.info("*** [1.2] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.2] OIDC refresh token: {}", oidcTR.getRefreshToken()); - TestModels.checkTokenResponse(oidcTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); + TestModelUtils.checkTokenResponse(oidcTR); + TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); } @Test @@ -145,8 +174,8 @@ public class TestKeycloakClient { logger.info("*** [1.2a] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.2a] OIDC refresh token: {}", oidcTR.getRefreshToken()); - TestModels.checkTokenResponse(oidcTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); + TestModelUtils.checkTokenResponse(oidcTR); + TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); } @Test @@ -157,8 +186,8 @@ public class TestKeycloakClient { logger.info("*** [1.3] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.3] OIDC refresh token: {}", oidcTR.getRefreshToken()); - TestModels.checkTokenResponse(oidcTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, false); + TestModelUtils.checkTokenResponse(oidcTR); + TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, false); } @Test @@ -169,8 +198,8 @@ public class TestKeycloakClient { logger.info("*** [1.3a] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.3a] OIDC refresh token: {}", oidcTR.getRefreshToken()); - TestModels.checkTokenResponse(oidcTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true); + 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, @@ -178,10 +207,10 @@ public class TestKeycloakClient { logger.info("*** [1.3a] OIDC restricted access token: {}", oidcRestrictedTR.getAccessToken()); logger.info("*** [1.3a] OIDC restricted refresh token: {}", oidcRestrictedTR.getRefreshToken()); - TestModels.checkTokenResponse(oidcTR); - TestModels.checkTokenResponse(oidcRestrictedTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcRestrictedTR), TEST_USER_USERNAME, true); + 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( @@ -197,16 +226,16 @@ public class TestKeycloakClient { logger.info("*** [1.3b] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.3b] OIDC refresh token: {}", oidcTR.getRefreshToken()); - TestModels.checkTokenResponse(oidcTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, false); + 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()); - TestModels.checkTokenResponse(umaTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), TEST_USER_USERNAME, true); + TestModelUtils.checkTokenResponse(umaTR); + TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), TEST_USER_USERNAME, true); } @Test @@ -220,8 +249,8 @@ public class TestKeycloakClient { logger.info("*** [1.3c] OIDC access token: {}", oidcTR.getAccessToken()); logger.info("*** [1.3c] OIDC refresh token: {}", oidcTR.getRefreshToken()); - TestModels.checkTokenResponse(oidcTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, true); + 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. } @@ -235,8 +264,8 @@ public class TestKeycloakClient { logger.info("*** [2.4] UMA access token: {}", umaTR.getAccessToken()); logger.info("*** [2.4] UMA refresh token: {}", umaTR.getRefreshToken()); - TestModels.checkTokenResponse(umaTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true); + TestModelUtils.checkTokenResponse(umaTR); + TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true); } @Test @@ -249,8 +278,8 @@ public class TestKeycloakClient { logger.info("*** [2.4a] UMA access token: {}", umaTR.getAccessToken()); logger.info("*** [2.4a] UMA refresh token: {}", umaTR.getRefreshToken()); - TestModels.checkTokenResponse(umaTR); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true); + TestModelUtils.checkTokenResponse(umaTR); + TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(umaTR), "service-account-" + CLIENT_ID, true); } @Test @@ -262,7 +291,7 @@ public class TestKeycloakClient { TokenIntrospectionResponse tir = client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()); - TestModels.checkTokenIntrospectionResponse(tir); + TestModelUtils.checkTokenIntrospectionResponse(tir); } @Test @@ -275,7 +304,7 @@ public class TestKeycloakClient { TokenIntrospectionResponse tir = client.introspectAccessToken(introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()); - TestModels.checkTokenIntrospectionResponse(tir); + TestModelUtils.checkTokenIntrospectionResponse(tir); } @Test @@ -291,7 +320,7 @@ public class TestKeycloakClient { TokenIntrospectionResponse tir = client.introspectAccessToken(introspectionURL, CLIENT_ID, CLIENT_SECRET, oidcTR.getAccessToken()); - TestModels.checkTokenIntrospectionResponse(tir, true); + TestModelUtils.checkTokenIntrospectionResponse(tir, true); } @Test @@ -302,7 +331,7 @@ public class TestKeycloakClient { TokenIntrospectionResponse tir = client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken()); - TestModels.checkTokenIntrospectionResponse(tir); + TestModelUtils.checkTokenIntrospectionResponse(tir); } @Test @@ -314,7 +343,7 @@ public class TestKeycloakClient { TokenIntrospectionResponse tir = client.introspectAccessToken(introspectionURL, CLIENT_ID, CLIENT_SECRET, umaTR.getAccessToken()); - TestModels.checkTokenIntrospectionResponse(tir); + TestModelUtils.checkTokenIntrospectionResponse(tir); } @Test @@ -398,8 +427,8 @@ public class TestKeycloakClient { logger.info("*** [4] UMA access token: {}", tokenResponse.getAccessToken()); logger.info("*** [4] UMA refresh token: {}", tokenResponse.getRefreshToken()); - TestModels.checkTokenResponse(tokenResponse); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tokenResponse), TEST_USER_USERNAME, true); + TestModelUtils.checkTokenResponse(tokenResponse); + TestModelUtils.checkAccessToken(ModelUtils.getAccessTokenFrom(tokenResponse), TEST_USER_USERNAME, true); } @Test @@ -407,19 +436,18 @@ public class TestKeycloakClient { 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); + TEST_USER_USERNAME, TEST_USER_PASSWORD); logger.info("*** [5.1] OIDC access token: {}", oidcTR.getAccessToken()); - TokenResponse exchangedTR = client.exchangeTokenForAccessToken(DEV_ROOT_CONTEXT, oidcTR, CLIENT_ID, - CLIENT_SECRET, CLIENT_ID); + TokenResponse exchangedTR = client.exchangeTokenForAccessToken(DEV_ROOT_CONTEXT, oidcTR.getAccessToken(), + CLIENT_ID, CLIENT_SECRET, CLIENT_ID); logger.info("*** [5.1] Exchanged access token: {}", exchangedTR.getAccessToken()); - TestModels.checkTokenResponse(exchangedTR, false); + TestModelUtils.checkTokenResponse(exchangedTR, false); Assert.assertNull(exchangedTR.getRefreshToken()); - TestModels.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, + TestModelUtils.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, exchangedTR.getAccessToken())); } @@ -436,14 +464,14 @@ public class TestKeycloakClient { logger.info("*** [5.1a] UMA access token: {}", umaTR.getAccessToken()); - TokenResponse exchangedTR = client.exchangeTokenForAccessToken(DEV_ROOT_CONTEXT, umaTR, CLIENT_ID, - CLIENT_SECRET, CLIENT_ID); + TokenResponse exchangedTR = client.exchangeTokenForAccessToken(DEV_ROOT_CONTEXT, umaTR.getAccessToken(), + CLIENT_ID, CLIENT_SECRET, CLIENT_ID); logger.info("*** [5.1a] Exchanged access token: {}", exchangedTR.getAccessToken()); - TestModels.checkTokenResponse(exchangedTR, false); + TestModelUtils.checkTokenResponse(exchangedTR, false); Assert.assertNull(exchangedTR.getRefreshToken()); - TestModels.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, + TestModelUtils.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, exchangedTR.getAccessToken())); } @@ -456,14 +484,14 @@ public class TestKeycloakClient { logger.info("*** [5.2] OIDC access token: {}", oidcTR.getAccessToken()); - TokenResponse exchangedTR = client.exchangeTokenForRefreshToken(DEV_ROOT_CONTEXT, oidcTR, CLIENT_ID, - CLIENT_SECRET, CLIENT_ID); + 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()); - TestModels.checkTokenResponse(exchangedTR); + TestModelUtils.checkTokenResponse(exchangedTR); - TestModels.checkTokenIntrospectionResponse( + TestModelUtils.checkTokenIntrospectionResponse( client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, CLIENT_SECRET, exchangedTR.getAccessToken())); } @@ -472,31 +500,32 @@ public class TestKeycloakClient { 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); + TEST_USER_USERNAME, TEST_USER_PASSWORD); logger.info("*** [5.3] OIDC access token: {}", oidcTR.getAccessToken()); - TokenResponse exchangedTR = client.exchangeTokenForOfflineToken(DEV_ROOT_CONTEXT, oidcTR, CLIENT_ID, - CLIENT_SECRET, CLIENT_ID); + TokenResponse exchangedTR = client.exchangeTokenForOfflineToken(DEV_ROOT_CONTEXT, oidcTR.getAccessToken(), + CLIENT_ID, CLIENT_SECRET, CLIENT_ID); logger.info("*** [5.3] Exchanged access token: {}", exchangedTR.getAccessToken()); logger.info("*** [5.3] Exchanged refresh token: {}", exchangedTR.getRefreshToken()); - TestModels.checkTokenResponse(exchangedTR, true); - TestModels.checkOfflineToken(exchangedTR); + TestModelUtils.checkTokenResponse(exchangedTR, true); + TestModelUtils.checkOfflineToken(exchangedTR); - TestModels.checkTokenIntrospectionResponse(client.introspectAccessToken(DEV_ROOT_CONTEXT, CLIENT_ID, + 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, GATEWAY, null, 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); - // } + @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); + } } diff --git a/src/test/java/org/gcube/common/keycloak/TestModels.java b/src/test/java/org/gcube/common/keycloak/TestModelUtils.java similarity index 86% rename from src/test/java/org/gcube/common/keycloak/TestModels.java rename to src/test/java/org/gcube/common/keycloak/TestModelUtils.java index c51b604..4a8ea2b 100644 --- a/src/test/java/org/gcube/common/keycloak/TestModels.java +++ b/src/test/java/org/gcube/common/keycloak/TestModelUtils.java @@ -1,6 +1,8 @@ package org.gcube.common.keycloak; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; import org.gcube.common.keycloak.model.AccessToken; @@ -18,9 +20,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; @FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class TestModels { +public class TestModelUtils { - protected static final Logger logger = LoggerFactory.getLogger(TestModels.class); + protected static final Logger logger = LoggerFactory.getLogger(TestModelUtils.class); @Before public void setUp() throws Exception { @@ -30,6 +32,19 @@ public class TestModels { public void tearDown() throws Exception { } + @Test + public void testTokenDigitalSignature() 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); + + // Valid signature + Assert.assertFalse("Token signature is valid", ModelUtils.isSignatureValid(tr.getAccessToken(), + ModelUtils.createRSAPublicKey( + new String(Files.readAllBytes(Paths.get("src/test/resources/rsa-public-key.pem")))))); + + } + @Test public void testTokenResponseForOIDC() throws Exception { logger.info("Start testing OIDC token response object binding..."); @@ -38,7 +53,7 @@ public class TestModels { logger.debug("OIDC token response:\n{}", ModelUtils.toJSONString(tr, true)); checkTokenResponse(tr); - + } @Test @@ -112,13 +127,13 @@ public class TestModels { 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()); + 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()); + Assert.assertNull("Expiration is not null", rt.getExp()); } public static void checkTokenIntrospectionResponse(TokenIntrospectionResponse tir) { diff --git a/src/test/resources/rsa-public-key.pem b/src/test/resources/rsa-public-key.pem new file mode 100644 index 0000000..44e740c --- /dev/null +++ b/src/test/resources/rsa-public-key.pem @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjyJAmPx5K2eGYjvRmiBC8as3nF/jsYSUgBnlul9TNEdWSPuxTzntTb37xDGPmVRkNyOCJmRnBcI8GbrWz8SHJ643JTKp8yx4zDQCgLD72crb9ah/Tfu8KpDz3+FRuYLE4EvvRCGBnsFO2vSM02iTAp7nSToOCX4jCCrDMBUUJkIzuZIQUBTx8lvWl/M6LtQAqS7Gw3wsZSklRcvsR9qlCUxJW3cvhALt9wWrejSJ3LaR6TMaNa8k6Ojk6bJ3/5c6OifxYde0YjXIOaeMkkgnfoQvs4cyhvNxCXLx65+VKV4Dlts7cTgJvuodV0w/UhJGfUxgA9V6IQowmwHNBnYAMwIDAQAB \ No newline at end of file