2424import java .util .ArrayList ;
2525import java .util .Base64 ;
2626import java .util .List ;
27+ import java .util .concurrent .TimeUnit ;
2728
2829import javax .inject .Inject ;
2930import javax .ws .rs .core .HttpHeaders ;
3031
3132import org .apache .cloudstack .auth .UserOAuth2Authenticator ;
3233import org .apache .cloudstack .oauth2 .dao .OauthProviderDao ;
3334import org .apache .cloudstack .oauth2 .vo .OauthProviderVO ;
35+ import org .apache .commons .codec .digest .DigestUtils ;
3436import org .apache .commons .lang3 .StringUtils ;
3537import org .apache .cxf .rs .security .jose .jws .JwsJwtCompactConsumer ;
3638import org .apache .cxf .rs .security .jose .jwt .JwtClaims ;
4648import com .cloud .exception .CloudAuthenticationException ;
4749import com .cloud .utils .component .AdapterBase ;
4850import com .cloud .utils .exception .CloudRuntimeException ;
51+ import com .github .benmanes .caffeine .cache .Cache ;
52+ import com .github .benmanes .caffeine .cache .Caffeine ;
4953import com .google .gson .JsonElement ;
5054import com .google .gson .JsonObject ;
5155import com .google .gson .JsonParser ;
@@ -54,13 +58,17 @@ public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Aut
5458
5559 public static final String KEYCLOAK_PROVIDER = "keycloak" ;
5660
57- protected String idToken = null ;
58-
5961 @ Inject
6062 OauthProviderDao oauthProviderDao ;
6163
6264 private CloseableHttpClient httpClient ;
6365
66+ private final Cache <String , String > validatedEmailCache =
67+ Caffeine .newBuilder ()
68+ .expireAfterWrite (60 , TimeUnit .SECONDS )
69+ .maximumSize (1024 )
70+ .build ();
71+
6472 public KeycloakOAuth2Provider () {
6573 this (HttpClientBuilder .create ().build ());
6674 }
@@ -95,11 +103,14 @@ public boolean verifyUser(String email, String secretCode, Long domainId) {
95103 throw new CloudAuthenticationException ("Keycloak provider is not registered, so user cannot be verified" );
96104 }
97105
98- String verifiedEmail = verifySecretCodeAndFetchEmail (secretCode , domainId );
106+ String verifiedEmail = consumeValidatedEmailFromCache (secretCode );
107+ if (StringUtils .isBlank (verifiedEmail )) {
108+ verifiedEmail = verifySecretCodeAndFetchEmail (secretCode , domainId );
109+ consumeValidatedEmailFromCache (secretCode );
110+ }
99111 if (StringUtils .isBlank (verifiedEmail ) || !email .equals (verifiedEmail )) {
100112 throw new CloudRuntimeException ("Unable to verify the email address with the provided secret" );
101113 }
102- clearIdToken ();
103114
104115 return true ;
105116 }
@@ -116,46 +127,47 @@ public String verifySecretCodeAndFetchEmail(String secretCode, Long domainId) {
116127 throw new CloudAuthenticationException ("Keycloak provider is not registered, so user cannot be verified" );
117128 }
118129
119- if (StringUtils .isBlank (idToken )) {
120- String auth = provider .getClientId () + ":" + provider .getSecretKey ();
121- String encodedAuth = Base64 .getEncoder ().encodeToString (auth .getBytes (StandardCharsets .UTF_8 ));
130+ String auth = provider .getClientId () + ":" + provider .getSecretKey ();
131+ String encodedAuth = Base64 .getEncoder ().encodeToString (auth .getBytes (StandardCharsets .UTF_8 ));
122132
123- List <NameValuePair > params = new ArrayList <>();
124- params .add (new BasicNameValuePair ("grant_type" , "authorization_code" ));
125- params .add (new BasicNameValuePair ("code" , secretCode ));
126- params .add (new BasicNameValuePair ("redirect_uri" , provider .getRedirectUri ()));
133+ List <NameValuePair > params = new ArrayList <>();
134+ params .add (new BasicNameValuePair ("grant_type" , "authorization_code" ));
135+ params .add (new BasicNameValuePair ("code" , secretCode ));
136+ params .add (new BasicNameValuePair ("redirect_uri" , provider .getRedirectUri ()));
127137
128- HttpPost post = new HttpPost (provider .getTokenUrl ());
129- post .setHeader (HttpHeaders .AUTHORIZATION , "Basic " + encodedAuth );
138+ HttpPost post = new HttpPost (provider .getTokenUrl ());
139+ post .setHeader (HttpHeaders .AUTHORIZATION , "Basic " + encodedAuth );
130140
131- try {
132- post .setEntity (new UrlEncodedFormEntity (params ));
133- } catch (UnsupportedEncodingException e ) {
134- throw new CloudRuntimeException ("Unable to generate URL parameters: " + e .getMessage ());
135- }
136-
137- try (CloseableHttpResponse response = httpClient .execute (post )) {
138- String body = EntityUtils .toString (response .getEntity ());
141+ try {
142+ post .setEntity (new UrlEncodedFormEntity (params ));
143+ } catch (UnsupportedEncodingException e ) {
144+ throw new CloudRuntimeException ("Unable to generate URL parameters: " + e .getMessage ());
145+ }
139146
140- if ( response . getStatusLine (). getStatusCode () != 200 ) {
141- throw new CloudRuntimeException ( "Keycloak error during token generation: " + body );
142- }
147+ String idToken ;
148+ try ( CloseableHttpResponse response = httpClient . execute ( post )) {
149+ String body = EntityUtils . toString ( response . getEntity ());
143150
144- JsonObject json = JsonParser .parseString (body ).getAsJsonObject ();
145- JsonElement fetchedIdToken = json .get ("id_token" );
146- if (fetchedIdToken == null ) {
147- throw new CloudRuntimeException ("No id_token found in token" );
148- }
149- String idTokenAsString = fetchedIdToken .getAsString ();
150- validateIdToken (idTokenAsString , provider );
151+ if (response .getStatusLine ().getStatusCode () != 200 ) {
152+ throw new CloudRuntimeException ("Keycloak error during token generation: " + body );
153+ }
151154
152- this .idToken = idTokenAsString ;
153- } catch (IOException e ) {
154- throw new CloudRuntimeException ("Unable to connect to Keycloak server" , e );
155+ JsonObject json = JsonParser .parseString (body ).getAsJsonObject ();
156+ JsonElement fetchedIdToken = json .get ("id_token" );
157+ if (fetchedIdToken == null ) {
158+ throw new CloudRuntimeException ("No id_token found in token" );
155159 }
160+ idToken = fetchedIdToken .getAsString ();
161+ validateIdToken (idToken , provider );
162+ } catch (IOException e ) {
163+ throw new CloudRuntimeException ("Unable to connect to Keycloak server" , e );
156164 }
157165
158- return obtainEmail (idToken , provider );
166+ String email = obtainEmail (idToken , provider );
167+ if (StringUtils .isNotBlank (email )) {
168+ validatedEmailCache .put (getCacheKey (secretCode ), email );
169+ }
170+ return email ;
159171 }
160172
161173 @ Override
@@ -183,8 +195,17 @@ private String obtainEmail(String idTokenStr, OauthProviderVO provider) {
183195 return (String ) claims .getClaim ("email" );
184196 }
185197
186- protected void clearIdToken () {
187- idToken = null ;
198+ private String getCacheKey (final String secretCode ) {
199+ return DigestUtils .sha256Hex (secretCode );
200+ }
201+
202+ private String consumeValidatedEmailFromCache (final String secretCode ) {
203+ final String key = getCacheKey (secretCode );
204+ final String email = validatedEmailCache .getIfPresent (key );
205+ if (email != null ) {
206+ validatedEmailCache .invalidate (key );
207+ }
208+ return email ;
188209 }
189210
190211 public void setHttpClient (CloseableHttpClient httpClient ) {
0 commit comments