Skip to content

Add keycloak OAuth provider#13033

Open
tazouxme wants to merge 11 commits intoapache:mainfrom
tazouxme:feature/add_keycloak_oauth_provider
Open

Add keycloak OAuth provider#13033
tazouxme wants to merge 11 commits intoapache:mainfrom
tazouxme:feature/add_keycloak_oauth_provider

Conversation

@tazouxme
Copy link
Copy Markdown

Description

Add Keycloak as a new OAuth provider. This PR adds two new fields in the UI and in the DB:

  • Authorization URL
  • Token URL

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):

details login new_provider

How Has This Been Tested?

Manual testing using local Keycloak instance with basic Realm

@boring-cyborg
Copy link
Copy Markdown

boring-cyborg Bot commented Apr 15, 2026

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:

@sureshanaparti
Copy link
Copy Markdown
Contributor

@blueorangutan package

@blueorangutan
Copy link
Copy Markdown

@sureshanaparti 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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 60.13514% with 59 lines in your changes missing coverage. Please review.
✅ Project coverage is 18.05%. Comparing base (a73cc9a) to head (9df0c38).

Files with missing lines Patch % Lines
...dstack/oauth2/keycloak/KeycloakOAuth2Provider.java 78.20% 12 Missing and 5 partials ⚠️
...ack/oauth2/api/response/OauthProviderResponse.java 25.00% 12 Missing ⚠️
...k/oauth2/api/command/RegisterOAuthProviderCmd.java 15.38% 10 Missing and 1 partial ⚠️
...ack/oauth2/api/command/UpdateOAuthProviderCmd.java 0.00% 8 Missing ⚠️
...pache/cloudstack/oauth2/OAuth2AuthManagerImpl.java 60.00% 4 Missing and 2 partials ⚠️
...g/apache/cloudstack/oauth2/vo/OauthProviderVO.java 75.00% 3 Missing ⚠️
...tack/oauth2/api/command/ListOAuthProvidersCmd.java 0.00% 1 Missing ⚠️
...cloudstack/oauth2/google/GoogleOAuth2Provider.java 80.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #13033      +/-   ##
============================================
+ Coverage     18.03%   18.05%   +0.01%     
- Complexity    16648    16667      +19     
============================================
  Files          6035     6036       +1     
  Lines        542487   542630     +143     
  Branches      66477    66489      +12     
============================================
+ Hits          97854    97959     +105     
- Misses       433612   433642      +30     
- Partials      11021    11029       +8     
Flag Coverage Δ
uitests 3.52% <ø> (-0.01%) ⬇️
unittests 19.21% <60.13%> (+0.01%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Keycloak as an additional OAuth2/OIDC provider by extending the OAuth provider model/API/schema and exposing new provider URL fields in the UI, along with a new Keycloak authenticator implementation and tests.

Changes:

  • Add authorizeurl and tokenurl fields end-to-end (DB schema, VO/response objects, API commands, and UI config/i18n).
  • Register a new KeycloakOAuth2Provider and include it in the default provider order.
  • Update GitHub token exchange to use a configurable token URL instead of a hardcoded endpoint.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
ui/src/views/auth/Login.vue Adds Keycloak social login button and consumes authorizeurl from listOauthProvider.
ui/src/config/section/config.js Extends OAuth provider UI config to edit/display authorizeurl/tokenurl and adds keycloak to provider options.
ui/public/locales/en.json Adds UI labels for Authorize URL and Token URL.
ui/public/assets/keycloak.svg Adds Keycloak icon asset for the login UI.
plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java Introduces unit tests for the new Keycloak provider.
plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml Registers Keycloak provider bean and adds it to default ordering.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java Adds authorizeUrl and tokenUrl columns/fields to the OAuth provider entity.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java Implements Keycloak OAuth2/OIDC flow using token endpoint + ID token parsing.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/google/GoogleOAuth2Provider.java Minor refactor/cleanup and fixes a format string.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java Switches GitHub token endpoint to DB-configured tokenUrl.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java Extends API response to include authorizeurl and tokenurl.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java Adds new parameters and returns them in response.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java Adds new parameters and validates Keycloak requires the URLs.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java Includes new URL fields in list responses.
plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java Plumbs new URL fields through register/update persistence.
plugins/user-authenticators/oauth2/pom.xml Adds CXF JOSE dependency used for JWT parsing.
engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql Adds authorize_url and token_url columns to cloud.oauth_provider.
api/src/main/java/org/apache/cloudstack/api/ApiConstants.java Adds API constants for authorizeurl and tokenurl.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +146 to +163
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();

if (!claims.getAudiences().contains(provider.getClientId())) {
throw new CloudAuthenticationException("Audience mismatch");
}

return (String) claims.getClaim("email");
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The ID token is parsed with JwsJwtCompactConsumer but its signature is never verified (and the tests even use {"alg":"none"} tokens). This allows token forgery. Please perform proper OIDC validation: verify JWS signature against the provider's JWKS, and validate standard claims (exp/iat, iss, aud, nonce) before trusting the email claim.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I agree with this comment. The fact is that the token is never checked in Google / Github. Should we introduce it ?

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.

@tazouxme , it makes sense but some conflicting considerations:

  • How much effort will this be?
  • How likely is an exploit and what is the potential damage?

My gut says this is a security feature we might want and not a direct vulnerability (as people can decide not to use a certain provider). Note that my gut has been known to be wrong.

Comment thread ui/src/views/auth/Login.vue Outdated
@blueorangutan
Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 17513

@DaanHoogland
Copy link
Copy Markdown
Contributor

thanks for the contribution @tazouxme , looks useful. Please have a look at co-pilot’s comments and the pre-commit and license check failures.

@kiranchavala
Copy link
Copy Markdown
Member

Looks very useful feature. Thanks for Contributing @tazouxme

Will test it out, I had previously tested saml authentication with keycloak

https://kiranchavala.in/blog/cloudstack-integration-with-keycloak/

@tazouxme
Copy link
Copy Markdown
Author

Thanks for your positive feedback. There's only a valid point from Copilot regarding the Token validation. Signature should be validated but currently not done for Google / Github. Should this be introduced ?

@DaanHoogland
Copy link
Copy Markdown
Contributor

Thanks for your positive feedback. There's only a valid point from Copilot regarding the Token validation. Signature should be validated but currently not done for Google / Github. Should this be introduced ?

If you feel like implementing it, your efforts are appreciated. I think it can be a separate PR.

@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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +52 to +57
public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator {

protected String idToken = null;

@Inject
OauthProviderDao oauthProviderDao;
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

idToken is stored as an instance field on the provider bean. Since these authenticators are created as singleton Spring beans by default, this cached token can be shared across concurrent login requests and leak/mix authentication state between users. Avoid storing per-request auth state in fields; keep the token in local variables and pass it through methods instead.

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +134
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
String idToken = json.get("id_token").getAsString();
validateIdToken(idToken, provider);

Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

The token endpoint response is assumed to always contain an id_token. If Keycloak is configured to not return an ID token (or an error response body is returned with HTTP 200), json.get("id_token") will be null and this will throw a NullPointerException. Please validate the JSON response (presence/type of id_token) and raise a clear authentication error when it’s missing.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +135
String idToken = json.get("id_token").getAsString();
validateIdToken(idToken, provider);

this.idToken = idToken;
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

The local variable String idToken = ... shadows the idToken field, which is easy to misread and can lead to subtle bugs when modifying this code. Use a different local name (e.g., fetchedIdToken) or eliminate the field entirely (see concurrency concern).

Suggested change
String idToken = json.get("id_token").getAsString();
validateIdToken(idToken, provider);
this.idToken = idToken;
String fetchedIdToken = json.get("id_token").getAsString();
validateIdToken(fetchedIdToken, provider);
this.idToken = fetchedIdToken;

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +166
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();

if (!claims.getAudiences().contains(provider.getClientId())) {
throw new CloudAuthenticationException("Audience mismatch");
}

return (String) claims.getClaim("email");
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

validateIdToken/obtainEmail parse the JWT but never verify its JWS signature or validate standard OIDC claims (e.g., exp, iss, nonce). This means a forged/unsigned token could be accepted as long as it contains the expected aud/email fields. Please implement proper ID token verification (signature validation via JWKS and required claim checks) before trusting the email claim.

Copilot uses AI. Check for mistakes.

@Override
public String getUserEmailAddress() throws CloudRuntimeException {
return null;
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

getUserEmailAddress() is currently unimplemented and always returns null. Since it is part of the UserOAuth2Authenticator contract, either implement it (e.g., derive the email from the cached token) or throw a clear exception indicating it’s unsupported to avoid confusing null propagation.

Suggested change
return null;
if (StringUtils.isBlank(idToken)) {
throw new CloudRuntimeException("User email address is unavailable because no ID token is cached");
}
OauthProviderVO provider = oauthProviderDao.findByProvider(getName());
if (provider == null) {
throw new CloudAuthenticationException("Keycloak provider is not registered, so user email address cannot be retrieved");
}
return obtainEmail(idToken, provider);

Copilot uses AI. Check for mistakes.
class="auth-btn github-auth"
style="height: 38px; width: 185px; padding: 0; margin-bottom: 5px;" >
<img src="/assets/github.svg" style="width: 32px; padding: 5px" />
<img src="/assets/github.svg" alt="Google" style="width: 32px; padding: 5px" />
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

The GitHub button icon has an incorrect alt text (currently "Google"), which harms accessibility/screen readers. Update the alt text to describe the GitHub icon.

Suggested change
<img src="/assets/github.svg" alt="Google" style="width: 32px; padding: 5px" />
<img src="/assets/github.svg" alt="GitHub" style="width: 32px; padding: 5px" />

Copilot uses AI. Check for mistakes.
Comment on lines +201 to 202
<img src="/assets/google.svg" alt="Github" style="width: 32px; padding: 5px" />
<a-typography-text>Sign in with Google</a-typography-text>
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

The Google button icon has an incorrect alt text (currently "Github"), which harms accessibility/screen readers. Update the alt text to describe the Google icon.

Copilot uses AI. Check for mistakes.
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.

6 participants