From f5ef1d2c92a83edfd750d913c55fb4a44249cd32 Mon Sep 17 00:00:00 2001 From: Mauro Mugnaini Date: Thu, 9 Dec 2021 15:05:26 +0100 Subject: [PATCH] [#22515] Added refresh token methods --- CHANGELOG.md | 3 + pom.xml | 2 +- .../keycloak/DefaultKeycloakClient.java | 159 +++++++++++++++--- .../gcube/common/keycloak/KeycloakClient.java | 115 ++++++++++++- .../keycloak/KeycloakClientException.java | 2 +- .../common/keycloak/model/ModelUtils.java | 67 ++++++-- .../common/keycloak/model/OIDCConstants.java | 14 ++ .../common/keycloak/TestKeycloakClient.java | 38 +++-- .../org/gcube/common/keycloak/TestModels.java | 22 ++- 9 files changed, 366 insertions(+), 56 deletions(-) create mode 100644 src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e19fe6b..7deec56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,5 +2,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm # Changelog for "keycloak-client" +## [v1.1.0-SNAPSHOT] + + ## [v1.0.1] - First release (#21389 #22155) provides the basic helper classes for Keycloak tokens retrieve and functions for the gCube framework integration (automatic service discovery). diff --git a/pom.xml b/pom.xml index 723fb81..b2fc372 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ org.gcube.common keycloak-client - 1.0.1 + 1.1.0-SNAPSHOT diff --git a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java index 9e6f2d0..1f801aa 100644 --- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java @@ -2,6 +2,7 @@ package org.gcube.common.keycloak; import static org.gcube.resources.discovery.icclient.ICFactory.clientFor; import static org.gcube.resources.discovery.icclient.ICFactory.queryFor; +import static org.gcube.common.keycloak.model.OIDCConstants.*; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; @@ -17,6 +18,7 @@ import java.util.stream.Collectors; import org.gcube.common.gxrest.request.GXHTTPStringRequest; import org.gcube.common.gxrest.response.inbound.GXInboundResponse; +import org.gcube.common.keycloak.model.ModelUtils; import org.gcube.common.keycloak.model.TokenResponse; import org.gcube.common.resources.gcore.ServiceEndpoint; import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint; @@ -26,11 +28,6 @@ import org.gcube.resources.discovery.client.queries.api.SimpleQuery; public class DefaultKeycloakClient implements KeycloakClient { - private static final String PERMISSION_PARAMETER = "permission"; - private static final String GRANT_TYPE_PARAMETER = "grant_type"; - private static final String UMA_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket"; - private static final String AUDIENCE_PARAMETER = "audience"; - @Override public URL findTokenEndpointURL() throws KeycloakClientException { logger.debug("Checking ScopeProvider's scope presence and format"); @@ -102,41 +99,38 @@ public class DefaultKeycloakClient implements KeycloakClient { List permissions) throws KeycloakClientException { if (tokenURL == null) { - throw new KeycloakClientException("'tokenURL' parameter must be not null"); + throw new KeycloakClientException("Token URL must be not null"); } if (authorization == null || "".equals(authorization)) { - throw new KeycloakClientException("'authorization' parameter must be not null nor empty"); + throw new KeycloakClientException("Authorization must be not null nor empty"); } if (audience == null || "".equals(audience)) { - throw new KeycloakClientException("'audience' parameter must be not null nor empty"); + throw new KeycloakClientException("Audience must be not null nor empty"); } logger.debug("Querying token from Keycloak server with URL: {}", tokenURL); - Map> params = new HashMap<>(); - params.put(GRANT_TYPE_PARAMETER, Arrays.asList(UMA_TOKEN_GRANT_TYPE)); - - try { - params.put(AUDIENCE_PARAMETER, Arrays.asList(URLEncoder.encode(checkAudience(audience), "UTF-8"))); - } catch (UnsupportedEncodingException e) { - logger.error("Cannot URL encode 'audience'", e); - } - if (permissions != null && !permissions.isEmpty()) { - params.put( - PERMISSION_PARAMETER, permissions.stream().map(s -> { - try { - return URLEncoder.encode(s, "UTF-8"); - } catch (UnsupportedEncodingException e) { - return ""; - } - }).collect(Collectors.toList())); - } - // Constructing request object GXHTTPStringRequest request; try { + Map> params = new HashMap<>(); + params.put(GRANT_TYPE_PARAMETER, Arrays.asList(UMA_TOKEN_GRANT_TYPE)); + + params.put(AUDIENCE_PARAMETER, Arrays.asList(URLEncoder.encode(checkAudience(audience), "UTF-8"))); + + if (permissions != null && !permissions.isEmpty()) { + params.put( + PERMISSION_PARAMETER, permissions.stream().map(s -> { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + return ""; + } + }).collect(Collectors.toList())); + } + String queryString = params.entrySet().stream() .flatMap(p -> p.getValue().stream().map(v -> p.getKey() + "=" + v)) .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); @@ -168,7 +162,7 @@ public class DefaultKeycloakClient implements KeycloakClient { } else { throw KeycloakClientException.create("Unable to get token", response.getHTTPCode(), response.getHeaderFields() - .getOrDefault("Content-Type", Collections.singletonList("unknown/unknown")).get(0), + .getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0), response.getMessage()); } } @@ -185,4 +179,113 @@ public class DefaultKeycloakClient implements KeycloakClient { return audience; } + @Override + public TokenResponse refreshToken(TokenResponse tokenResponse) throws KeycloakClientException { + return refreshToken((String) null, tokenResponse); + } + + @Override + public TokenResponse refreshToken(URL tokenURL, TokenResponse tokenResponse) throws KeycloakClientException { + return refreshToken(tokenURL, null, null, tokenResponse); + } + + @Override + public TokenResponse refreshToken(String clientId, TokenResponse tokenResponse) throws KeycloakClientException { + return refreshToken(clientId, null, tokenResponse); + } + + @Override + public TokenResponse refreshToken(String clientId, String clientSecret, TokenResponse tokenResponse) + throws KeycloakClientException { + + return refreshToken(findTokenEndpointURL(), clientId, clientSecret, tokenResponse); + } + + @Override + public TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, TokenResponse tokenResponse) + throws KeycloakClientException { + + if (clientId == null) { + logger.debug("Client id not set, trying to get it from access token info"); + try { + clientId = ModelUtils.getClientIdFromToken(ModelUtils.getAccessTokenFrom(tokenResponse)); + } catch (Exception e) { + throw new KeycloakClientException("Cannot construct access token object from token response", e); + } + } + return refreshToken(tokenURL, clientId, clientSecret, tokenResponse.getRefreshToken()); + } + + @Override + public TokenResponse refreshToken(String clientId, String refreshTokenJWTString) throws KeycloakClientException { + return refreshToken(clientId, null, refreshTokenJWTString); + } + + @Override + public TokenResponse refreshToken(String clientId, String clientSecret, String refreshTokenJWTString) + throws KeycloakClientException { + + return refreshToken(findTokenEndpointURL(), clientId, clientSecret, refreshTokenJWTString); + } + + @Override + public TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, String refreshTokenJWTString) + throws KeycloakClientException { + + if (tokenURL == null) { + throw new KeycloakClientException("Token URL must be not null"); + } + + if (clientId == null || "".equals(clientId)) { + throw new KeycloakClientException("Client id must be not null nor empty"); + } + + if (refreshTokenJWTString == null || "".equals(clientId)) { + throw new KeycloakClientException("Refresh token JWT encoded string must be not null nor empty"); + } + + logger.debug("Refreshing token from Keycloak server with URL: {}", tokenURL); + + // 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")); + if (clientSecret != null && !"".equals(clientSecret)) { + params.put(CLIENT_SECRET_PARAMETER, URLEncoder.encode(clientSecret, "UTF-8")); + } + String queryString = params.entrySet().stream() + .map(p -> p.getKey() + "=" + p.getValue()) + .reduce((p1, p2) -> p1 + "&" + p2).orElse(""); + + request = GXHTTPStringRequest.newRequest(tokenURL.toString()).header("Content-Type", + "application/x-www-form-urlencoded").withBody(queryString); + + request.isExternalCall(true); + } catch (Exception e) { + throw new KeycloakClientException("Cannot construct the request object correctly", e); + } + + 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 get token", response.getHTTPCode(), + response.getHeaderFields() + .getOrDefault("content-type", Collections.singletonList("unknown/unknown")).get(0), + response.getMessage()); + } + } + } diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java index d0807ab..a0a10e6 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java @@ -18,6 +18,7 @@ public interface KeycloakClient { /** * Finds the keycloak endpoint {@link URL} discovering it in the current scope provided by {@link ScopeProvider} + * * @return the keycloak endpoint URL in the current scope * @throws KeycloakClientException if something goes wrong discovering the endpoint URL */ @@ -54,7 +55,7 @@ public interface KeycloakClient { throws KeycloakClientException; /** - * Queries an UMA token from the discovered Keycloak server in the current scope, by using provided clientId and client secret + * Queries an UMA token from the Keycloak server discovered in the current scope, by using provided clientId and client secret * for the given audience (context), in URLEncoded form or not, and optionally a list of permissions. * * @param clientId the client id @@ -68,7 +69,7 @@ public interface KeycloakClient { throws KeycloakClientException; /** - * Queries an UMA token from the discovered Keycloak server in the current scope, by using provided clientId and client secret + * Queries an UMA token from the Keycloak server discovered in the current scope, by using provided clientId and client secret * for the current scope audience (context), in URLEncoded form or not, and optionally a list of permissions. * * @param clientId the client id @@ -79,4 +80,114 @@ public interface KeycloakClient { */ TokenResponse queryUMAToken(String clientId, String clientSecret, List permissions) throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server discovered in the current scope using the refresh + * token JWT encoded string in the token response object. + * + * Client id will be read from "issued for" access token's claim and client secret will be not sent. + *
NOTE: For public clients types only. + * + * @param tokenResponse the previously issued token as {@link TokenResponse} object + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(TokenResponse tokenResponse) throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server using the refresh token JWT encoded string in the + * token response object. + * + * Client id will be read from "issued for" access token's claim and client secret will be not sent. + *
NOTE: For public clients types only. + * + * @param tokenUrl the token endpoint {@link URL} of the OIDC server + * @param tokenResponse the previously issued token as {@link TokenResponse} object + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(URL tokenURL, TokenResponse tokenResponse) throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server discovered in the current scope using the refresh + * token JWT encoded string in the token response object and the provided client id. + * + * Client secret will be not sent. + *
NOTE: For public clients types only. + * + * @param clientId the requestor client id, may be null and in this case will be take from the access token "issued for" claim + * @param tokenResponse the previously issued token as {@link TokenResponse} object + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(String clientId, TokenResponse tokenResponse) throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server discovered in the current scope using the refresh + * token JWT encoded string in the token response object and the provided client id and secret. + * + * @param clientId the requestor client id, may be null and in this case will be take from the access token "issued for" claim + * @param clientSecret the requestor client secret, may be null for non-confidential clients + * @param tokenResponse the previously issued token as {@link TokenResponse} object + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(String clientId, String clientSecret, TokenResponse tokenResponse) + throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server using the refresh token JWT encoded string in the + * token response object and the provided client id and secret. + * + * @param clientId the requestor client id, may be null and in this case will be take from the access token "issued for" claim + * @param clientSecret the requestor client secret, may be null for non-confidential clients + * @param tokenResponse the previously issued token as {@link TokenResponse} object + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, TokenResponse tokenResponse) + throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server discovered in the current scope using the provided + * client id and the refresh token JWT encoded string obtained with the access token in the previous token response. + * + * Client secret will be not used. + *
NOTE: For public clients types only. + * + * @param clientId the requestor client id + * @param refreshTokenJWTString the previously issued refresh token JWT string taken from the same token response of the access token parameter + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(String clientId, String refreshTokenJWTString) throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server discovered in the current scope using the provided + * client id and secret and the refresh token JWT encoded string obtained with the access token in the previous + * token response. + * + * @param clientId the requestor client id + * @param clientSecret the requestor client secret, may be null for non-confidential clients + * @param refreshTokenJWTString the previously issued refresh token JWT string taken from the same token response of the access token parameter + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(String clientId, String clientSecret, String refreshTokenJWTString) + throws KeycloakClientException; + + /** + * Refreshes a previously issued token from the Keycloak server by using the client id and secret + * and the refresh token JWT encoded string obtained with the access token in the previous token response. + * + * @param tokenUrl the token endpoint {@link URL} of the OIDC server + * @param clientId the requestor client id + * @param clientSecret the requestor client secret, may be null for non-confidential clients + * @param refreshTokenJWTString the previously issued refresh token JWT string + * @return the refreshed token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the refresh query + */ + TokenResponse refreshToken(URL tokenURL, String clientId, String clientSecret, String refreshTokenJWTString) + throws KeycloakClientException; + } \ No newline at end of file diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java b/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java index dd1ea1e..1222769 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClientException.java @@ -35,7 +35,7 @@ public class KeycloakClientException extends Exception { super(message); } - public KeycloakClientException(String message, Exception cause) { + public KeycloakClientException(String message, Throwable cause) { super(message, cause); } 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 c8fabb0..a4225a5 100644 --- a/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java +++ b/src/main/java/org/gcube/common/keycloak/model/ModelUtils.java @@ -1,6 +1,8 @@ package org.gcube.common.keycloak.model; +import java.util.Arrays; import java.util.Base64; +import java.util.List; import org.gcube.com.fasterxml.jackson.annotation.JsonInclude.Include; import org.gcube.com.fasterxml.jackson.core.JsonProcessingException; @@ -13,6 +15,8 @@ public class ModelUtils { protected static final Logger logger = LoggerFactory.getLogger(ModelUtils.class); + private static final String ACCOUNT_AUDIENCE_RESOURCE = "account"; + private static final ObjectMapper mapper = new ObjectMapper(); static { @@ -33,31 +37,40 @@ public class ModelUtils { } } - private static byte[] getDecodedPayload(String value) { - return getBase64Decoded(getEncodedPayload(value)); + public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse) throws Exception { + return getAccessTokenPayloadJSONStringFrom(tokenResponse, true); } - public static String getAccessTokenPayloadStringFrom(TokenResponse tokenResponse) throws Exception { - return getAccessTokenPayloadStringFrom(tokenResponse, true); - } - - public static String getAccessTokenPayloadStringFrom(TokenResponse tokenResponse, boolean prettyPrint) throws Exception { + public static String getAccessTokenPayloadJSONStringFrom(TokenResponse tokenResponse, boolean prettyPrint) + throws Exception { return toJSONString(getAccessTokenFrom(tokenResponse, Object.class), prettyPrint); } public static AccessToken getAccessTokenFrom(TokenResponse tokenResponse) throws Exception { - return getAccessTokenFrom(tokenResponse, RefreshToken.class); + return getAccessTokenFrom(tokenResponse, AccessToken.class); + } + + public static AccessToken getAccessTokenFrom(String authorizationHeaderOrBase64EncodedJWT) throws Exception { + return getAccessTokenFrom(authorizationHeaderOrBase64EncodedJWT.matches("[b|B]earer ") + ? authorizationHeaderOrBase64EncodedJWT.substring("bearer ".length()) + : authorizationHeaderOrBase64EncodedJWT, AccessToken.class); } private static T getAccessTokenFrom(TokenResponse tokenResponse, Class clazz) throws Exception { - return mapper.readValue(getDecodedPayload(tokenResponse.getAccessToken()), clazz); + return getAccessTokenFrom(tokenResponse.getAccessToken(), clazz); + } + + private static T getAccessTokenFrom(String accessToken, Class clazz) throws Exception { + return mapper.readValue(getDecodedPayload(accessToken), clazz); } public static String getRefreshTokenPayloadStringFrom(TokenResponse tokenResponse) throws Exception { return getRefreshTokenPayloadStringFrom(tokenResponse, true); } - public static String getRefreshTokenPayloadStringFrom(TokenResponse tokenResponse, boolean prettyPrint) throws Exception { + public static String getRefreshTokenPayloadStringFrom(TokenResponse tokenResponse, boolean prettyPrint) + throws Exception { + return toJSONString(getRefreshTokenFrom(tokenResponse, Object.class), prettyPrint); } @@ -82,16 +95,50 @@ public class ModelUtils { } } + public static byte[] getDecodedHeader(String value) { + return getBase64Decoded(getEncodedHeader(value)); + } + public static String getEncodedHeader(String encodedJWT) { return splitAndGet(encodedJWT, 0); } + public static byte[] getDecodedPayload(String value) { + return getBase64Decoded(getEncodedPayload(value)); + } + public static String getEncodedPayload(String encodedJWT) { return splitAndGet(encodedJWT, 1); } + public static byte[] getDecodedSignature(String value) { + return getBase64Decoded(getEncodedSignature(value)); + } + public static String getEncodedSignature(String encodedJWT) { return splitAndGet(encodedJWT, 2); } + public static String getClientIdFromToken(AccessToken accessToken) { + String clientId; + logger.debug("Client id not provided, using authorized party field (azp)"); + clientId = accessToken.getIssuedFor(); + if (clientId == null) { + logger.debug("Issued for field (azp) not present, getting first of the audience field (aud)"); + clientId = getFirstAudienceNoAccount(accessToken); + } + return clientId; + } + + private static String getFirstAudienceNoAccount(AccessToken accessToken) { + // Trying to get it from the token's audience ('aud' field), getting the first except the 'account' + List tokenAud = Arrays.asList(accessToken.getAudience()); + tokenAud.remove(ACCOUNT_AUDIENCE_RESOURCE); + if (tokenAud.size() > 0) { + return tokenAud.iterator().next(); + } else { + // Setting it to empty string to avoid NPE in encoding + return ""; + } + } } diff --git a/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java b/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java new file mode 100644 index 0000000..45212dd --- /dev/null +++ b/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java @@ -0,0 +1,14 @@ +package org.gcube.common.keycloak.model; + +public class OIDCConstants { + + public static final String PERMISSION_PARAMETER = "permission"; + public static final String GRANT_TYPE_PARAMETER = "grant_type"; + public static final String UMA_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket"; + public static final String AUDIENCE_PARAMETER = "audience"; + public static final String REFRESH_TOKEN_GRANT_TYPE = "refresh_token"; + public static final String REFRESH_TOKEN_PARAMETER = "refresh_token"; + public static final String CLIENT_ID_PARAMETER = "client_id"; + public static final String CLIENT_SECRET_PARAMETER = "client_secret"; + +} diff --git a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java index 2fca755..fe1d097 100644 --- a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java +++ b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java @@ -8,10 +8,13 @@ import org.gcube.common.scope.api.ScopeProvider; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.FixMethodOrder; import org.junit.Test; +import org.junit.runners.MethodSorters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestKeycloakClient { protected static final Logger logger = LoggerFactory.getLogger(TestKeycloakClient.class); @@ -21,6 +24,8 @@ public class TestKeycloakClient { private static final String CLIENT_SECRET = "38f76152-2b7c-418f-9b67-66f4cc2f401e"; private static final String TEST_AUDIENCE = "conductor-server"; + private static TokenResponse tr = null; + @Before public void setUp() throws Exception { ScopeProvider.instance.set("/gcube"); @@ -31,7 +36,7 @@ public class TestKeycloakClient { } @Test - public void testEndpointDiscovery() throws Exception { + public void test1EndpointDiscovery() throws Exception { logger.info("Start testing Keycloak endpoint discovery..."); URL url = KeycloakClientFactory.newInstance().findTokenEndpointURL(); Assert.assertNotNull(url); @@ -40,31 +45,38 @@ public class TestKeycloakClient { } @Test - public void testQueryUMATokenWithDiscoveryInCurrentScope() throws Exception { + public void test2QueryUMATokenWithDiscoveryInCurrentScope() throws Exception { logger.info("Start testing query UMA token from Keycloak with endpoint discovery and current scope..."); - TokenResponse tr = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, null); + tr = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, null); TestModels.checkTokenResponse(tr); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tr), "service-account-" + CLIENT_ID); } @Test - public void testQueryUMATokenWithDiscovery() throws Exception { + public void test3QueryUMATokenWithDiscovery() throws Exception { logger.info("Start testing query UMA token from Keycloak with endpoint discovery..."); - TokenResponse tr = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, - null); + tr = KeycloakClientFactory.newInstance().queryUMAToken(CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null); + TestModels.checkTokenResponse(tr); + TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tr), "service-account-" + CLIENT_ID); + } + + @Test + public void test4QueryUMAToken() throws Exception { + logger.info("Start testing query UMA token from Keycloak with URL..."); + tr = KeycloakClientFactory.newInstance().queryUMAToken(new URL(DEV_ENDPOINT), CLIENT_ID, CLIENT_SECRET, + TEST_AUDIENCE, null); TestModels.checkTokenResponse(tr); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tr), "service-account-" + CLIENT_ID); } @Test - public void testQueryUMAToken() throws Exception { - logger.info("Start testing query UMA token from Keycloak with URL..."); - TokenResponse tr = KeycloakClientFactory.newInstance() - .queryUMAToken(new URL(DEV_ENDPOINT), CLIENT_ID, CLIENT_SECRET, TEST_AUDIENCE, null); - - TestModels.checkTokenResponse(tr); - TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(tr), "service-account-" + CLIENT_ID); + public void test5RefreshTokenWithDiscovery() throws Exception { + logger.info("Start testing refresh UMA token from Keycloak with endpoint discovery..."); + TokenResponse refreshedTR = KeycloakClientFactory.newInstance().refreshToken(CLIENT_ID, CLIENT_SECRET, tr); + TestModels.checkTokenResponse(refreshedTR); + TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(refreshedTR), "service-account-" + CLIENT_ID); + TestModels.checkRefreshToken(ModelUtils.getRefreshTokenFrom(refreshedTR)); } } diff --git a/src/test/java/org/gcube/common/keycloak/TestModels.java b/src/test/java/org/gcube/common/keycloak/TestModels.java index 511b3bc..b1967ef 100644 --- a/src/test/java/org/gcube/common/keycloak/TestModels.java +++ b/src/test/java/org/gcube/common/keycloak/TestModels.java @@ -3,17 +3,20 @@ package org.gcube.common.keycloak; import java.io.File; import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; -import org.gcube.common.keycloak.model.TokenResponse; import org.gcube.common.keycloak.model.AccessToken; import org.gcube.common.keycloak.model.ModelUtils; import org.gcube.common.keycloak.model.RefreshToken; +import org.gcube.common.keycloak.model.TokenResponse; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.FixMethodOrder; import org.junit.Test; +import org.junit.runners.MethodSorters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestModels { protected static final Logger logger = LoggerFactory.getLogger(TestModels.class); @@ -56,6 +59,22 @@ public class TestModels { checkAccessToken(at, null); } + @Test + public void testRemoveBearerPrefixInHeader() throws Exception { + TokenResponse tr = new ObjectMapper().readValue(new File("src/test/resources/oidc-token-response.json"), + TokenResponse.class); + + AccessToken at1 = ModelUtils.getAccessTokenFrom(tr.getAccessToken()); + AccessToken at2 = ModelUtils.getAccessTokenFrom("Bearer " + tr.getAccessToken()); + AccessToken at3 = ModelUtils.getAccessTokenFrom("bearer " + tr.getAccessToken()); + + checkAccessToken(at1, null); + checkAccessToken(at2, null); + checkAccessToken(at3, null); + Assert.assertEquals(ModelUtils.toJSONString(at1), ModelUtils.toJSONString(at2)); + Assert.assertEquals(ModelUtils.toJSONString(at2), ModelUtils.toJSONString(at3)); + } + @Test public void testUMARefreshToken() throws Exception { logger.info("Start testing refresh token object binding..."); @@ -86,4 +105,5 @@ public class TestModels { Assert.assertNotNull(rt.getOtherClaims()); Assert.assertNotNull(rt.getAudience()); } + }