Skip to content

plugin/oauth2: fix thread-safety of GoogleOAuth2Provider token handling#13564

Open
rhenar0 wants to merge 3 commits into
apache:4.22from
rhenar0:fix/oauth2-google-thread-safety-4.22
Open

plugin/oauth2: fix thread-safety of GoogleOAuth2Provider token handling#13564
rhenar0 wants to merge 3 commits into
apache:4.22from
rhenar0:fix/oauth2-google-thread-safety-4.22

Conversation

@rhenar0

@rhenar0 rhenar0 commented Jul 7, 2026

Copy link
Copy Markdown

Description

This PR fixes a thread-safety bug in GoogleOAuth2Provider that breaks Google OAuth2 login under concurrent use, and can leave the provider permanently broken until the management server is restarted.

GoogleOAuth2Provider is a singleton Spring bean, but it stored the OAuth2 accessToken / refreshToken in shared mutable instance fields:

  1. Race condition between concurrent logins: the StringUtils.isAnyEmpty(accessToken, refreshToken) guard in verifyCodeAndFetchEmail() made a concurrent login B skip exchanging its own authorization code and call the userinfo endpoint with login A's tokens. Google then returns A's email, which does not match B's, so verifyUser() fails with Unable to verify the email address with the provided secret.
  2. Poisoned state is never cleaned up: clearAccessAndRefreshTokens() was only called on the success path of verifyUser(), so any failure left stale tokens in the singleton, and every subsequent Google login reused them and failed, until a cloudstack-management restart.

The token caching provided no benefit: each login carries its own one-time authorization code that must be exchanged individually.

Changes:

  • Remove the accessToken / refreshToken instance fields; verifyCodeAndFetchEmail() now always exchanges the authorization code and keeps the tokens in local variables.
  • Remove clearAccessAndRefreshTokens() and its call site in verifyUser().
  • Surface token exchange failures as a CloudRuntimeException with a meaningful message instead of a bare RuntimeException.
  • Adapt GoogleOAuth2ProviderTest to mock the token exchange flow, and add tests covering that every call performs its own token exchange and that a failed exchange raises a CloudRuntimeException.

No change to the OAuth scopes, the userinfo call, or the email comparison logic.

Fixes: #13563

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI
  • Test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Feature/Enhancement Scale

  • Major
  • Minor

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

Screenshots (if appropriate):

How Has This Been Tested?

  • mvn -pl plugins/user-authenticators/oauth2 -am clean install -DskipTests followed by mvn -pl plugins/user-authenticators/oauth2 test: all 29 unit tests of the module pass, including the 8 tests of GoogleOAuth2ProviderTest (2 of them new).
  • The new testVerifyCodeAndFetchEmailExchangesCodeOnEveryCall test verifies that two successive calls to verifyCodeAndFetchEmail() each trigger their own token exchange (flow.newTokenRequest(...) called once per code), which is exactly the scenario broken by the shared-state guard.
  • The bug itself was observed and reproduced on a CloudStack 4.22.0.0 production environment (Rocky Linux management server, KVM, Google as primary login method for ~hundreds of users); see OAuth2 Google login permanently breaks under concurrent logins due to shared mutable token state in GoogleOAuth2Provider #13563 for the reproduction steps.

How did you try to break this feature and the system with this change?

  • The new testVerifyUserWithFailedTokenExchange test submits a failing token exchange and asserts it surfaces as a CloudRuntimeException; with no shared state left in the singleton, a failed login can no longer affect subsequent logins.
  • Checked the other UserOAuth2Authenticator call sites: verifyCodeAndFetchEmail() and verifyUser() keep their existing signatures and semantics, and the removed members were internal to this provider (clearAccessAndRefreshTokens() had no other callers, and the accessToken/refreshToken fields were only referenced by the provider and its test).

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 <noreply@anthropic.com>
@boring-cyborg

boring-cyborg Bot commented Jul 7, 2026

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache CloudStack community! If you have any issues or are unsure about any anything please check our Contribution Guide (https://github.com/apache/cloudstack/blob/main/CONTRIBUTING.md)
Here are some useful points:

@PrashantBhanage PrashantBhanage left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic looks solid and the thread-safety fix is great. Just found a small runtime exception syntax error and a minor variable naming cleanup.

rhenar0 and others added 2 commits July 8, 2026 13:05
…oudstack/oauth2/google/GoogleOAuth2Provider.java

Co-authored-by: prrssshhhh <prashantbhanage717@gmail.com>
…oudstack/oauth2/google/GoogleOAuth2Provider.java

Co-authored-by: prrssshhhh <prashantbhanage717@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants