From f16b3d704b10ffa3c695de918e78c66e24d26233 Mon Sep 17 00:00:00 2001 From: Hope | Revan - Hugo CHASSAING Date: Tue, 7 Jul 2026 18:24:11 +0200 Subject: [PATCH 1/6] plugin/oauth2: fix thread-safety of GoogleOAuth2Provider token handling GoogleOAuth2Provider is a Spring singleton but stored the OAuth2 access and refresh tokens in shared instance fields. Under concurrent logins, the StringUtils.isAnyEmpty() guard made a second login reuse the first login's tokens instead of exchanging its own authorization code, so Google returned the first user's email and verification failed with "Unable to verify the email address with the provided secret". Additionally, clearAccessAndRefreshTokens() was only invoked on the success path of verifyUser(), so any failure left stale tokens in the singleton and every subsequent login failed until a management server restart. Fix by removing the instance fields entirely: always exchange the authorization code in verifyCodeAndFetchEmail() and keep the tokens in local variables. Also surface token exchange failures as a CloudRuntimeException with a meaningful message instead of a bare RuntimeException. Co-Authored-By: Claude Fable 5 --- .../oauth2/google/GoogleOAuth2Provider.java | 29 ++--- .../google/GoogleOAuth2ProviderTest.java | 114 +++++++++++++----- 2 files changed, 96 insertions(+), 47 deletions(-) diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java index 42ed1451ccd5..9501a0ddb9a5 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -40,9 +40,6 @@ public class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { - protected String accessToken = null; - protected String refreshToken = null; - @Inject OauthProviderDao _oauthProviderDao; @@ -71,7 +68,6 @@ public boolean verifyUser(String email, String secretCode) { if (verifiedEmail == null || !email.equals(verifiedEmail)) { throw new CloudRuntimeException("Unable to verify the email address with the provided secret"); } - clearAccessAndRefreshTokens(); return true; } @@ -96,18 +92,16 @@ public String verifyCodeAndFetchEmail(String secretCode) { httpTransport, jsonFactory, clientSecrets, scopes) .build(); - if (StringUtils.isAnyEmpty(accessToken, refreshToken)) { - GoogleTokenResponse tokenResponse = null; - try { - tokenResponse = flow.newTokenRequest(secretCode) - .setRedirectUri(redirectURI) - .execute(); - } catch (IOException e) { - throw new RuntimeException(e); - } - accessToken = tokenResponse.getAccessToken(); - refreshToken = tokenResponse.getRefreshToken(); + GoogleTokenResponse tokenResponse = null; + try { + tokenResponse = flow.newTokenRequest(secretCode) + .setRedirectUri(redirectURI) + .execute(); + } catch (IOException e) { + throw new CloudRuntimeException(String.format("Failed to exchange the OAuth2 authorization code for tokens: %s", e.getMessage()), e); } + String accessToken = tokenResponse.getAccessToken(); + String refreshToken = tokenResponse.getRefreshToken(); GoogleCredential credential = new GoogleCredential.Builder() .setTransport(httpTransport) @@ -127,11 +121,6 @@ public String verifyCodeAndFetchEmail(String secretCode) { return userinfo.getEmail(); } - protected void clearAccessAndRefreshTokens() { - accessToken = null; - refreshToken = null; - } - @Override public String getUserEmailAddress() throws CloudRuntimeException { return null; diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java index fa8a5a7c03cf..5ab28d4bc76c 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java @@ -19,6 +19,9 @@ import com.cloud.exception.CloudAuthenticationException; import com.cloud.utils.exception.CloudRuntimeException; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; +import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; import com.google.api.services.oauth2.Oauth2; import com.google.api.services.oauth2.model.Userinfo; import org.apache.cloudstack.oauth2.dao.OauthProviderDao; @@ -35,10 +38,13 @@ import java.io.IOException; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class GoogleOAuth2ProviderTest { @@ -62,6 +68,26 @@ public void tearDown() throws Exception { closeable.close(); } + private OauthProviderVO mockRegisteredProvider() { + OauthProviderVO providerVO = mock(OauthProviderVO.class); + when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); + when(providerVO.getProvider()).thenReturn("testProvider"); + when(providerVO.getSecretKey()).thenReturn("testSecret"); + when(providerVO.getClientId()).thenReturn("testClientid"); + return providerVO; + } + + private GoogleAuthorizationCodeFlow mockTokenExchangeFlow(GoogleAuthorizationCodeTokenRequest tokenRequest) throws IOException { + GoogleAuthorizationCodeFlow flow = mock(GoogleAuthorizationCodeFlow.class); + GoogleTokenResponse tokenResponse = mock(GoogleTokenResponse.class); + when(flow.newTokenRequest(anyString())).thenReturn(tokenRequest); + when(tokenRequest.setRedirectUri(any())).thenReturn(tokenRequest); + when(tokenRequest.execute()).thenReturn(tokenResponse); + when(tokenResponse.getAccessToken()).thenReturn("testAccessToken"); + when(tokenResponse.getRefreshToken()).thenReturn("testRefreshToken"); + return flow; + } + @Test(expected = CloudAuthenticationException.class) public void testVerifyUserWithNullEmail() { _googleOAuth2Provider.verifyUser(null, "secretCode"); @@ -80,15 +106,13 @@ public void testVerifyUserWithUnregisteredProvider() { @Test(expected = CloudRuntimeException.class) public void testVerifyUserWithInvalidSecretCode() throws IOException { - OauthProviderVO providerVO = mock(OauthProviderVO.class); - when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); - when(providerVO.getProvider()).thenReturn("testProvider"); - when(providerVO.getSecretKey()).thenReturn("testSecret"); - when(providerVO.getClientId()).thenReturn("testClientid"); - _googleOAuth2Provider.accessToken = "testAccessToken"; - _googleOAuth2Provider.refreshToken = "testRefreshToken"; + mockRegisteredProvider(); + GoogleAuthorizationCodeTokenRequest tokenRequest = mock(GoogleAuthorizationCodeTokenRequest.class); + GoogleAuthorizationCodeFlow flow = mockTokenExchangeFlow(tokenRequest); Oauth2 oauth2 = mock(Oauth2.class); - try (MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + try (MockedConstruction ignoredFlow = Mockito.mockConstruction(GoogleAuthorizationCodeFlow.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(flow)); + MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, (mock, context) -> when(mock.build()).thenReturn(oauth2))) { Userinfo userinfo = mock(Userinfo.class); Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); @@ -104,15 +128,13 @@ public void testVerifyUserWithInvalidSecretCode() throws IOException { @Test(expected = CloudRuntimeException.class) public void testVerifyUserWithMismatchedEmail() throws IOException { - OauthProviderVO providerVO = mock(OauthProviderVO.class); - when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); - when(providerVO.getProvider()).thenReturn("testProvider"); - when(providerVO.getSecretKey()).thenReturn("testSecret"); - when(providerVO.getClientId()).thenReturn("testClientid"); - _googleOAuth2Provider.accessToken = "testAccessToken"; - _googleOAuth2Provider.refreshToken = "testRefreshToken"; + mockRegisteredProvider(); + GoogleAuthorizationCodeTokenRequest tokenRequest = mock(GoogleAuthorizationCodeTokenRequest.class); + GoogleAuthorizationCodeFlow flow = mockTokenExchangeFlow(tokenRequest); Oauth2 oauth2 = mock(Oauth2.class); - try (MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + try (MockedConstruction ignoredFlow = Mockito.mockConstruction(GoogleAuthorizationCodeFlow.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(flow)); + MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, (mock, context) -> when(mock.build()).thenReturn(oauth2))) { Userinfo userinfo = mock(Userinfo.class); Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); @@ -126,17 +148,29 @@ public void testVerifyUserWithMismatchedEmail() throws IOException { } } + @Test(expected = CloudRuntimeException.class) + public void testVerifyUserWithFailedTokenExchange() throws IOException { + mockRegisteredProvider(); + GoogleAuthorizationCodeFlow flow = mock(GoogleAuthorizationCodeFlow.class); + GoogleAuthorizationCodeTokenRequest tokenRequest = mock(GoogleAuthorizationCodeTokenRequest.class); + when(flow.newTokenRequest(anyString())).thenReturn(tokenRequest); + when(tokenRequest.setRedirectUri(any())).thenReturn(tokenRequest); + when(tokenRequest.execute()).thenThrow(new IOException("invalid_grant")); + try (MockedConstruction ignoredFlow = Mockito.mockConstruction(GoogleAuthorizationCodeFlow.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(flow))) { + _googleOAuth2Provider.verifyUser("email@example.com", "secretCode"); + } + } + @Test public void testVerifyUserEmail() throws IOException { - OauthProviderVO providerVO = mock(OauthProviderVO.class); - when(_oauthProviderDao.findByProvider(anyString())).thenReturn(providerVO); - when(providerVO.getProvider()).thenReturn("testProvider"); - when(providerVO.getSecretKey()).thenReturn("testSecret"); - when(providerVO.getClientId()).thenReturn("testClientid"); - _googleOAuth2Provider.accessToken = "testAccessToken"; - _googleOAuth2Provider.refreshToken = "testRefreshToken"; + mockRegisteredProvider(); + GoogleAuthorizationCodeTokenRequest tokenRequest = mock(GoogleAuthorizationCodeTokenRequest.class); + GoogleAuthorizationCodeFlow flow = mockTokenExchangeFlow(tokenRequest); Oauth2 oauth2 = mock(Oauth2.class); - try (MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + try (MockedConstruction ignoredFlow = Mockito.mockConstruction(GoogleAuthorizationCodeFlow.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(flow)); + MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, (mock, context) -> when(mock.build()).thenReturn(oauth2))) { Userinfo userinfo = mock(Userinfo.class); Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); @@ -149,8 +183,34 @@ public void testVerifyUserEmail() throws IOException { boolean result = _googleOAuth2Provider.verifyUser("email@example.com", "secretCode"); assertTrue(result); - assertNull(_googleOAuth2Provider.accessToken); - assertNull(_googleOAuth2Provider.refreshToken); + verify(tokenRequest, times(1)).execute(); + } + } + + @Test + public void testVerifyCodeAndFetchEmailExchangesCodeOnEveryCall() throws IOException { + mockRegisteredProvider(); + GoogleAuthorizationCodeTokenRequest tokenRequest = mock(GoogleAuthorizationCodeTokenRequest.class); + GoogleAuthorizationCodeFlow flow = mockTokenExchangeFlow(tokenRequest); + Oauth2 oauth2 = mock(Oauth2.class); + try (MockedConstruction ignoredFlow = Mockito.mockConstruction(GoogleAuthorizationCodeFlow.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(flow)); + MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(oauth2))) { + Userinfo userinfo = mock(Userinfo.class); + Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); + when(oauth2.userinfo()).thenReturn(userinfo1); + Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class); + when(userinfo1.get()).thenReturn(userinfoGet); + when(userinfoGet.execute()).thenReturn(userinfo); + when(userinfo.getEmail()).thenReturn("email@example.com"); + + assertEquals("email@example.com", _googleOAuth2Provider.verifyCodeAndFetchEmail("secretCode1")); + assertEquals("email@example.com", _googleOAuth2Provider.verifyCodeAndFetchEmail("secretCode2")); + + verify(flow, times(1)).newTokenRequest("secretCode1"); + verify(flow, times(1)).newTokenRequest("secretCode2"); + verify(tokenRequest, times(2)).execute(); } } } From c83e076ac94900638d772709691182b34cce6c15 Mon Sep 17 00:00:00 2001 From: Hope | Revan - Hugo CHASSAING Date: Wed, 8 Jul 2026 13:05:14 +0200 Subject: [PATCH 2/6] Update plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java Co-authored-by: prrssshhhh --- .../cloudstack/oauth2/google/GoogleOAuth2Provider.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java index 9501a0ddb9a5..69a7f39f51ef 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -74,7 +74,10 @@ public boolean verifyUser(String email, String secretCode) { @Override public String verifyCodeAndFetchEmail(String secretCode) { - OauthProviderVO githubProvider = _oauthProviderDao.findByProvider(getName()); + OauthProviderVO googleProvider = _oauthProviderDao.findByProvider(getName()); + String clientId = googleProvider.getClientId(); + String secret = googleProvider.getSecretKey(); + String redirectURI = googleProvider.getRedirectUri(); String clientId = githubProvider.getClientId(); String secret = githubProvider.getSecretKey(); String redirectURI = githubProvider.getRedirectUri(); From 2237bfc479b81a817b0ba303d06e64916304acbd Mon Sep 17 00:00:00 2001 From: Hope | Revan - Hugo CHASSAING Date: Wed, 8 Jul 2026 13:05:21 +0200 Subject: [PATCH 3/6] Update plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java Co-authored-by: prrssshhhh --- .../apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java index 69a7f39f51ef..c0e402d0a922 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -119,7 +119,7 @@ public String verifyCodeAndFetchEmail(String secretCode) { try { userinfo = oauth2.userinfo().get().execute(); } catch (IOException e) { - throw new CloudRuntimeException(String.format("Failed to fetch the email address with the provided secret: %s" + e.getMessage())); + throw new CloudRuntimeException(String.format("Failed to fetch the email address with the provided secret: %s", e.getMessage()), e); } return userinfo.getEmail(); } From eace76f5716b8502b62237ddb11cbfb0ca4f3b82 Mon Sep 17 00:00:00 2001 From: Hope | Revan - Hugo CHASSAING Date: Thu, 9 Jul 2026 09:50:28 +0200 Subject: [PATCH 4/6] plugin/oauth2: fix misapplied review suggestion The rename suggestion was applied on top of the original lines instead of replacing them, leaving duplicate variable declarations referencing the removed githubProvider variable, which broke compilation. Co-Authored-By: Claude Fable 5 --- .../apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java index c0e402d0a922..899edef7b328 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -78,9 +78,6 @@ public String verifyCodeAndFetchEmail(String secretCode) { String clientId = googleProvider.getClientId(); String secret = googleProvider.getSecretKey(); String redirectURI = googleProvider.getRedirectUri(); - String clientId = githubProvider.getClientId(); - String secret = githubProvider.getSecretKey(); - String redirectURI = githubProvider.getRedirectUri(); GoogleClientSecrets clientSecrets = new GoogleClientSecrets() .setWeb(new GoogleClientSecrets.Details() .setClientId(clientId) From 2ee36f3953b18a160845fa7cd2e714fe1f1f0076 Mon Sep 17 00:00:00 2001 From: Hope | Revan - Hugo CHASSAING Date: Thu, 9 Jul 2026 10:19:29 +0200 Subject: [PATCH 5/6] plugin/oauth2: cache OAuth2 tokens per authorization code The CloudStack OAuth2 login flow exchanges the same one-time authorization code twice: once in verifyOauthCodeAndGetUser to fetch the user's email, and once in the subsequent oauthlogin call that authenticates the user. Unconditionally exchanging the code broke the login flow, as Google rejects the second exchange of a one-time code with invalid_grant. Cache the exchanged tokens keyed by the authorization code instead (bounded size, short TTL): the second phase of a login reuses the tokens obtained for its own code, while distinct logins never share state, keeping the provider thread-safe. Failed exchanges are not cached, so a failure cannot poison subsequent logins. Tested on a 4.22.0.0 environment: concurrent Google logins succeed and a failed login no longer breaks subsequent ones. --- .../oauth2/google/GoogleOAuth2Provider.java | 34 ++++++++++++++----- .../google/GoogleOAuth2ProviderTest.java | 28 ++++++++++++++- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java index 899edef7b328..906527bea951 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.oauth2.google; import com.cloud.exception.CloudAuthenticationException; +import com.cloud.utils.Pair; import com.cloud.utils.component.AdapterBase; import com.cloud.utils.exception.CloudRuntimeException; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; @@ -28,6 +29,9 @@ import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.services.oauth2.Oauth2; import com.google.api.services.oauth2.model.Userinfo; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.util.concurrent.UncheckedExecutionException; import org.apache.cloudstack.auth.UserOAuth2Authenticator; import org.apache.cloudstack.oauth2.dao.OauthProviderDao; import org.apache.cloudstack.oauth2.vo.OauthProviderVO; @@ -37,9 +41,19 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; public class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { + // The login flow exchanges the same one-time authorization code twice (once in + // verifyOauthCodeAndGetUser, once in the subsequent oauthlogin), so the tokens are + // cached per code; entries are short-lived as codes are only valid for a few minutes. + protected final Cache> tokensByCode = CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterWrite(10, TimeUnit.MINUTES) + .build(); + @Inject OauthProviderDao _oauthProviderDao; @@ -92,16 +106,20 @@ public String verifyCodeAndFetchEmail(String secretCode) { httpTransport, jsonFactory, clientSecrets, scopes) .build(); - GoogleTokenResponse tokenResponse = null; + Pair tokens; try { - tokenResponse = flow.newTokenRequest(secretCode) - .setRedirectUri(redirectURI) - .execute(); - } catch (IOException e) { - throw new CloudRuntimeException(String.format("Failed to exchange the OAuth2 authorization code for tokens: %s", e.getMessage()), e); + tokens = tokensByCode.get(secretCode, () -> { + GoogleTokenResponse tokenResponse = flow.newTokenRequest(secretCode) + .setRedirectUri(redirectURI) + .execute(); + return new Pair<>(tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); + }); + } catch (ExecutionException | UncheckedExecutionException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + throw new CloudRuntimeException(String.format("Failed to exchange the OAuth2 authorization code for tokens: %s", cause.getMessage()), cause); } - String accessToken = tokenResponse.getAccessToken(); - String refreshToken = tokenResponse.getRefreshToken(); + String accessToken = tokens.first(); + String refreshToken = tokens.second(); GoogleCredential credential = new GoogleCredential.Builder() .setTransport(httpTransport) diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java index 5ab28d4bc76c..72e52fbe2863 100644 --- a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java +++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2ProviderTest.java @@ -188,7 +188,7 @@ public void testVerifyUserEmail() throws IOException { } @Test - public void testVerifyCodeAndFetchEmailExchangesCodeOnEveryCall() throws IOException { + public void testVerifyCodeAndFetchEmailExchangesEachCodeIndependently() throws IOException { mockRegisteredProvider(); GoogleAuthorizationCodeTokenRequest tokenRequest = mock(GoogleAuthorizationCodeTokenRequest.class); GoogleAuthorizationCodeFlow flow = mockTokenExchangeFlow(tokenRequest); @@ -213,4 +213,30 @@ public void testVerifyCodeAndFetchEmailExchangesCodeOnEveryCall() throws IOExcep verify(tokenRequest, times(2)).execute(); } } + + @Test + public void testVerifyCodeAndFetchEmailReusesTokensForSameCode() throws IOException { + mockRegisteredProvider(); + GoogleAuthorizationCodeTokenRequest tokenRequest = mock(GoogleAuthorizationCodeTokenRequest.class); + GoogleAuthorizationCodeFlow flow = mockTokenExchangeFlow(tokenRequest); + Oauth2 oauth2 = mock(Oauth2.class); + try (MockedConstruction ignoredFlow = Mockito.mockConstruction(GoogleAuthorizationCodeFlow.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(flow)); + MockedConstruction ignored = Mockito.mockConstruction(Oauth2.Builder.class, + (mock, context) -> when(mock.build()).thenReturn(oauth2))) { + Userinfo userinfo = mock(Userinfo.class); + Oauth2.Userinfo userinfo1 = mock(Oauth2.Userinfo.class); + when(oauth2.userinfo()).thenReturn(userinfo1); + Oauth2.Userinfo.Get userinfoGet = mock(Oauth2.Userinfo.Get.class); + when(userinfo1.get()).thenReturn(userinfoGet); + when(userinfoGet.execute()).thenReturn(userinfo); + when(userinfo.getEmail()).thenReturn("email@example.com"); + + // the login flow uses the same one-time code twice: verifyOauthCodeAndGetUser then oauthlogin + assertEquals("email@example.com", _googleOAuth2Provider.verifyCodeAndFetchEmail("secretCode")); + assertTrue(_googleOAuth2Provider.verifyUser("email@example.com", "secretCode")); + + verify(tokenRequest, times(1)).execute(); + } + } } From f2584204c1a3adf54a1b2da27054bd8e2ce53d05 Mon Sep 17 00:00:00 2001 From: Hope | Revan - Hugo CHASSAING Date: Thu, 9 Jul 2026 10:23:22 +0200 Subject: [PATCH 6/6] Remove redundant comments from GoogleOAuth2Provider Remove redundant comments from GoogleOAuth2Provider --- .../apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java index 906527bea951..7d4927b9cf27 100644 --- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java +++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java @@ -46,9 +46,6 @@ public class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator { - // The login flow exchanges the same one-time authorization code twice (once in - // verifyOauthCodeAndGetUser, once in the subsequent oauthlogin), so the tokens are - // cached per code; entries are short-lived as codes are only valid for a few minutes. protected final Cache> tokensByCode = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES)