Skip to content

oauth2: add a generic OIDC provider type - #14205

Open
nagaboinaramgopal wants to merge 3 commits into
apache:mainfrom
nagaboinaramgopal:feature/generic-oidc-provider
Open

nagaboinaramgopal wants to merge 3 commits into
apache:mainfrom
nagaboinaramgopal:feature/generic-oidc-provider

Conversation

@nagaboinaramgopal

@nagaboinaramgopal nagaboinaramgopal commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Description

The OAuth2 plugin resolves a login to a UserOAuth2Authenticator through a fixed
provider-name to Spring-bean map. Each OIDC vendor is its own bean
(GoogleOAuth2Provider, GithubOAuth2Provider, KeycloakOAuth2Provider) running the
same authorization-code flow with no vendor-specific logic in it. Two consequences:
adding an IdP means shipping a new class, and because provider is simultaneously the
display name and the routing key, a domain can register exactly one keycloak.

This PR decouples the two by adding a type column to oauth_provider:

  • OAuth2AuthManagerImpl.getUserOAuth2AuthenticationProvider looks the name up in the
    bean map first, as before, and only on a miss falls back to the registration's type.
    provider becomes an admin-chosen label; type selects the implementation. One bean
    can serve any number of registrations under arbitrary names.
  • getUserOAuth2AuthenticationProvider, verifyUser and verifySecretCodeAndFetchEmail
    now carry the registration name, so a shared bean knows which row it is acting for.
  • Existing google/github/keycloak rows have a null type and continue to dispatch
    by name, on the same code path as today.

GenericOIDCOAuth2Provider is registered under type=oidc and configured with the issuer
URL alone. It reads token_endpoint and jwks_uri from the issuer's
.well-known/openid-configuration (cached; the document's issuer must match the
configured value), and validates the id_token before trusting any claim in it: signature
against the JWKS key named by the token kid, then issuer, audience and expiry, via CXF's
JwsJwtCompactConsumer / JwkUtils / JwtUtils. That is cxf-rt-rs-security-jose,
already on the plugin classpath, so no new dependency.

Unlike the vendor providers, it holds no token in an instance field. The code is exchanged
per call, and a code verified through verifyOAuthCodeAndGetUser is cached for 60s and
consumed once, so the oauthlogin that immediately follows does not re-present it to the
IdP.

API / response fix: ListOAuthProvidersCmd and UpdateOAuthProviderCmd derived the
response enabled flag from authenticatorPluginNames.contains(provider), a
name-to-bean check, which reported every generically-named registration as disabled. Both
now also accept a registration whose type resolves to a plugin.

type is settable on register but not on update: changing it would swap the
implementation under an existing row.

Schema: adds type and issuer_url to oauth_provider in the 4.23.0.0 to 24.0.0
upgrade file.

UI: Login.vue renders a button per registered oidc provider (in addition to the
existing google/github/keycloak buttons), labelled "Sign in with ". Clicking it
reads the issuer's authorization_endpoint from discovery and starts the
authorization-code flow. As with the keycloak provider, the registered redirecturi must
carry verifyOauth (for example https://<ui-host>/client?verifyOauth) so the callback
lands on the verify handler.

Fixes: #9609

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

Screenshots (if appropriate):

1-login-with-oidc-button 2-keycloak-login 3-dashboard-logged-in

How Has This Been Tested?

Unit tests - oauth2 module, 103 tests green.

  • GenericOIDCOAuth2ProviderTest (27 tests) generates an RSA keypair, publishes the
    matching JWKS, and asserts that a genuinely signed token is accepted while these are
    rejected: a token with altered claims, one signed by a different key, one naming a kid
    absent from the JWKS, and issuer / audience / expiry mismatches. Also covers discovery
    document validation (issuer mismatch) and that a verified code is redeemed exactly once.
  • OAuth2AuthManagerImplTest covers the type fallback, an unknown type rejected at
    register time, and a real bean name winning without a DB lookup.
  • Vendor provider tests updated for the name-carrying signatures.
  • DatabaseUpgradeCheckerTest green with the new schema file.

Integration - KVM advanced zone against Keycloak 25. Registered an oidc provider
pointing at a Keycloak realm and logged a real user in through the Keycloak form:
verifyOAuthCodeAndGetUser validates the RS256 signature against the realm JWKS and
returns the email, oauthlogin with the same code issues the session, and
listOauthProviders reports the generically-named provider as enabled.

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

  • A login presenting a valid, correctly signed token but claiming a different CloudStack
    user's email - rejected, no session.
  • Replay of an already-consumed code - rejected, no session.
  • A bogus/garbage code - rejected, no session.
  • After each of the above, a fresh correct login still succeeds.
  • Existing google/github/keycloak registrations are untouched and still log in
    (name-based dispatch, null type).
  • Discovery document whose issuer does not match the configured issuer URL - rejected.

@nagaboinaramgopal

Copy link
Copy Markdown
Contributor Author

This is the generic OIDC provider from the discussion on #13854: one oidc type that serves any number of registrations under admin-chosen names, with discovery and full id_token validation, so a new IdP no longer needs its own class.

It overlaps #13499 (ForgeRock), which @bddvlpr put on hold in favour of doing the generic feature. My GenericOIDCOAuth2Provider is separate from the AbstractOIDCOAuth2Provider there, but three files touch the same code: RegisterOAuthProviderCmd, spring-oauth2-context.xml and Login.vue. Whichever lands second is a small rebase and I am happy for that to be mine.

@bddvlpr @Damans227 this builds on your design, so please review. If you would rather fold the ForgeRock case in here as an oidc registration, or keep it as a named provider on top, either works.

@nagaboinaramgopal

Copy link
Copy Markdown
Contributor Author

@DaanHoogland @weizhouapache Could you pls take a look

@DaanHoogland

Copy link
Copy Markdown
Contributor

@blueorangutan package

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 3.70%. Comparing base (1a48a87) to head (a8db372).

❗ There is a different number of reports uploaded between BASE (1a48a87) and HEAD (a8db372). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (1a48a87) HEAD (a8db372)
unittests 1 0
Additional details and impacted files
@@              Coverage Diff              @@
##               main   #14205       +/-   ##
=============================================
- Coverage     19.91%    3.70%   -16.21%     
=============================================
  Files          6373      487     -5886     
  Lines        577230    42001   -535229     
  Branches      70696     7943    -62753     
=============================================
- Hits         114950     1558   -113392     
+ Misses       449713    40217   -409496     
+ Partials      12567      226    -12341     
Flag Coverage Δ
uitests 3.70% <ø> (-0.01%) ⬇️
unittests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.

@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Dear author, please fix the conflicts and sync your branch with the base branch.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Authentication state handling, domain cache isolation, and several OIDC interoperability issues must be corrected.

Review effort: Balanced
Findings: 3 High severity · 4 Medium severity · 3 Low severity

Open (10)
What changed in this PR

Adds reusable generic OIDC authentication, separating provider labels from implementation types.

Changes:

  • Adds OIDC discovery, token validation, and registration-type dispatch.
  • Extends schema and APIs with provider type and issuer URL.
  • Adds generic OIDC login UI and tests.
File Description
ui/​src/​views/​auth/​Login.vue Adds generic OIDC login buttons and discovery.
ui/​public/​locales/​en.json Adds OIDC login messages.
ui/​public/​assets/​oidc.svg Adds the OIDC logo.
plugins/​user-authenticators/​oauth2/​src/​test/​java/​org/​apache/​cloudstack/​oauth2/​oidc/​GenericOIDCOAuth2ProviderTest.java Tests OIDC validation and code caching.
plugins/​user-authenticators/​oauth2/​src/​test/​java/​org/​apache/​cloudstack/​oauth2/​OAuth2UserAuthenticatorTest.java Updates provider-aware authentication tests.
plugins/​user-authenticators/​oauth2/​src/​test/​java/​org/​apache/​cloudstack/​oauth2/​OAuth2AuthManagerImplTest.java Tests type-based provider dispatch.
plugins/​user-authenticators/​oauth2/​src/​main/​resources/​META-INF/​cloudstack/​oauth2/​spring-oauth2-context.xml Registers the generic OIDC bean.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​vo/​OauthProviderVO.java Maps type and issuer fields.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​oidc/​GenericOIDCOAuth2Provider.java Implements generic OIDC authentication.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​OAuth2UserAuthenticator.java Passes registration context during login.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​OAuth2AuthManagerImpl.java Resolves providers by registration type.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​OAuth2AuthManager.java Extends the manager contract.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​api/​response/​OauthProviderResponse.java Exposes type and issuer URL.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​api/​command/​UpdateOAuthProviderCmd.java Supports issuer URL updates.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​api/​command/​RegisterOAuthProviderCmd.java Supports generic OIDC registration.
plugins/​user-authenticators/​oauth2/​src/​main/​java/​org/​apache/​cloudstack/​oauth2/​api/​command/​ListOAuthProvidersCmd.java Reports type-backed providers correctly.
engine/​schema/​src/​main/​resources/​META-INF/​db/​schema-42300to2400.sql Adds type and issuer columns.
api/​src/​main/​java/​org/​apache/​cloudstack/​auth/​UserOAuth2Authenticator.java Adds registration-aware default methods.
api/​src/​main/​java/​org/​apache/​cloudstack/​api/​ApiConstants.java Adds the issuer URL API constant.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ui/src/views/auth/Login.vue
Long domainId = normalizeGlobalScope(resolveDomainIdFromIdOrPath(cmd.getDomainId(), cmd.getDomainPath()));
String authorizeUrl = StringUtils.trim(cmd.getAuthorizeUrl());
String tokenUrl = StringUtils.trim(cmd.getTokenUrl());
String type = StringUtils.trim(cmd.getType());
Comment thread ui/src/views/auth/Login.vue
Comment thread ui/src/views/auth/Login.vue
Comment thread ui/src/views/auth/Login.vue

@Damans227 Damans227 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can you rebase? there are conflicts with main

The OAuth2 plugin resolves a login to a UserOAuth2Authenticator through a fixed
provider-name to Spring-bean map, and each OIDC vendor is its own bean running the
same authorization-code flow with no vendor-specific logic. A new IdP needs a new
class, and since provider is both the display name and the routing key, a domain can
register only one keycloak.

This adds a type column to oauth_provider. OAuth2AuthManagerImpl looks the name up in
the bean map first and only on a miss falls back to the registration's type, so
provider becomes an admin-chosen label and type selects the implementation. One bean
then serves any number of registrations under arbitrary names. Existing google, github
and keycloak rows carry a null type and dispatch by name as before.

GenericOIDCOAuth2Provider is registered under type oidc and configured with the issuer
URL. It reads the token and JWKS endpoints from the issuer's discovery document (the
issuer must match), and validates the id_token before trusting it: signature against the
JWKS key named by the token kid, then issuer, audience and expiry, using the CXF JOSE
library already on the classpath. It holds no token state between logins, and a code
verified through verifyOAuthCodeAndGetUser is redeemed once so the following oauthlogin
does not re-present it.

ListOAuthProvidersCmd and UpdateOAuthProviderCmd derived the response enabled flag from a
name-to-bean check, which reported every generically-named registration as disabled; both
now also accept a registration whose type resolves to a plugin.

The schema change adds type and issuer_url to oauth_provider in the 4.23.0.0 to 24.0.0
upgrade file. Login.vue renders the OAuth buttons from the registered provider list so a
generic oidc provider gets a Sign in with <label> button and starts the flow from the
issuer's authorize endpoint, resolved from discovery.
@nagaboinaramgopal
nagaboinaramgopal force-pushed the feature/generic-oidc-provider branch from 2a72050 to f98eb11 Compare October 1, 2026 17:04
@nagaboinaramgopal

Copy link
Copy Markdown
Contributor Author

Thanks @Damans227, good catch. I've rebased it onto the latest main now, so the conflicts should be gone. Please let me know if you spot anything else.

- fall back to the id_token header algorithm when a published JWK omits alg
  (Entra ID does this), so verification no longer fails for those providers
- cache the JWKS per issuer like the discovery document, refetching once on
  an unknown key id for rotation, instead of fetching on every login
- reject registering an OIDC type under a name reserved by a built-in
  provider, where the name would shadow the type during dispatch
- log the provider's raw token-exchange error server side and return a
  generic message to the caller
@Damans227 Damans227 mentioned this pull request Oct 1, 2026
2 of 14 tasks
- require a verified email (email_verified) in the id_token before using it
  as the account identity, so an unverified address cannot claim a user
- key the single-use verified-email cache by domain as well, so a code
  verified for one domain cannot be consumed for another
- store the provider type lowercased so list, update and the UI agree with
  the case-insensitive dispatch lookup
- correct the since metadata on the new type/issuerUrl parameters and
  response fields from 4.24.0 to 24.0.0
@DaanHoogland

Copy link
Copy Markdown
Contributor

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@DaanHoogland a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✖️ el8 ✖️ el9 ✖️ debian ✖️ suse15. SL-JID 19384

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate Cloudstack SSO with Generic OpenID Connect on-premise solution

5 participants