Skip to content

Add keycloak OAuth provider#13033

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

Add keycloak OAuth provider#13033
tazouxme wants to merge 6 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.54422% with 58 lines in your changes missing coverage. Please review.
✅ Project coverage is 17.96%. Comparing base (9f57a4d) to head (a0270e1).
⚠️ Report is 46 commits behind head on main.

Files with missing lines Patch % Lines
...dstack/oauth2/keycloak/KeycloakOAuth2Provider.java 80.26% 11 Missing and 4 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/github/GithubOAuth2Provider.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.01%   17.96%   -0.05%     
- Complexity    16474    16540      +66     
============================================
  Files          5976     6023      +47     
  Lines        537858   541527    +3669     
  Branches      66049    66357     +308     
============================================
+ Hits          96882    97290     +408     
- Misses       430052   433264    +3212     
- Partials      10924    10973      +49     
Flag Coverage Δ
uitests 3.52% <ø> (-0.01%) ⬇️
unittests 19.12% <60.54%> (-0.06%) ⬇️

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 +117 to +119
if (StringUtils.equals("keycloak", getProvider())) {
if (getAuthorizeUrl() == null || "".equals(getAuthorizeUrl())) {
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter authorizationurl is mandatory for keycloak OAuth Provider");
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 Keycloak-only parameter validation uses org.apache.commons.codec.binary.StringUtils for string comparison and then manual null/empty checks. This is error-prone and inconsistent with the rest of this module (which uses org.apache.commons.lang3.StringUtils); please switch to lang3 and use isBlank/equalsIgnoreCase as appropriate.

Copilot uses AI. Check for mistakes.
public void execute() throws ServerApiException, ConcurrentOperationException, EntityExistsException {
if (StringUtils.equals("keycloak", getProvider())) {
if (getAuthorizeUrl() == null || "".equals(getAuthorizeUrl())) {
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter authorizationurl is mandatory for keycloak OAuth Provider");
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 validation error message references "authorizationurl", but the API parameter added is authorizeurl (ApiConstants.AUTHORIZE_URL). Please align the message with the actual parameter name.

Suggested change
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter authorizationurl is mandatory for keycloak OAuth Provider");
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter authorizeurl is mandatory for keycloak OAuth Provider");

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +55
protected String idToken = null;

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.

idToken is stored as an instance field. Since provider beans are typically singletons, this shared mutable state can leak between concurrent login attempts. Please avoid storing per-request tokens on the provider instance; keep them local to the method or in a request/session-scoped context.

Copilot uses AI. Check for mistakes.
try {
post.setEntity(new UrlEncodedFormEntity(params));
} catch (UnsupportedEncodingException e) {
throw new CloudRuntimeException("Unable to generating URL parameters: " + e.getMessage());
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.

Minor grammar issue in exception message: "Unable to generating URL parameters" should be "Unable to generate URL parameters".

Suggested change
throw new CloudRuntimeException("Unable to generating URL parameters: " + e.getMessage());
throw new CloudRuntimeException("Unable to generate URL parameters: " + e.getMessage());

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +133
boolean result = provider.verifyUser("user@example.com", secretCode);

assertTrue("L'utilisateur devrait être vérifié avec succès", result);
}
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.

This test is annotated with @Test(expected = CloudRuntimeException.class) but then asserts success (assertTrue(...)). The assertion is unreachable when the exception is thrown and the message is misleading; please rewrite the test to explicitly assert the exception (and keep messages consistent with the expected outcome).

Copilot uses AI. Check for mistakes.
Comment on lines 88 to 92
OauthProviderVO githubProvider = _oauthProviderDao.findByProvider(getName());
String tokenUrl = "https://github.com/login/oauth/access_token";
String generatedAccessToken = null;
try {
URL url = new URL(tokenUrl);
URL url = new URL(githubProvider.getTokenUrl());
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
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.

githubProvider.getTokenUrl() can be null/blank for existing deployments (the new DB columns default to NULL and older registered GitHub providers won't have a token URL). new URL(null) will throw and break GitHub OAuth after upgrade. Consider falling back to the standard GitHub token URL when tokenUrl is blank and/or backfilling/enforcing it at registration/migration time.

Copilot uses AI. Check for mistakes.
@Parameter(name = ApiConstants.REDIRECT_URI, type = CommandType.STRING, description = "Redirect URI pre-registered in the specific OAuth provider", required = true)
private String redirectUri;

@Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for keyloack provider)")
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.

Typo in parameter description: "keyloack" should be "keycloak".

Suggested change
@Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for keyloack provider)")
@Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL for OAuth initialization (only required for keycloak provider)")

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +105
public String verifyCodeAndFetchEmail(String secretCode) {
OauthProviderVO provider = oauthProviderDao.findByProvider(getName());

if (StringUtils.isBlank(idToken)) {
String auth = provider.getClientId() + ":" + provider.getSecretKey();
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
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.

verifyCodeAndFetchEmail dereferences provider without checking for null. This method can be called directly via OAuth2AuthManagerImpl.verifyCodeAndFetchEmail(...), so an unregistered Keycloak provider would trigger a NullPointerException. Please add an explicit null check and throw a clear authentication/validation exception.

Copilot uses AI. Check for mistakes.
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.
:href="getKeycloakUrl(from)"
class="auth-btn keycloak-auth"
style="height: 38px; width: 185px; padding: 0" >
<img src="/assets/keycloak.svg" style="width: 32px; padding: 5px" />
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 new Keycloak login button adds an without an alt attribute. This is an accessibility issue for screen readers; please add a meaningful alt text (or mark it as decorative with empty alt if appropriate).

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

Copilot uses AI. Check for mistakes.
@blueorangutan
Copy link
Copy Markdown

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

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.

4 participants