This commit is contained in:
Joël 2026-05-12 08:17:17 +01:00 committed by GitHub
commit 1da29222b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 651 additions and 87 deletions

View File

@ -1330,8 +1330,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";

View File

@ -118,6 +118,10 @@ 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'' ');
--- Quota tariff/usage mapping
CREATE TABLE IF NOT EXISTS `cloud_usage`.`quota_tariff_usage` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,

View File

@ -38,6 +38,11 @@
<artifactId>cloud-framework-config</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-security-jose</artifactId>
<version>${cs.cxf.version}</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-docs</artifactId>

View File

@ -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.dao.OauthProviderDao;
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<Class<?>> getAuthCommands() {
List<Class<?>> cmdList = new ArrayList<Class<?>>();
List<Class<?>> cmdList = new ArrayList<>();
cmdList.add(OauthLoginAPIAuthenticatorCmd.class);
cmdList.add(ListOAuthProvidersCmd.class);
cmdList.add(VerifyOAuthCodeAndGetUserCmd.class);
@ -84,7 +83,7 @@ public class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
@Override
public List<Class<?>> getCommands() {
List<Class<?>> cmdList = new ArrayList<Class<?>>();
List<Class<?>> cmdList = new ArrayList<>();
cmdList.add(RegisterOAuthProviderCmd.class);
cmdList.add(DeleteOAuthProviderCmd.class);
cmdList.add(UpdateOAuthProviderCmd.class);
@ -127,9 +126,7 @@ public class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
@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 class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
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 class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
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 class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
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 class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
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 class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
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 @@ public class OAuth2AuthManagerImpl extends ManagerBase implements OAuth2AuthMana
oauthProviderVO.setClientId(clientId);
oauthProviderVO.setSecretKey(secretKey);
oauthProviderVO.setRedirectUri(redirectUri);
oauthProviderVO.setAuthorizeUrl(authorizeUrl);
oauthProviderVO.setTokenUrl(tokenUrl);
oauthProviderVO.setEnabled(true);
_oauthProviderDao.persist(oauthProviderVO);

View File

@ -21,8 +21,10 @@ import java.util.ArrayList;
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.api.response.OauthProviderResponse;
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 class ListOAuthProvidersCmd extends BaseListCmd implements APIAuthenticat
List<OauthProviderResponse> 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 {

View File

@ -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.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.collections.MapUtils;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.lang3.StringUtils;
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 keycloack 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 class RegisterOAuthProviderCmd extends BaseCmd {
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 class RegisterOAuthProviderCmd extends BaseCmd {
@Override
public void execute() throws ServerApiException, ConcurrentOperationException, EntityExistsException {
if (StringUtils.equals("keycloak", getProvider())) {
if (StringUtils.isBlank(getAuthorizeUrl())) {
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter authorizeurl is mandatory for keycloak OAuth Provider");
}
if (StringUtils.isBlank(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);

View File

@ -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 final class UpdateOAuthProviderCmd extends BaseCmd {
return redirectUri;
}
public String getAuthorizeUrl() {
return authorizeUrl;
}
public String getTokenUrl() {
return tokenUrl;
}
public Boolean getEnabled() {
return enabled;
}
@ -115,7 +129,8 @@ public final class UpdateOAuthProviderCmd extends BaseCmd {
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<UserOAuth2Authenticator> userOAuth2AuthenticatorPlugins = _oauthMgr.listUserOAuth2AuthenticationProviders();
List<String> authenticatorPluginNames = new ArrayList<>();

View File

@ -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 class OauthProviderResponse extends BaseResponse {
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;
}

View File

@ -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.net.URL;
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

View File

@ -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.JsonFactory;
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 class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authe
@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 class GoogleOAuth2Provider extends AdapterBase implements UserOAuth2Authe
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();
}

View File

@ -0,0 +1,177 @@
//
// 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 (provider == null) {
throw new CloudAuthenticationException("Keycloak provider is not registered, so user cannot be verified");
}
if (StringUtils.isBlank(idToken)) {
String auth = provider.getClientId() + ":" + provider.getSecretKey();
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
List<NameValuePair> 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 generate 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;
}
}

View File

@ -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.GeneratedValue;
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 class OauthProviderVO implements Identity, InternalIdentity {
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;
}

View File

@ -35,6 +35,9 @@
<bean id="GithubOAuth2Provider" class="org.apache.cloudstack.oauth2.github.GithubOAuth2Provider">
<property name="name" value="github" />
</bean>
<bean id="KeycloakOAuth2Provider" class="org.apache.cloudstack.oauth2.keycloak.KeycloakOAuth2Provider">
<property name="name" value="keycloak" />
</bean>
<bean id="OAuth2AuthManager" class="org.apache.cloudstack.oauth2.OAuth2AuthManagerImpl">
<property name="name" value="OAUTH2Auth" />
@ -45,7 +48,7 @@
class="org.apache.cloudstack.spring.lifecycle.registry.ExtensionRegistry">
<property name="orderConfigKey" value="user.oauth2.providers.order" />
<property name="excludeKey" value="oauth2.plugins.exclude" />
<property name="orderConfigDefault" value="google,github" />
<property name="orderConfigDefault" value="google,github,keycloak" />
</bean>
<bean class="org.apache.cloudstack.spring.lifecycle.registry.RegistryLifecycle">

View File

@ -0,0 +1,225 @@
//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
//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 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("Connection refused"));
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);
provider.verifyUser("user@example.com", secretCode);
}
@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);
provider.verifyUser(testEmail, secretCode);
}
@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("User successfully verified", result);
}
@Test
public void testGetDescription() {
assertEquals("Keycloak OAuth2 Provider Plugin", provider.getDescription());
}
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512"><style>.st9{fill:#d0d0d0}.st11{fill:#d9d9d9}.st13{fill:#d8d8d8}.st14{fill:#e2e2e2}.st16{fill:#dedede}.st21{fill:#00b8e3}.st22{fill:#33c6e9}.st23{fill:#008aaa}</style><g id="g2460" transform="translate(.714 .07)"><path id="path1588" d="M432.9 149.2c-1.4 0-2.7-.7-3.4-2L370.1 44.1c-.7-1.2-2-2-3.5-2H124.2c-1.4 0-2.7.7-3.4 2L58.9 150.9l23.9 34.9c-.7 1.2-6.2 24-5.5 25.2L58.9 360.9l61.9 106.9c.7 1.2 2 2 3.4 2h242.4c1.4 0 2.7-.7 3.5-2l59.4-103.2c.7-1.2 2-2 3.4-2h73.8c2.4 0 4.4-2 4.4-4.4V153.6c0-2.4-2-4.4-4.4-4.4z" style="fill:#4d4d4d"/><path id="path1594" d="M72.7 245.3 6.4 269.4l-6.6-11.3c-.7-1.2-.7-2.7 0-3.9l30-52z" style="fill:#e1e1e1"/><path id="polygon1794" d="M511.3 258.3V309l-43.7-44.5z" style="fill:#c8c8c8"/><path id="path1798" d="m467.5 264.5 43.7 44.5v49.6c0 2.4-2 4.4-4.4 4.4H456z" style="fill:#c2c2c2"/><path id="polygon1802" d="M467.5 264.5 456 362.9h-61.2l-18.5-44.7z" style="fill:#c7c7c7"/><path id="polygon1804" d="M511.3 211.2v47l-43.7 6.2z" style="fill:#cecece"/><path id="path1808" d="M511.3 153.6v57.6l-43.7 53.2-33.1-115.3h72.2c2.4-.1 4.5 1.8 4.6 4.3z" style="fill:#d3d3d3"/><path id="polygon1812" d="M394.8 362.9h-32.3l-8.4-12 22.1-32.7z" style="fill:#c6c6c6"/><path id="polygon1814" d="m467.5 264.5-121.1-51.2 63.7-64.1h24.4z" style="fill:#d5d5d5"/><path id="path1816" d="m346.5 213.3 29.8 105 91.2-53.8z" class="st9"/><path id="polygon1818" d="m353.8 362.9.4-12 8.4 12z" style="fill:#bfbfbf"/><path id="polygon1820" d="m410.1 149.2-63.7 64.1-11.4-57.4 24.6-6.8h50.5z" class="st11"/><path id="path1822" d="m346.5 213.3-147 33.9 154.7 103.7z" style="fill:#d4d4d4"/><path id="path1824" d="m346.5 213.3 7.7 137.6 22.1-32.7z" class="st9"/><path id="path1826" d="m335 155.9-135.5 91.2 147-33.9z" class="st11"/><path id="polygon1828" d="m199.5 247.2-63.7 115.7H99.6L72.7 245.3z" class="st13"/><path id="path1830" d="m134.3 149.2-61.5 96.1L57.3 155l2.2-3.8c.7-1.2 2-1.9 3.4-1.9z" class="st14"/><path id="path1832" d="M99.6 362.9H62.7c-1.4 0-2.8-.8-3.5-2L6.4 269.4l66.4-24.1z" class="st13"/><path id="polygon1834" d="M29.9 202.1 57.1 155l15.7 90.3z" style="fill:#e4e4e4"/><path id="polygon1836" d="m335 155.9-40.8-6.8H159.4l40.1 98z" class="st16"/><path id="polygon1838" d="m199.5 247.2-40.1-98h-25.1l-61.5 96.1z" class="st16"/><path id="polygon1840" d="M324.7 362.9h29.1l.4-12z" style="fill:#c5c5c5"/><path id="polygon1842" d="M266.7 362.9h58l29.5-12-154.7-103.7 27.9 115.7z" class="st9"/><path id="polygon1844" d="m227.4 362.9-27.9-115.7-63.7 115.7z" style="fill:#d1d1d1"/><path id="polygon1856" d="m335.4 149.2-.4 6.8 24.6-6.8z" style="fill:#ddd"/><path id="polygon1858" d="m335 155.9-3.8-6.8h-37z" style="fill:#e3e3e3"/><path id="polygon1860" d="m335 155.9.4-6.8h-4.2z" class="st14"/><path id="path1862" d="m223.9 151-59.7 103.4c-.3.5-.4 1.1-.4 1.7h-41.7l82-142q.75.45 1.2 1.2l18.6 32.3c.5 1.1.5 2.4 0 3.4" class="st21"/><path id="path1864" d="M223.8 364.9 205.3 397q-.45.75-1.2 1.2l-82-142.2h41.7c0 .6.1 1.1.4 1.6l59.6 103.2c.8 1.2.9 2.9 0 4.1" class="st22"/><path id="path1866" d="m204 114.2-82 141.9-20.6 35.6-19.6-34c-.3-.5-.4-1-.4-1.6s.1-1.2.4-1.7l19.9-34.4 60.4-104.5c.6-1.1 1.8-1.8 3-1.8h37.2c.6 0 1.2.2 1.7.5" class="st23"/><path id="path1868" d="M204 398.2c-.5.3-1.1.5-1.8.5h-37.1c-1.3 0-2.4-.7-3-1.8l-55.2-95.6-5.5-9.5 20.6-35.6z" class="st21"/><path id="path1870" d="m368.9 256.1-82 142q-.75-.45-1.2-1.2L267 364.7c-.5-1-.5-2.3 0-3.3L326.7 258c.3-.5.5-1.2.5-1.8z" class="st23"/><path id="path1872" d="M409.4 256.1c0 .6-.2 1.3-.5 1.8l-80.3 139.3c-.6 1-1.8 1.7-3 1.6h-37c-.6 0-1.2-.2-1.8-.5L368.9 256l20.6-35.6 19.5 33.8c.3.7.4 1.3.4 1.9" class="st21"/><path id="path1874" d="M368.9 256.1h-41.7c0-.6-.2-1.2-.5-1.8L267 151.2c-.6-1.1-.6-2.5 0-3.6l18.6-32.2q.45-.75 1.2-1.2z" class="st21"/><path id="path1876" d="m389.4 220.5-20.6 35.6-82-142c.6-.3 1.2-.5 1.8-.5h37.1c1.2 0 2.3.6 3 1.6z" class="st22"/></g></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -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",
@ -2584,6 +2585,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",

View File

@ -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',

View File

@ -186,7 +186,7 @@
:href="getGitHubUrl(from)"
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" />
<a-typography-text>Sign in with Github</a-typography-text>
</a-button>
</div>
@ -198,10 +198,22 @@
:href="getGoogleUrl(from)"
class="auth-btn google-auth"
style="height: 38px; width: 185px; padding: 0" >
<img src="/assets/google.svg" style="width: 32px; padding: 5px" />
<img src="/assets/google.svg" alt="Github" style="width: 32px; padding: 5px" />
<a-typography-text>Sign in with Google</a-typography-text>
</a-button>
</div>
<div class="social-auth" v-if="keycloakprovider">
<a-button
@click="handleKeycloakProviderAndDomain"
tag="a"
color="primary"
:href="getKeycloakUrl(from)"
class="auth-btn keycloak-auth"
style="height: 38px; width: 185px; padding: 0" >
<img src="/assets/keycloak.svg" alt="Keycloak" style="width: 32px; padding: 5px" />
<a-typography-text>Sign in with Keycloak</a-typography-text>
</a-button>
</div>
</div>
</a-form>
</template>
@ -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