From d5ddbfd067619244040f9d93114bd8578d8ecf8f Mon Sep 17 00:00:00 2001 From: Mauro Mugnaini Date: Tue, 11 Jul 2023 13:36:23 +0200 Subject: [PATCH] Added support of password grant flow (corresponding to the now deprecated OAuth2 flow: Resource Owner Password Credentials grant) also for specific context/audience by using the specific D4S mapper. (#25291) --- CHANGELOG.md | 5 +- .../keycloak/DefaultKeycloakClient.java | 68 +++++++++++++++++-- .../gcube/common/keycloak/KeycloakClient.java | 68 +++++++++++++++++++ .../common/keycloak/model/OIDCConstants.java | 3 + .../common/keycloak/TestKeycloakClient.java | 26 +++++++ 5 files changed, 163 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad89e7d..65c57d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [v2.0.0-SNAPSHOT] - Removed the discovery functionality to be compatible with SmartGears.v4 and moved to the new library `keycloak-client-legacy-is` that will provide the backward compatibility. (#23478). - Fixed typo in `AccessToken` class for `setAccessToken(..)` method (#23654) -- Added predictive infrastructure URL support based on context (and on context and realm if the target realm is not the default one) and overloaded all methods that take the URL as argument with the context (#23655) -- Added support for the use of the custom Keycloak's D4S mapper that maps/shrink the `aud` (and optionally also the resource access) to the value requested via `X-D4Science-Context` HTTP header +- Added predictive infrastructure URL support based on context (and on context and realm if the target realm is not the default one) and overloaded all methods that take the URL as argument with the context. (#23655) +- Added support for the use of the custom Keycloak's D4S mapper that maps/shrink the `aud` (and optionally also the resource access) to the value requested via `X-D4Science-Context` HTTP header. +- Added support of password grant flow (corresponding to the now deprecated OAuth2 flow: Resource Owner Password Credentials grant) also for specific context/audience by using the specific D4S mapper. (#25291) ## [v1.3.0-SNAPSHOT] - Added functions to introspect and verify access tokens (both OIDC and UMA are supported) (#23326). diff --git a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java index 13c4b95..6ff9a44 100644 --- a/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/DefaultKeycloakClient.java @@ -10,6 +10,9 @@ import static org.gcube.common.keycloak.model.OIDCConstants.REFRESH_TOKEN_GRANT_ import static org.gcube.common.keycloak.model.OIDCConstants.REFRESH_TOKEN_PARAMETER; import static org.gcube.common.keycloak.model.OIDCConstants.TOKEN_PARAMETER; import static org.gcube.common.keycloak.model.OIDCConstants.UMA_TOKEN_GRANT_TYPE; +import static org.gcube.common.keycloak.model.OIDCConstants.PASSWORD_GRANT_TYPE; +import static org.gcube.common.keycloak.model.OIDCConstants.USERNAME_PARAMETER; +import static org.gcube.common.keycloak.model.OIDCConstants.PASSWORD_PARAMETER; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -129,7 +132,23 @@ public class DefaultKeycloakClient implements KeycloakClient { public TokenResponse queryOIDCTokenWithContext(String context, String clientId, String clientSecret, String audience) throws KeycloakClientException { - return queryOIDCToken(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret); + return queryOIDCTokenWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, + audience); + } + + @Override + public TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username, + String password) throws KeycloakClientException { + + return queryOIDCTokenOfUserWithContext(context, clientId, clientSecret, username, password, null); + } + + @Override + public TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret, + String username, String password, String audience) throws KeycloakClientException { + + return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), clientId, clientSecret, + username, password, null); } @Override @@ -143,35 +162,74 @@ public class DefaultKeycloakClient implements KeycloakClient { public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String clientId, String clientSecret, String audience) throws KeycloakClientException { - return queryOIDCTokenWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret), audience); + return queryOIDCTokenWithContext(tokenURL, constructBasicAuthenticationHeader(clientId, clientSecret), + audience); } protected String constructBasicAuthenticationHeader(String clientId, String clientSecret) { return "Basic " + Base64.getEncoder().encodeToString((clientId + ":" + clientSecret).getBytes()); } + @Override + public TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username, + String password, String audience) throws KeycloakClientException { + + return queryOIDCTokenOfUserWithContext(getTokenEndpointURL(getRealmBaseURL(context)), authorization, username, + password, audience); + } + @Override public TokenResponse queryOIDCToken(String context, String authorization) throws KeycloakClientException { return queryOIDCTokenWithContext(context, authorization, null); } @Override - public TokenResponse queryOIDCTokenWithContext(String context, String authorization, String audience) throws KeycloakClientException { + 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, constructBasicAuthenticationHeader(clientId, clientSecret), + username, password, audience); + } + + @Override + public TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username, + String password, String audience) throws KeycloakClientException { + + Map> params = new HashMap<>(); + params.put(GRANT_TYPE_PARAMETER, Arrays.asList(PASSWORD_GRANT_TYPE)); + params.put(USERNAME_PARAMETER, Arrays.asList(username)); + params.put(PASSWORD_PARAMETER, Arrays.asList(password)); + + Map headers = new HashMap<>(); + logger.debug("Adding authorization header as: {}", authorization); + headers.put(AUTHORIZATION_HEADER, authorization); + + if (audience != null) { + logger.debug("Adding d4s context header as: {}", audience); + headers.put(D4S_CONTEXT_HEADER_NAME, audience); + } + + return performRequest(tokenURL, headers, params); + } + @Override public TokenResponse queryOIDCToken(URL tokenURL, String authorization) throws KeycloakClientException { return queryOIDCTokenWithContext(tokenURL, authorization, null); } @Override - public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience) throws KeycloakClientException { + public TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, String audience) + throws KeycloakClientException { logger.debug("Querying OIDC token from Keycloak server with URL: {}", tokenURL); Map> params = new HashMap<>(); params.put(GRANT_TYPE_PARAMETER, Arrays.asList(CLIENT_CREDENTIALS_GRANT_TYPE)); -// params.put(SCOPE_PARAMETER, Arrays.asList("conductor-server")); Map headers = new HashMap<>(); logger.debug("Adding authorization header as: {}", authorization); diff --git a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java index fbef138..dfde2c5 100644 --- a/src/main/java/org/gcube/common/keycloak/KeycloakClient.java +++ b/src/main/java/org/gcube/common/keycloak/KeycloakClient.java @@ -23,6 +23,7 @@ public interface KeycloakClient { * @throws KeycloakClientException if something goes wrong discovering the endpoint URL */ URL getRealmBaseURL(String context) throws KeycloakClientException; + /** * Returns the Keycloak base {@link URL} for the given context and in the given realm. @@ -84,6 +85,34 @@ public interface KeycloakClient { */ TokenResponse queryOIDCTokenWithContext(String context, 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. + * + * @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 + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenOfUser(String context, String clientId, String clientSecret, String username, String password) throws KeycloakClientException; + + /** + * Queries an OIDC token for a specific user from the 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 + * @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenOfUserWithContext(String context, String clientId, String clientSecret, String username, String password, String audience) throws KeycloakClientException; + + /** * Queries an OIDC token from the Keycloak server, by using provided clientId and client secret. * @@ -107,6 +136,19 @@ public interface KeycloakClient { */ 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; + /** * Queries an OIDC token from the Keycloak server, by using provided authorization. * @@ -130,6 +172,19 @@ public interface KeycloakClient { */ TokenResponse queryOIDCTokenWithContext(String context, String authorization, 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. + * + * @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 + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenOfUserWithContext(String context, String authorization, String username, String password, String audience) throws KeycloakClientException; + /** * Queries an OIDC token from the Keycloak server, by using provided authorization. * @@ -151,6 +206,19 @@ public interface KeycloakClient { */ TokenResponse queryOIDCTokenWithContext(URL tokenURL, String authorization, 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. + * + * @param tokenUrl the token endpoint {@link URL} of the OIDC server + * @param authorization the authorization to be set as header (e.g. a "Basic ...." auth or an encoded JWT access token preceded by the "Bearer " string) + * @param username the user's username + * @param password the user's password + * @param audience an optional parameter to shrink the token's audience to the requested one (e.g. a specific context), by leveraging on the custom HTTP header and corresponding mapper on Keycloak + * @return the issued token as {@link TokenResponse} object + * @throws KeycloakClientException if something goes wrong performing the query + */ + TokenResponse queryOIDCTokenOfUserWithContext(URL tokenURL, String authorization, String username, String password, String audience) throws KeycloakClientException; + /** * Queries an UMA token from the Keycloak server, by using provided authorization, for the given audience (context), * in URLEncoded form or not, and optionally a list of permissions. 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 cc7963f..b39495a 100644 --- a/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java +++ b/src/main/java/org/gcube/common/keycloak/model/OIDCConstants.java @@ -13,5 +13,8 @@ public class OIDCConstants { public static final String CLIENT_ID_PARAMETER = "client_id"; public static final String CLIENT_SECRET_PARAMETER = "client_secret"; public static final String TOKEN_PARAMETER = "token"; + public static final String PASSWORD_GRANT_TYPE = "password"; + public static final String USERNAME_PARAMETER = "username"; + public static final String PASSWORD_PARAMETER = "password"; } diff --git a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java index d0641fd..d4969e3 100644 --- a/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java +++ b/src/test/java/org/gcube/common/keycloak/TestKeycloakClient.java @@ -27,6 +27,8 @@ public class TestKeycloakClient { protected static final String CLIENT_ID = "keycloak-client-unit-test"; protected static final String CLIENT_SECRET = "ebf6f82e-9511-408e-8321-203081e472d8"; protected static final String TEST_AUDIENCE = "conductor-server"; + protected static final String TEST_USER_USERNAME = "testuser"; + protected static final String TEST_USER_PASSWORD = "t35tp455"; protected static final String OLD_OIDC_ACCESS_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSSklZNEpoNF9qdDdvNmREY0NlUDFfS1l0akcxVExXVW9oMkQ2Tzk1bFNBIn0.eyJleHAiOjE2NTI5Nzk4NDUsImlhdCI6MTY1Mjk3OTU0NSwianRpIjoiMzQ2MjgwMWItODg4NS00YTM4LWJkNDUtNWExM2U1MGE5MGU5IiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5kZXYuZDRzY2llbmNlLm9yZy9hdXRoL3JlYWxtcy9kNHNjaWVuY2UiLCJhdWQiOlsiJTJGZ2N1YmUiLCIlMkZnY3ViZSUyRmRldnNlYyUyRmRldlZSRSIsImFjY291bnQiXSwic3ViIjoiYTQ3ZGZlMTYtYjRlZC00NGVkLWExZDktOTdlY2Q1MDQzNjBjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoia2V5Y2xvYWstY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6ImQ4MDk3MDBmLWEyNDUtNDI3Zi1hYzhjLTQxYjFkZDNkYTQ3MCIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsIkluZnJhc3RydWN0dXJlLUNsaWVudCIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiJTJGZ2N1YmUiOnsicm9sZXMiOlsiTWVtYmVyIl19LCJrZXljbG9hay1jbGllbnQiOnsicm9sZXMiOlsidW1hX3Byb3RlY3Rpb24iXX0sIiUyRmdjdWJlJTJGZGV2c2VjJTJGZGV2VlJFIjp7InJvbGVzIjpbIk1lbWJlciJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SWQiOiJrZXljbG9hay1jbGllbnQiLCJjbGllbnRIb3N0IjoiOTMuNjYuMTg1Ljc1IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzZXJ2aWNlLWFjY291bnQta2V5Y2xvYWstY2xpZW50IiwiY2xpZW50QWRkcmVzcyI6IjkzLjY2LjE4NS43NSJ9.FQu4ox2HWeqeaY7nHYVGeJVpkJOcASfOb8tbOUeG-GB6sMjRB2S8PjLLaw63r_c42yxKszP04XdxGqIWqXTtoD9QCiUHTT5yJTkIpio4tMMGHth9Fbx-9dwk0yy_IFi1_OsCvZFmOQRdjMuUkj1lSqslCzAw-2E5q1Zt415-au5pEVJYNTFqIsG72ChJwh6eq1Dh1XBy8krb7YVPQyIwxO_awgAYO5hbsdvXYlRfCrnB38kk2V6-CQ-XYoL1m7xIB-gjhKCiFvDmmntQSRCZFgb0qi8eOmh9FdzPxZgx7yPJwAAj17dS4B_gz9FpZBVciNzpA6Lf4P2bqvoD9-R6ow"; protected static final String OLD_UMA_ACCESS_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJSSklZNEpoNF9qdDdvNmREY0NlUDFfS1l0akcxVExXVW9oMkQ2Tzk1bFNBIn0.eyJleHAiOjE2NTI5ODA0NzgsImlhdCI6MTY1Mjk4MDE3OCwianRpIjoiNjBkNzU3MGMtZmQxOC00NGQ1LTg1MzUtODhlMmFmOGQ1ZTgwIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5kZXYuZDRzY2llbmNlLm9yZy9hdXRoL3JlYWxtcy9kNHNjaWVuY2UiLCJhdWQiOiJjb25kdWN0b3Itc2VydmVyIiwic3ViIjoiYTQ3ZGZlMTYtYjRlZC00NGVkLWExZDktOTdlY2Q1MDQzNjBjIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoia2V5Y2xvYWstY2xpZW50Iiwic2Vzc2lvbl9zdGF0ZSI6IjI3NDUyN2M5LWNkZjMtNGM2Yi1iNTUxLTFmMTRkZGE5ZGVlZiIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiSW5mcmFzdHJ1Y3R1cmUtQ2xpZW50Il19LCJhdXRob3JpemF0aW9uIjp7InBlcm1pc3Npb25zIjpbeyJzY29wZXMiOlsiZ2V0Il0sInJzaWQiOiIyNDlmZDQ2OS03OWM1LTRiODUtYjE5NS1mMjliM2ViNjAzNDUiLCJyc25hbWUiOiJtZXRhZGF0YSJ9LHsic2NvcGVzIjpbImdldCIsInN0YXJ0IiwidGVybWluYXRlIl0sInJzaWQiOiJhNmYzZWFkZS03NDA0LTRlNWQtOTA3MC04MDBhZGI1YWFjNGUiLCJyc25hbWUiOiJ3b3JrZmxvdyJ9LHsicnNpZCI6IjFiNmMwMGI3LTkxMzktNGVhYS1hYWM3LTIwMjMxZmVlMDVhNSIsInJzbmFtZSI6IkRlZmF1bHQgUmVzb3VyY2UifV19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJjbGllbnRJZCI6ImtleWNsb2FrLWNsaWVudCIsImNsaWVudEhvc3QiOiI5My42Ni4xODUuNzUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC1rZXljbG9hay1jbGllbnQiLCJjbGllbnRBZGRyZXNzIjoiOTMuNjYuMTg1Ljc1In0.Hh62E56R-amHwoDPFQEylMvrvmNzWnC_4bDI7_iQYAPJ5YzCNH9d7zcdGaQ96kRmps_JRc2Giv_1W9kYorOhlXl-5QLDrSoqrqFxrNpEGG5r5jpNJbusbu4wNUKiCt_GMnM1UmztgXiQeuggNGkmeBIjotj0eubnmIbUV9ukHj3v7Z5PwNKKX3BCpsghd1u8lg6Nfqk_Oho4GXUfdaFY_AR3SNqzVI_9YLhND_a03MNNWlnfOvj8T4nDCKBZIs91tVyiu98d2TjnQt8PdlVwokMP3LA58m0Khy2cmUm1KF2k0zlzP8MxV9wTxNrpovMr-PnbtEPZ_IlVQIzHwjHfwQ"; @@ -102,6 +104,30 @@ public class TestKeycloakClient { TestModels.checkTokenResponse(oidcTR); TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), "service-account-" + CLIENT_ID, false); } + + @Test + public void test13QueryOIDCTokenOfUser() throws Exception { + logger.info("*** [1.3] Start testing query OIDC token from Keycloak with URL for user..."); + oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUser(DEV_ROOT_CONTEXT, CLIENT_ID, + CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD); + + logger.info("*** [1.3] OIDC access token: {}", oidcTR.getAccessToken()); + logger.info("*** [1.3] OIDC refresh token: {}", oidcTR.getRefreshToken()); + TestModels.checkTokenResponse(oidcTR); + TestModels.checkAccessToken(ModelUtils.getAccessTokenFrom(oidcTR), TEST_USER_USERNAME, false); + } + + @Test + public void test13aQueryOIDCTokenOfUserWithContext() throws Exception { + logger.info("*** [1.3] Start testing query OIDC token from Keycloak with URL for user..."); + oidcTR = KeycloakClientFactory.newInstance().queryOIDCTokenOfUserWithContext(DEV_ROOT_CONTEXT, CLIENT_ID, + CLIENT_SECRET, TEST_USER_USERNAME, TEST_USER_PASSWORD, TEST_AUDIENCE); + + 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, true); + } @Test public void test24QueryUMAToken() throws Exception {