diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 05c6098bc726..8433f45947fd 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -1322,8 +1322,10 @@ public class ApiConstants {
public static final String VNF_CONFIGURE_MANAGEMENT = "vnfconfiguremanagement";
public static final String VNF_CIDR_LIST = "vnfcidrlist";
+ public static final String AUTHORIZE_URL = "authorizeurl";
public static final String CLIENT_ID = "clientid";
public static final String REDIRECT_URI = "redirecturi";
+ public static final String TOKEN_URL = "tokenurl";
public static final String IS_TAG_A_RULE = "istagarule";
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
index 4cb9eb7cb2c4..02f2aaabd10e 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
@@ -117,3 +117,7 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vpc_offerings','conserve_mode', 'tin
--- Disable/enable NICs
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.nics','enabled', 'TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Indicates whether the NIC is enabled or not'' ');
+
+--- Add URLs for OAuth provider
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider','authorize_url', 'VARCHAR(255) DEFAULT NULL COMMENT ''Authorize URL for OAuth initialization'' ');
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider','token_url', 'VARCHAR(255) DEFAULT NULL COMMENT ''Token URL for OAuth finalization'' ');
diff --git a/plugins/user-authenticators/oauth2/pom.xml b/plugins/user-authenticators/oauth2/pom.xml
index 6ab7b9f5faba..89694440591c 100644
--- a/plugins/user-authenticators/oauth2/pom.xml
+++ b/plugins/user-authenticators/oauth2/pom.xml
@@ -38,6 +38,11 @@
cloud-framework-config
${project.version}
+
+ org.apache.cxf
+ cxf-rt-rs-security-jose
+ ${cs.cxf.version}
+
com.google.apis
google-api-services-docs
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java
index b65027d6a249..b1bb8292f24a 100644
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/OAuth2AuthManagerImpl.java
@@ -18,10 +18,14 @@
//
package org.apache.cloudstack.oauth2;
-import com.cloud.user.dao.UserDao;
-import com.cloud.utils.component.Manager;
-import com.cloud.utils.component.ManagerBase;
-import com.cloud.utils.exception.CloudRuntimeException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
import org.apache.cloudstack.auth.UserOAuth2Authenticator;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
@@ -35,16 +39,11 @@
import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
import org.apache.commons.lang3.StringUtils;
-import javax.inject.Inject;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import com.cloud.utils.component.Manager;
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.exception.CloudRuntimeException;
public class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthManager, Manager, Configurable {
- @Inject
- private UserDao _userDao;
@Inject
protected OauthProviderDao _oauthProviderDao;
@@ -55,7 +54,7 @@ public class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
@Override
public List> getAuthCommands() {
- List> cmdList = new ArrayList>();
+ List> cmdList = new ArrayList<>();
cmdList.add(OauthLoginAPIAuthenticatorCmd.class);
cmdList.add(ListOAuthProvidersCmd.class);
cmdList.add(VerifyOAuthCodeAndGetUserCmd.class);
@@ -84,7 +83,7 @@ public boolean stop() {
@Override
public List> getCommands() {
- List> cmdList = new ArrayList>();
+ List> cmdList = new ArrayList<>();
cmdList.add(RegisterOAuthProviderCmd.class);
cmdList.add(DeleteOAuthProviderCmd.class);
cmdList.add(UpdateOAuthProviderCmd.class);
@@ -127,9 +126,7 @@ protected void initializeUserOAuth2AuthenticationProvidersMap() {
@Override
public String verifyCodeAndFetchEmail(String code, String provider) {
UserOAuth2Authenticator authenticator = getUserOAuth2AuthenticationProvider(provider);
- String email = authenticator.verifyCodeAndFetchEmail(code);
-
- return email;
+ return authenticator.verifyCodeAndFetchEmail(code);
}
@Override
@@ -139,6 +136,8 @@ public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) {
String clientId = StringUtils.trim(cmd.getClientId());
String redirectUri = StringUtils.trim(cmd.getRedirectUri());
String secretKey = StringUtils.trim(cmd.getSecretKey());
+ String authorizeUrl = StringUtils.trim(cmd.getAuthorizeUrl());
+ String tokenUrl = StringUtils.trim(cmd.getTokenUrl());
if (!isOAuthPluginEnabled()) {
throw new CloudRuntimeException("OAuth is not enabled, please enable to register");
@@ -148,7 +147,7 @@ public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) {
throw new CloudRuntimeException(String.format("Provider with the name %s is already registered", provider));
}
- return saveOauthProvider(provider, description, clientId, secretKey, redirectUri);
+ return saveOauthProvider(provider, description, clientId, secretKey, redirectUri, authorizeUrl, tokenUrl);
}
@Override
@@ -171,6 +170,8 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) {
String clientId = StringUtils.trim(cmd.getClientId());
String redirectUri = StringUtils.trim(cmd.getRedirectUri());
String secretKey = StringUtils.trim(cmd.getSecretKey());
+ String authorizeUrl = StringUtils.trim(cmd.getAuthorizeUrl());
+ String tokenUrl = StringUtils.trim(cmd.getTokenUrl());
Boolean enabled = cmd.getEnabled();
OauthProviderVO providerVO = _oauthProviderDao.findById(id);
@@ -190,6 +191,12 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) {
if (StringUtils.isNotEmpty(secretKey)) {
providerVO.setSecretKey(secretKey);
}
+ if (StringUtils.isNotEmpty(authorizeUrl)) {
+ providerVO.setAuthorizeUrl(authorizeUrl);
+ }
+ if (StringUtils.isNotEmpty(tokenUrl)) {
+ providerVO.setTokenUrl(tokenUrl);
+ }
if (enabled != null) {
providerVO.setEnabled(enabled);
}
@@ -199,7 +206,7 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) {
return _oauthProviderDao.findById(id);
}
- private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String secretKey, String redirectUri) {
+ private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl, String tokenUrl) {
final OauthProviderVO oauthProviderVO = new OauthProviderVO();
oauthProviderVO.setProvider(provider);
@@ -207,6 +214,8 @@ private OauthProviderVO saveOauthProvider(String provider, String description, S
oauthProviderVO.setClientId(clientId);
oauthProviderVO.setSecretKey(secretKey);
oauthProviderVO.setRedirectUri(redirectUri);
+ oauthProviderVO.setAuthorizeUrl(authorizeUrl);
+ oauthProviderVO.setTokenUrl(tokenUrl);
oauthProviderVO.setEnabled(true);
_oauthProviderDao.persist(oauthProviderVO);
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java
index abdbf65dbb42..9b91a1d879c2 100644
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/ListOAuthProvidersCmd.java
@@ -21,8 +21,10 @@
import java.util.List;
import java.util.Map;
-import com.cloud.api.response.ApiResponseSerializer;
-import com.cloud.user.Account;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
@@ -40,9 +42,8 @@
import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
import org.apache.commons.lang.ArrayUtils;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
+import com.cloud.api.response.ApiResponseSerializer;
+import com.cloud.user.Account;
@APICommand(name = "listOauthProvider", description = "List OAuth providers registered", responseObject = OauthProviderResponse.class, entityType = {},
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
@@ -108,7 +109,7 @@ public String authenticate(String command, Map params, HttpSes
List responses = new ArrayList<>();
for (OauthProviderVO result : resultList) {
OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(),
- result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri());
+ result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(), result.getAuthorizeUrl(), result.getTokenUrl());
if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
r.setEnabled(true);
} else {
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java
index b31cbde97c52..96fce48b7a92 100644
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/RegisterOAuthProviderCmd.java
@@ -14,26 +14,28 @@
// limitations under the License.
package org.apache.cloudstack.oauth2.api.command;
+import java.util.Collection;
+import java.util.Map;
+
import javax.inject.Inject;
import javax.persistence.EntityExistsException;
-import org.apache.cloudstack.api.response.SuccessResponse;
-import org.apache.cloudstack.oauth2.OAuth2AuthManager;
-import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
-import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
-import org.apache.commons.collections.MapUtils;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.oauth2.OAuth2AuthManager;
+import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
+import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import org.apache.commons.codec.binary.StringUtils;
+import org.apache.commons.collections.MapUtils;
import com.cloud.exception.ConcurrentOperationException;
-import java.util.Collection;
-import java.util.Map;
-
@APICommand(name = "registerOauthProvider", responseObject = SuccessResponse.class, description = "Register the OAuth2 provider in CloudStack", since = "4.19.0")
public class RegisterOAuthProviderCmd extends BaseCmd {
@@ -56,6 +58,12 @@ public class RegisterOAuthProviderCmd extends BaseCmd {
@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)")
+ private String authorizeUrl;
+
+ @Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL for OAuth finalization (only required for keycloak provider)")
+ private String tokenUrl;
+
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
description = "Any OAuth provider details in key/value pairs using format details[i].keyname=keyvalue. Example: details[0].clientsecret=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")
protected Map details;
@@ -85,6 +93,14 @@ public String getRedirectUri() {
return redirectUri;
}
+ public String getAuthorizeUrl() {
+ return authorizeUrl;
+ }
+
+ public String getTokenUrl() {
+ return tokenUrl;
+ }
+
public Map getDetails() {
if (MapUtils.isEmpty(details)) {
return null;
@@ -98,10 +114,20 @@ public Map getDetails() {
@Override
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");
+ }
+ if (getTokenUrl() == null || "".equals(getTokenUrl())) {
+ throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter tokenurl is mandatory for keycloak OAuth Provider");
+ }
+ }
+
OauthProviderVO provider = _oauth2mgr.registerOauthProvider(this);
OauthProviderResponse response = new OauthProviderResponse(provider.getUuid(), provider.getProvider(),
- provider.getDescription(), provider.getClientId(), provider.getSecretKey(), provider.getRedirectUri());
+ provider.getDescription(), provider.getClientId(), provider.getSecretKey(), provider.getRedirectUri(),
+ provider.getAuthorizeUrl(), provider.getTokenUrl());
response.setResponseName(getCommandName());
response.setObjectName(ApiConstants.OAUTH_PROVIDER);
setResponseObject(response);
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java
index 1c79b7b144c8..a8b0604a9bba 100644
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/command/UpdateOAuthProviderCmd.java
@@ -16,23 +16,23 @@
// under the License.
package org.apache.cloudstack.oauth2.api.command;
-import org.apache.cloudstack.api.ApiCommandResourceType;
-import org.apache.cloudstack.auth.UserOAuth2Authenticator;
-import org.apache.cloudstack.oauth2.OAuth2AuthManager;
-import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
-import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.auth.UserOAuth2Authenticator;
import org.apache.cloudstack.context.CallContext;
-
-import javax.inject.Inject;
-import java.util.ArrayList;
-import java.util.List;
+import org.apache.cloudstack.oauth2.OAuth2AuthManager;
+import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
+import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
@APICommand(name = "updateOauthProvider", description = "Updates the registered OAuth provider details", responseObject = OauthProviderResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.19.0")
@@ -57,6 +57,12 @@ public final class UpdateOAuthProviderCmd extends BaseCmd {
@Parameter(name = ApiConstants.REDIRECT_URI, type = CommandType.STRING, description = "Redirect URI pre-registered in the specific OAuth provider")
private String redirectUri;
+ @Parameter(name = ApiConstants.AUTHORIZE_URL, type = CommandType.STRING, description = "Authorize URL pre-registered in the specific OAuth provider")
+ private String authorizeUrl;
+
+ @Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL pre-registered in the specific OAuth provider")
+ private String tokenUrl;
+
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "OAuth provider will be enabled or disabled based on this value")
private Boolean enabled;
@@ -87,6 +93,14 @@ public String getRedirectUri() {
return redirectUri;
}
+ public String getAuthorizeUrl() {
+ return authorizeUrl;
+ }
+
+ public String getTokenUrl() {
+ return tokenUrl;
+ }
+
public Boolean getEnabled() {
return enabled;
}
@@ -115,7 +129,8 @@ public void execute() {
OauthProviderVO result = _oauthMgr.updateOauthProvider(this);
if (result != null) {
OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(),
- result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri());
+ result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(),
+ result.getAuthorizeUrl(), result.getTokenUrl());
List userOAuth2AuthenticatorPlugins = _oauthMgr.listUserOAuth2AuthenticationProviders();
List authenticatorPluginNames = new ArrayList<>();
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java
index e0c40bef9b4d..289dc6650137 100644
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/api/response/OauthProviderResponse.java
@@ -16,13 +16,14 @@
// under the License.
package org.apache.cloudstack.oauth2.api.response;
-import com.cloud.serializer.Param;
-import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
@EntityReference(value = OauthProviderVO.class)
public class OauthProviderResponse extends BaseResponse {
@@ -54,18 +55,28 @@ public class OauthProviderResponse extends BaseResponse {
@Param(description = "Redirect URI registered in the OAuth provider")
private String redirectUri;
+ @SerializedName(ApiConstants.AUTHORIZE_URL)
+ @Param(description = "Authorize URL registered in the OAuth provider")
+ private String authorizeUrl;
+
+ @SerializedName(ApiConstants.TOKEN_URL)
+ @Param(description = "Token URL registered in the OAuth provider")
+ private String tokenUrl;
+
@SerializedName(ApiConstants.ENABLED)
@Param(description = "Whether the OAuth provider is enabled or not")
private boolean enabled;
- public OauthProviderResponse(String id, String provider, String description, String clientId, String secretKey, String redirectUri) {
+ public OauthProviderResponse(String id, String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl, String tokenUrl) {
this.id = id;
this.provider = provider;
this.name = provider;
this.description = description;
this.clientId = clientId;
this.secretKey = secretKey;
- this.redirectUri = redirectUri;
+ this.redirectUri = redirectUri;
+ this.authorizeUrl = authorizeUrl;
+ this.tokenUrl = tokenUrl;
}
public String getId() {
@@ -117,6 +128,22 @@ public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
+ public String getAuthorizeUrl() {
+ return authorizeUrl;
+ }
+
+ public void setAuthorizeUrl(String authorizeUrl) {
+ this.authorizeUrl = authorizeUrl;
+ }
+
+ public String getTokenUrl() {
+ return tokenUrl;
+ }
+
+ public void setTokenUrl(String tokenUrl) {
+ this.tokenUrl = tokenUrl;
+ }
+
public String getSecretKey() {
return secretKey;
}
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java
index e4a7fae101f0..8722a0b219ce 100644
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/github/GithubOAuth2Provider.java
@@ -16,17 +16,6 @@
//under the License.
package org.apache.cloudstack.oauth2.github;
-import com.cloud.utils.component.AdapterBase;
-import com.cloud.utils.exception.CloudRuntimeException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.cloudstack.auth.UserOAuth2Authenticator;
-import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
-import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
-import org.apache.commons.lang3.StringUtils;
-
-import javax.inject.Inject;
-
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -36,6 +25,18 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import javax.inject.Inject;
+
+import org.apache.cloudstack.auth.UserOAuth2Authenticator;
+import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
+import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import org.apache.commons.lang3.StringUtils;
+
+import com.cloud.utils.component.AdapterBase;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
public class GithubOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator {
@Inject
@@ -85,10 +86,9 @@ public String verifyCodeAndFetchEmail(String secretCode) {
protected String getAccessToken(String secretCode) throws CloudRuntimeException {
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();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
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..885930181c91 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
@@ -16,6 +16,17 @@
//under the License.
package org.apache.cloudstack.oauth2.google;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.apache.cloudstack.auth.UserOAuth2Authenticator;
+import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
+import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import org.apache.commons.lang3.StringUtils;
+
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.exception.CloudRuntimeException;
@@ -28,15 +39,6 @@
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.model.Userinfo;
-import org.apache.cloudstack.auth.UserOAuth2Authenticator;
-import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
-import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
-import org.apache.commons.lang3.StringUtils;
-
-import javax.inject.Inject;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
public class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator {
@@ -78,10 +80,10 @@ public boolean verifyUser(String email, String secretCode) {
@Override
public String verifyCodeAndFetchEmail(String secretCode) {
- OauthProviderVO githubProvider = _oauthProviderDao.findByProvider(getName());
- String clientId = githubProvider.getClientId();
- String secret = githubProvider.getSecretKey();
- String redirectURI = githubProvider.getRedirectUri();
+ OauthProviderVO googleProvider = _oauthProviderDao.findByProvider(getName());
+ String clientId = googleProvider.getClientId();
+ String secret = googleProvider.getSecretKey();
+ String redirectURI = googleProvider.getRedirectUri();
GoogleClientSecrets clientSecrets = new GoogleClientSecrets()
.setWeb(new GoogleClientSecrets.Details()
.setClientId(clientId)
@@ -122,7 +124,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()));
}
return userinfo.getEmail();
}
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java
new file mode 100644
index 000000000000..0d028cfc4a1b
--- /dev/null
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2Provider.java
@@ -0,0 +1,174 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+package org.apache.cloudstack.oauth2.keycloak;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.ws.rs.core.HttpHeaders;
+
+import org.apache.cloudstack.auth.UserOAuth2Authenticator;
+import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
+import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.cxf.rs.security.jose.jws.JwsJwtCompactConsumer;
+import org.apache.cxf.rs.security.jose.jwt.JwtClaims;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+
+import com.cloud.exception.CloudAuthenticationException;
+import com.cloud.utils.component.AdapterBase;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+public class KeycloakOAuth2Provider extends AdapterBase implements UserOAuth2Authenticator {
+
+ protected String idToken = null;
+
+ @Inject
+ OauthProviderDao oauthProviderDao;
+
+ private CloseableHttpClient httpClient;
+
+ public KeycloakOAuth2Provider() {
+ this(HttpClientBuilder.create().build());
+ }
+
+ public KeycloakOAuth2Provider(CloseableHttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public String getName() {
+ return "keycloak";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Keycloak OAuth2 Provider Plugin";
+ }
+
+ @Override
+ public boolean verifyUser(String email, String secretCode) {
+ if (StringUtils.isAnyEmpty(email, secretCode)) {
+ throw new CloudAuthenticationException("Either email or secret code should not be null/empty");
+ }
+
+ OauthProviderVO providerVO = oauthProviderDao.findByProvider(getName());
+ if (providerVO == null) {
+ throw new CloudAuthenticationException("Keycloak provider is not registered, so user cannot be verified");
+ }
+
+ String verifiedEmail = verifyCodeAndFetchEmail(secretCode);
+ if (StringUtils.isBlank(verifiedEmail) || !email.equals(verifiedEmail)) {
+ throw new CloudRuntimeException("Unable to verify the email address with the provided secret");
+ }
+ clearIdToken();
+
+ return true;
+ }
+
+ @Override
+ 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));
+
+ List params = new ArrayList<>();
+ params.add(new BasicNameValuePair("grant_type", "authorization_code"));
+ params.add(new BasicNameValuePair("code", secretCode));
+ params.add(new BasicNameValuePair("redirect_uri", provider.getRedirectUri()));
+
+ HttpPost post = new HttpPost(provider.getTokenUrl());
+ post.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodedAuth);
+
+ try {
+ post.setEntity(new UrlEncodedFormEntity(params));
+ } catch (UnsupportedEncodingException e) {
+ throw new CloudRuntimeException("Unable to generating URL parameters: " + e.getMessage());
+ }
+
+ try (CloseableHttpResponse response = httpClient.execute(post)) {
+ String body = EntityUtils.toString(response.getEntity());
+
+ if (response.getStatusLine().getStatusCode() != 200) {
+ throw new CloudRuntimeException("Keycloak error during token generation: " + body);
+ }
+
+ JsonObject json = JsonParser.parseString(body).getAsJsonObject();
+ String idToken = json.get("id_token").getAsString();
+ validateIdToken(idToken, provider);
+
+ this.idToken = idToken;
+ } catch (IOException e) {
+ throw new CloudRuntimeException("Unable to connect to Keycloak server", e);
+ }
+ }
+
+ return obtainEmail(idToken, provider);
+ }
+
+ @Override
+ public String getUserEmailAddress() throws CloudRuntimeException {
+ return null;
+ }
+
+ 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");
+ }
+
+ protected void clearIdToken() {
+ idToken = null;
+ }
+
+ public void setHttpClient(CloseableHttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+}
diff --git a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java
index efd6004e8f97..54d667bc9143 100644
--- a/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java
+++ b/plugins/user-authenticators/oauth2/src/main/java/org/apache/cloudstack/oauth2/vo/OauthProviderVO.java
@@ -16,9 +16,8 @@
// under the License.
package org.apache.cloudstack.oauth2.vo;
-import com.cloud.utils.db.GenericDao;
-import org.apache.cloudstack.api.Identity;
-import org.apache.cloudstack.api.InternalIdentity;
+import java.util.Date;
+import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
@@ -26,8 +25,11 @@
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
-import java.util.Date;
-import java.util.UUID;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+import com.cloud.utils.db.GenericDao;
@Entity
@Table(name = "oauth_provider")
@@ -55,6 +57,12 @@ public class OauthProviderVO implements Identity, InternalIdentity {
@Column(name = "redirect_uri")
private String redirectUri;
+ @Column(name = "authorize_url")
+ private String authorizeUrl;
+
+ @Column(name = "token_url")
+ private String tokenUrl;
+
@Column(name = GenericDao.CREATED_COLUMN)
private Date created;
@@ -110,6 +118,22 @@ public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
+ public String getAuthorizeUrl() {
+ return authorizeUrl;
+ }
+
+ public void setAuthorizeUrl(String authorizeUrl) {
+ this.authorizeUrl = authorizeUrl;
+ }
+
+ public String getTokenUrl() {
+ return tokenUrl;
+ }
+
+ public void setTokenUrl(String tokenUrl) {
+ this.tokenUrl = tokenUrl;
+ }
+
public String getSecretKey() {
return secretKey;
}
diff --git a/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml b/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml
index 04a6c8dabfe7..06fe60f4c25e 100644
--- a/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml
+++ b/plugins/user-authenticators/oauth2/src/main/resources/META-INF/cloudstack/oauth2/spring-oauth2-context.xml
@@ -35,6 +35,9 @@
+
+
+
@@ -45,7 +48,7 @@
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
-
+
diff --git a/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java
new file mode 100644
index 000000000000..aa85f52d9250
--- /dev/null
+++ b/plugins/user-authenticators/oauth2/src/test/java/org/apache/cloudstack/oauth2/keycloak/KeycloakOAuth2ProviderTest.java
@@ -0,0 +1,213 @@
+package org.apache.cloudstack.oauth2.keycloak;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.apache.cloudstack.oauth2.dao.OauthProviderDao;
+import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
+import org.apache.http.HttpEntity;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.cloud.exception.CloudAuthenticationException;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+public class KeycloakOAuth2ProviderTest {
+
+ @Mock
+ private OauthProviderDao oauthProviderDao;
+
+ @Mock
+ private CloseableHttpClient httpClient;
+
+ private KeycloakOAuth2Provider provider;
+
+ private OauthProviderVO mockProviderVO;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ provider = new KeycloakOAuth2Provider(httpClient);
+ provider.oauthProviderDao = oauthProviderDao;
+
+ mockProviderVO = new OauthProviderVO();
+ mockProviderVO.setClientId("test-client");
+ mockProviderVO.setSecretKey("test-secret");
+ mockProviderVO.setTokenUrl("http://localhost/token");
+ mockProviderVO.setRedirectUri("http://localhost/redirect");
+ }
+
+ @Test
+ public void testGetName() {
+ assertEquals("keycloak", provider.getName());
+ }
+
+ @Test(expected = CloudAuthenticationException.class)
+ public void testVerifyUserEmptyParams() {
+ provider.verifyUser("", "");
+ }
+
+ @Test(expected = CloudAuthenticationException.class)
+ public void testVerifyUserProviderNotFound() {
+ when(oauthProviderDao.findByProvider("keycloak")).thenReturn(null);
+ provider.verifyUser("test@example.com", "code123");
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testVerifyCodeAndFetchEmailHttpError() throws IOException {
+ when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO);
+
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ StatusLine statusLine = mock(StatusLine.class);
+
+ when(statusLine.getStatusCode()).thenReturn(400);
+ when(response.getStatusLine()).thenReturn(statusLine);
+
+ HttpEntity entity = mock(HttpEntity.class);
+ when(entity.getContent()).thenReturn(new ByteArrayInputStream("error".getBytes()));
+ when(response.getEntity()).thenReturn(entity);
+
+ when(httpClient.execute(any(HttpPost.class))).thenReturn(response);
+
+ provider.verifyCodeAndFetchEmail("invalid-code");
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testVerifyCodeAndFetchEmailNetworkFailure() throws IOException {
+ when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO);
+ when(httpClient.execute(any(HttpPost.class))).thenThrow(new IOException("Connexion refusée"));
+
+ provider.verifyCodeAndFetchEmail("code");
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testVerifyUserWithMismatchedEmail() throws IOException {
+ when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO);
+
+ String testEmail = "anotheruser@example.com";
+ String secretCode = "valid-auth-code";
+
+ String header = "{\"alg\":\"none\"}";
+ String payload = "{" +
+ "\"aud\":[\"test-client\"]," +
+ "\"email\":\"" + testEmail + "\"," +
+ "\"iss\":\"http://keycloak\"," +
+ "\"sub\":\"12345\"" +
+ "}";
+
+ String encodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(header.getBytes());
+ String encodedPayload = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.getBytes());
+ String fakeJwt = encodedHeader + "." + encodedPayload + ".not-checked-signature";
+
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ StatusLine statusLine = mock(StatusLine.class);
+ HttpEntity entity = mock(HttpEntity.class);
+
+ when(statusLine.getStatusCode()).thenReturn(200);
+ when(response.getStatusLine()).thenReturn(statusLine);
+
+ String jsonResponseBody = "{\"id_token\":\"" + fakeJwt + "\", \"access_token\":\"acc-123\"}";
+ when(entity.getContent()).thenReturn(new ByteArrayInputStream(jsonResponseBody.getBytes(StandardCharsets.UTF_8)));
+ when(response.getEntity()).thenReturn(entity);
+
+ when(httpClient.execute(any(HttpPost.class))).thenReturn(response);
+
+ boolean result = provider.verifyUser("user@example.com", secretCode);
+
+ assertTrue("L'utilisateur devrait être vérifié avec succès", result);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testVerifyUserWithMismatchedClient() throws IOException {
+ when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO);
+
+ String testEmail = "anotheruser@example.com";
+ String secretCode = "valid-auth-code";
+
+ String header = "{\"alg\":\"none\"}";
+ String payload = "{" +
+ "\"aud\":[\"anothertest-client\"]," +
+ "\"email\":\"" + testEmail + "\"," +
+ "\"iss\":\"http://keycloak\"," +
+ "\"sub\":\"12345\"" +
+ "}";
+
+ String encodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(header.getBytes());
+ String encodedPayload = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.getBytes());
+ String fakeJwt = encodedHeader + "." + encodedPayload + ".not-checked-signature";
+
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ StatusLine statusLine = mock(StatusLine.class);
+ HttpEntity entity = mock(HttpEntity.class);
+
+ when(statusLine.getStatusCode()).thenReturn(200);
+ when(response.getStatusLine()).thenReturn(statusLine);
+
+ String jsonResponseBody = "{\"id_token\":\"" + fakeJwt + "\", \"access_token\":\"acc-123\"}";
+ when(entity.getContent()).thenReturn(new ByteArrayInputStream(jsonResponseBody.getBytes(StandardCharsets.UTF_8)));
+ when(response.getEntity()).thenReturn(entity);
+
+ when(httpClient.execute(any(HttpPost.class))).thenReturn(response);
+
+ boolean result = provider.verifyUser(testEmail, secretCode);
+
+ assertTrue("L'utilisateur devrait être vérifié avec succès", result);
+ }
+
+ @Test
+ public void testVerifyUserEmail() throws IOException {
+ when(oauthProviderDao.findByProvider("keycloak")).thenReturn(mockProviderVO);
+
+ String testEmail = "user@example.com";
+ String secretCode = "valid-auth-code";
+
+ String header = "{\"alg\":\"none\"}";
+ String payload = "{" +
+ "\"aud\":[\"test-client\"]," +
+ "\"email\":\"" + testEmail + "\"," +
+ "\"iss\":\"http://keycloak\"," +
+ "\"sub\":\"12345\"" +
+ "}";
+
+ String encodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(header.getBytes());
+ String encodedPayload = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.getBytes());
+ String fakeJwt = encodedHeader + "." + encodedPayload + ".not-checked-signature";
+
+ CloseableHttpResponse response = mock(CloseableHttpResponse.class);
+ StatusLine statusLine = mock(StatusLine.class);
+ HttpEntity entity = mock(HttpEntity.class);
+
+ when(statusLine.getStatusCode()).thenReturn(200);
+ when(response.getStatusLine()).thenReturn(statusLine);
+
+ String jsonResponseBody = "{\"id_token\":\"" + fakeJwt + "\", \"access_token\":\"acc-123\"}";
+ when(entity.getContent()).thenReturn(new ByteArrayInputStream(jsonResponseBody.getBytes(StandardCharsets.UTF_8)));
+ when(response.getEntity()).thenReturn(entity);
+
+ when(httpClient.execute(any(HttpPost.class))).thenReturn(response);
+
+ boolean result = provider.verifyUser(testEmail, secretCode);
+
+ assertTrue("L'utilisateur devrait être vérifié avec succès", result);
+ }
+
+ @Test
+ public void testGetDescription() {
+ assertEquals("Keycloak OAuth2 Provider Plugin", provider.getDescription());
+ }
+}
\ No newline at end of file
diff --git a/ui/public/assets/keycloak.svg b/ui/public/assets/keycloak.svg
new file mode 100644
index 000000000000..3e8115efc160
--- /dev/null
+++ b/ui/public/assets/keycloak.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 6f3a623b8092..b9af10ee3499 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -438,6 +438,7 @@
"label.attaching": "Attaching",
"label.authentication.method": "Authentication Method",
"label.authentication.sshkey": "System SSH Key",
+"label.authorizeurl": "Authorize URL",
"label.use.existing.vcenter.credentials.from.zone": "Use existing vCenter credentials from the Zone",
"label.autoscale": "AutoScale",
"label.autoscalevmgroupname": "AutoScaling Group",
@@ -2576,6 +2577,7 @@
"label.to": "to",
"label.token": "Token",
"label.token.for.dashboard.login": "Token for dashboard login can be retrieved using following command",
+"label.tokenurl": "Token URL",
"label.tools": "Tools",
"label.total": "Total",
"label.total.network": "Total Networks",
diff --git a/ui/src/config/section/config.js b/ui/src/config/section/config.js
index e190515855e6..2a83b25c002f 100644
--- a/ui/src/config/section/config.js
+++ b/ui/src/config/section/config.js
@@ -80,7 +80,7 @@ export default {
docHelp: 'adminguide/accounts.html#using-an-ldap-server-for-user-authentication',
permission: ['listOauthProvider'],
columns: ['provider', 'enabled', 'description', 'clientid', 'secretkey', 'redirecturi'],
- details: ['provider', 'description', 'enabled', 'clientid', 'secretkey', 'redirecturi'],
+ details: ['provider', 'description', 'enabled', 'clientid', 'secretkey', 'redirecturi', 'authorizeurl', 'tokenurl'],
actions: [
{
api: 'registerOauthProvider',
@@ -89,11 +89,11 @@ export default {
listView: true,
dataView: false,
args: [
- 'provider', 'description', 'clientid', 'redirecturi', 'secretkey'
+ 'provider', 'description', 'clientid', 'redirecturi', 'secretkey', 'authorizeurl', 'tokenurl'
],
mapping: {
provider: {
- options: ['google', 'github']
+ options: ['google', 'github', 'keycloak']
}
}
},
@@ -103,7 +103,7 @@ export default {
label: 'label.edit',
dataView: true,
popup: true,
- args: ['description', 'clientid', 'redirecturi', 'secretkey']
+ args: ['description', 'clientid', 'redirecturi', 'secretkey', 'authorizeurl', 'tokenurl']
},
{
api: 'updateOauthProvider',
diff --git a/ui/src/views/auth/Login.vue b/ui/src/views/auth/Login.vue
index 24065f47b1aa..691fd75cf34e 100644
--- a/ui/src/views/auth/Login.vue
+++ b/ui/src/views/auth/Login.vue
@@ -202,6 +202,18 @@
Sign in with Google
+
+
+
+ Sign in with Keycloak
+
+
@@ -231,10 +243,14 @@ export default {
socialLogin: false,
googleprovider: false,
githubprovider: false,
+ keycloakprovider: false,
googleredirecturi: '',
githubredirecturi: '',
+ keycloakredirecturi: '',
googleclientid: '',
githubclientid: '',
+ keycloakclientid: '',
+ keycloakauthorizeurl: '',
loginType: 0,
state: {
time: 60,
@@ -325,8 +341,14 @@ export default {
this.githubclientid = item.clientid
this.githubredirecturi = item.redirecturi
}
+ if (item.provider === 'keycloak') {
+ this.keycloakprovider = item.enabled
+ this.keycloakclientid = item.clientid
+ this.keycloakredirecturi = item.redirecturi
+ this.keycloakauthorizeurl = item.authorizeurl
+ }
})
- this.socialLogin = this.googleprovider || this.githubprovider
+ this.socialLogin = this.googleprovider || this.githubprovider || this.keycloakprovider
}
})
postAPI('forgotPassword', {}).then(response => {
@@ -362,6 +384,10 @@ export default {
this.handleDomain()
this.$store.commit('SET_OAUTH_PROVIDER_USED_TO_LOGIN', 'google')
},
+ handleKeycloakProviderAndDomain () {
+ this.handleDomain()
+ this.$store.commit('SET_OAUTH_PROVIDER_USED_TO_LOGIN', 'keycloak')
+ },
handleDomain () {
const values = toRaw(this.form)
if (!values.domain) {
@@ -401,6 +427,20 @@ export default {
return `${rootUrl}?${qs.toString()}`
},
+ getKeycloakUrl (from) {
+ const rootURl = this.keycloakauthorizeurl
+ const options = {
+ redirect_uri: this.keycloakredirecturi,
+ client_id: this.keycloakclientid,
+ response_type: 'code',
+ scope: 'openid email',
+ state: 'cloudstack'
+ }
+
+ const qs = new URLSearchParams(options)
+
+ return `${rootURl}?${qs.toString()}`
+ },
handleSubmit (e) {
e.preventDefault()
if (this.state.loginBtn) return