From 506f664bea65ba4d4d811606bbf647f3359151c2 Mon Sep 17 00:00:00 2001 From: "joel.tazzari" Date: Wed, 15 Apr 2026 19:14:34 +0200 Subject: [PATCH] Cache idToken --- .../keycloak/KeycloakOAuth2Provider.java | 71 ++++++++++++------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java index f6c9ff563c1..0d028cfc4a1 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java @@ -51,6 +51,8 @@ import com.google.gson.JsonParser; public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { + protected String idToken = null; + @Inject OauthProviderDao oauthProviderDao; @@ -89,6 +91,7 @@ public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Aut if (StringUtils.isBlank(verifiedEmail) || !email.equals(verifiedEmail)) { throw new CloudRuntimeException("Unable to verify the email address with the provided secret"); } + clearIdToken(); return true; } @@ -97,37 +100,42 @@ public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Aut public String verifyCodeAndFetchEmail(String secretCode) { OauthProviderVO provider = oauthProviderDao.findByProvider(getName()); - String auth = provider.getClientId() + ":" + provider.getSecretKey(); - String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + if (StringUtils.isBlank(idToken)) { + String auth = provider.getClientId() + ":" + provider.getSecretKey(); + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); - List params = new ArrayList<>(); - params.add(new BasicNameValuePair("grant_type", "authorization_code")); - params.add(new BasicNameValuePair("code", secretCode)); - params.add(new BasicNameValuePair("redirect_uri", provider.getRedirectUri())); + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("grant_type", "authorization_code")); + params.add(new BasicNameValuePair("code", secretCode)); + params.add(new BasicNameValuePair("redirect_uri", provider.getRedirectUri())); - HttpPost post = new HttpPost(provider.getTokenUrl()); - post.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodedAuth); + HttpPost post = new HttpPost(provider.getTokenUrl()); + post.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodedAuth); - try { - post.setEntity(new UrlEncodedFormEntity(params)); - } catch (UnsupportedEncodingException e) { - throw new CloudRuntimeException("Unable to generating URL parameters: " + e.getMessage()); - } - - try (CloseableHttpResponse response = httpClient.execute(post)) { - String body = EntityUtils.toString(response.getEntity()); - - if (response.getStatusLine().getStatusCode() != 200) { - throw new CloudRuntimeException("Keycloak error during token generation: " + body); + try { + post.setEntity(new UrlEncodedFormEntity(params)); + } catch (UnsupportedEncodingException e) { + throw new CloudRuntimeException("Unable to generating URL parameters: " + e.getMessage()); } - JsonObject json = JsonParser.parseString(body).getAsJsonObject(); - String idToken = json.get("id_token").getAsString(); + try (CloseableHttpResponse response = httpClient.execute(post)) { + String body = EntityUtils.toString(response.getEntity()); - return validateIdTokenAndGetEmail(idToken, provider); - } catch (IOException e) { - throw new CloudRuntimeException("Unable to connect to Keycloak server", e); + if (response.getStatusLine().getStatusCode() != 200) { + throw new CloudRuntimeException("Keycloak error during token generation: " + body); + } + + JsonObject json = JsonParser.parseString(body).getAsJsonObject(); + String idToken = json.get("id_token").getAsString(); + validateIdToken(idToken, provider); + + this.idToken = idToken; + } catch (IOException e) { + throw new CloudRuntimeException("Unable to connect to Keycloak server", e); + } } + + return obtainEmail(idToken, provider); } @Override @@ -135,7 +143,16 @@ public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Aut return null; } - private String validateIdTokenAndGetEmail(String idTokenStr, OauthProviderVO provider) { + private void validateIdToken(String idTokenStr, OauthProviderVO provider) { + JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(idTokenStr); + JwtClaims claims = jwtConsumer.getJwtToken().getClaims(); + + if (!claims.getAudiences().contains(provider.getClientId())) { + throw new CloudAuthenticationException("Audience mismatch"); + } + } + + private String obtainEmail(String idTokenStr, OauthProviderVO provider) { JwsJwtCompactConsumer jwtConsumer = new JwsJwtCompactConsumer(idTokenStr); JwtClaims claims = jwtConsumer.getJwtToken().getClaims(); @@ -146,6 +163,10 @@ public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Aut return (String) claims.getClaim("email"); } + protected void clearIdToken() { + idToken = null; + } + public void setHttpClient(CloseableHttpClient httpClient) { this.httpClient = httpClient; }