Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,7 @@ public class ApiConstants {
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 ISSUER_URL = "issuerurl";

public static final String IS_TAG_A_RULE = "istagarule";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ public interface UserOAuth2Authenticator extends Adapter {
*/
String verifySecretCodeAndFetchEmail(String secretCode, Long domainId);

/**
* Verifies the user against the registration identified by providerName. Implementations that
* serve a single registration ignore the name; implementations shared by several registrations
* use it to select the one to authenticate against.
* @return true if it's a valid user, otherwise false
*/
default boolean verifyUser(String email, String secretCode, Long domainId, String providerName) {
return verifyUser(email, secretCode, domainId);
}

/**
* Verifies the secret code against the registration identified by providerName and fetches email.
* @return email for the specified registration
*/
default String verifySecretCodeAndFetchEmail(String secretCode, Long domainId, String providerName) {
return verifySecretCodeAndFetchEmail(secretCode, domainId);
}

/**
* Fetches email using the accessToken
* @return returns email
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@
--;
-- Schema upgrade from 4.23.0.0 to 24.0.0
--;

-- Generic OIDC OAuth2 provider: a type to select the implementation, and the issuer URL for discovery
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider', 'type', 'VARCHAR(40) DEFAULT NULL COMMENT ''Provider implementation serving this registration, for example oidc; NULL means the provider name selects the implementation'' AFTER `token_url` ');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.oauth_provider', 'issuer_url', 'VARCHAR(255) DEFAULT NULL COMMENT ''Issuer URL of the OpenID Connect provider, used to read its discovery document'' AFTER `type` ');
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public interface OAuth2AuthManager extends PluggableAPIAuthenticator, PluggableS
*/
UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(final String providerName);

/**
* Finds the user OAuth2 provider serving the named registration within a domain scope. A name
* that matches no provider plugin is resolved through the type of its registration, so that a
* generic provider can serve registrations under administrator chosen names.
* @param providerName name of the registration
* @param domainId domain id, or null for global
* @return OAuth2 provider
*/
UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(final String providerName, final Long domainId);

String verifySecretCodeAndFetchEmail(String code, String provider, Long domainId);

OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,30 @@ public List<UserOAuth2Authenticator> listUserOAuth2AuthenticationProviders() {

@Override
public UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(String providerName) {
return getUserOAuth2AuthenticationProvider(providerName, null);
}

@Override
public UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(String providerName, Long domainId) {
if (StringUtils.isEmpty(providerName)) {
throw new CloudRuntimeException("OAuth2 authentication provider name is empty");
}
if (!userOAuth2AuthenticationProvidersMap.containsKey(providerName.toLowerCase())) {
UserOAuth2Authenticator authenticator = userOAuth2AuthenticationProvidersMap.get(providerName.toLowerCase());
if (authenticator == null) {
authenticator = findAuthenticatorByRegisteredType(providerName, domainId);
}
if (authenticator == null) {
throw new CloudRuntimeException(String.format("Failed to find OAuth2 authentication provider by the name: %s.", providerName));
}
return userOAuth2AuthenticationProvidersMap.get(providerName.toLowerCase());
return authenticator;
}

protected UserOAuth2Authenticator findAuthenticatorByRegisteredType(String providerName, Long domainId) {
OauthProviderVO registration = _oauthProviderDao.findByProviderAndDomainWithGlobalFallback(providerName, domainId);
if (registration == null || StringUtils.isBlank(registration.getType())) {
return null;
}
return userOAuth2AuthenticationProvidersMap.get(registration.getType().toLowerCase());
}

public List<UserOAuth2Authenticator> getUserOAuth2AuthenticationProviders() {
Expand All @@ -152,8 +169,8 @@ protected void initializeUserOAuth2AuthenticationProvidersMap() {

@Override
public String verifySecretCodeAndFetchEmail(String code, String provider, Long domainId) {
UserOAuth2Authenticator authenticator = getUserOAuth2AuthenticationProvider(provider);
String email = authenticator.verifySecretCodeAndFetchEmail(code, domainId);
UserOAuth2Authenticator authenticator = getUserOAuth2AuthenticationProvider(provider, domainId);
String email = authenticator.verifySecretCodeAndFetchEmail(code, domainId, provider);

return email;
}
Expand All @@ -168,11 +185,17 @@ public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) {
Long domainId = normalizeGlobalScope(resolveDomainIdFromIdOrPath(cmd.getDomainId(), cmd.getDomainPath()));
String authorizeUrl = StringUtils.trim(cmd.getAuthorizeUrl());
String tokenUrl = StringUtils.trim(cmd.getTokenUrl());
String type = StringUtils.trim(cmd.getType());
String issuerUrl = StringUtils.trim(cmd.getIssuerUrl());

if (!isOAuthPluginEnabled(domainId)) {
throw new CloudRuntimeException("OAuth is not enabled, please enable to register");
}

if (StringUtils.isNotBlank(type) && !userOAuth2AuthenticationProvidersMap.containsKey(type.toLowerCase())) {
throw new CloudRuntimeException(String.format("No OAuth2 provider plugin is available for the type %s", type));
}

// Check for existing provider with same name and domain
OauthProviderVO providerVO = _oauthProviderDao.findByProviderAndDomain(provider, domainId);
if (providerVO != null) {
Expand All @@ -183,7 +206,7 @@ public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) {
}
}

return saveOauthProvider(provider, description, clientId, secretKey, redirectUri, authorizeUrl, tokenUrl, domainId);
return saveOauthProvider(provider, description, clientId, secretKey, redirectUri, authorizeUrl, tokenUrl, domainId, type, issuerUrl);
}

@Override
Expand Down Expand Up @@ -212,6 +235,7 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) {
String secretKey = StringUtils.trim(cmd.getSecretKey());
String authorizeUrl = StringUtils.trim(cmd.getAuthorizeUrl());
String tokenUrl = StringUtils.trim(cmd.getTokenUrl());
String issuerUrl = StringUtils.trim(cmd.getIssuerUrl());
Boolean enabled = cmd.getEnabled();

OauthProviderVO providerVO = _oauthProviderDao.findById(id);
Expand Down Expand Up @@ -261,6 +285,9 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) {
if (StringUtils.isNotEmpty(tokenUrl)) {
providerVO.setTokenUrl(tokenUrl);
}
if (StringUtils.isNotEmpty(issuerUrl)) {
providerVO.setIssuerUrl(issuerUrl);
}
if (enabled != null) {
providerVO.setEnabled(enabled);
}
Expand All @@ -271,7 +298,8 @@ public OauthProviderVO updateOauthProvider(UpdateOAuthProviderCmd cmd) {
return _oauthProviderDao.findById(id);
}

private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl, String tokenUrl, Long domainId) {
private OauthProviderVO saveOauthProvider(String provider, String description, String clientId, String secretKey, String redirectUri, String authorizeUrl,
String tokenUrl, Long domainId, String type, String issuerUrl) {
final OauthProviderVO oauthProviderVO = new OauthProviderVO();

oauthProviderVO.setProvider(provider);
Expand All @@ -282,6 +310,8 @@ private OauthProviderVO saveOauthProvider(String provider, String description, S
oauthProviderVO.setDomainId(domainId);
oauthProviderVO.setAuthorizeUrl(authorizeUrl);
oauthProviderVO.setTokenUrl(tokenUrl);
oauthProviderVO.setType(type);
oauthProviderVO.setIssuerUrl(issuerUrl);
oauthProviderVO.setEnabled(true);

_oauthProviderDao.persist(oauthProviderVO);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ public Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username,
String email = ((emailArray == null) ? null : emailArray[0]);
String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]);

UserOAuth2Authenticator authenticator = userOAuth2mgr.getUserOAuth2AuthenticationProvider(oauthProvider);
if (Objects.nonNull(user) && authenticator.verifyUser(email, secretCode, domainId)) {
UserOAuth2Authenticator authenticator = userOAuth2mgr.getUserOAuth2AuthenticationProvider(oauthProvider, domainId);
if (Objects.nonNull(user) && authenticator.verifyUser(email, secretCode, domainId, oauthProvider)) {
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;

import com.cloud.api.ApiDBUtils;
import com.cloud.api.ApiServer;
Expand Down Expand Up @@ -184,8 +185,10 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(),
result.getDescription(), result.getClientId(), secretKeyAllowed ? result.getSecretKey() : null,
result.getRedirectUri(), result.getAuthorizeUrl(), result.getTokenUrl(), domain);
r.setType(result.getType());
r.setIssuerUrl(result.getIssuerUrl());
boolean oauthEnabled = OAuth2AuthManager.isPluginEnabledForDomain(result.getDomainId());
if (oauthEnabled && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
if (oauthEnabled && isServedByPlugin(result, authenticatorPluginNames) && result.isEnabled()) {
r.setEnabled(true);
} else {
r.setEnabled(false);
Expand All @@ -200,7 +203,7 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
for (OauthProviderVO domainProvider : allProviders) {
if (domainProvider.getDomainId() != null && domainProvider.isEnabled()
&& OAuth2AuthManager.isPluginEnabledForDomain(domainProvider.getDomainId())
&& authenticatorPluginNames.contains(domainProvider.getProvider())) {
&& isServedByPlugin(domainProvider, authenticatorPluginNames)) {
totalEnabledCount++;
}
}
Expand All @@ -214,6 +217,11 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
return ApiResponseSerializer.toSerializedString(response, responseType);
}

protected boolean isServedByPlugin(OauthProviderVO provider, List<String> authenticatorPluginNames) {
return authenticatorPluginNames.contains(provider.getProvider())
|| (StringUtils.isNotBlank(provider.getType()) && authenticatorPluginNames.contains(provider.getType()));
}

@Override
public APIAuthenticationType getAPIType() {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.apache.cloudstack.oauth2.OAuth2AuthManager;
import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
import org.apache.cloudstack.oauth2.keycloak.KeycloakOAuth2Provider;
import org.apache.cloudstack.oauth2.oidc.GenericOIDCOAuth2Provider;
import org.apache.cloudstack.oauth2.vo.OauthProviderVO;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -76,6 +77,16 @@ public class RegisterOAuthProviderCmd extends BaseCmd {
@Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL for OAuth finalization (only required for keycloak provider)", since = "4.23.0")
private String tokenUrl;

@Parameter(name = ApiConstants.TYPE, type = CommandType.STRING,
description = "Type of the provider implementation serving this registration, for example oidc for any OpenID Connect compliant provider. "
+ "When set, the name in provider is a label chosen by the administrator rather than a built in provider name.", since = "4.24.0")
private String type;

@Parameter(name = ApiConstants.ISSUER_URL, type = CommandType.STRING,
description = "Issuer URL of the OpenID Connect provider, required for type oidc. The token endpoint and the keys that sign "
+ "its tokens are read from the issuer's discovery document", since = "4.24.0")
private String issuerUrl;

@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;
Expand Down Expand Up @@ -121,6 +132,14 @@ public String getTokenUrl() {
return tokenUrl;
}

public String getType() {
return type;
}

public String getIssuerUrl() {
return issuerUrl;
}

public Map getDetails() {
if (MapUtils.isEmpty(details)) {
return null;
Expand All @@ -143,12 +162,18 @@ public void execute() throws ServerApiException, ConcurrentOperationException, E
}
}

if (StringUtils.equalsIgnoreCase(GenericOIDCOAuth2Provider.OIDC_PROVIDER_TYPE, getType()) && StringUtils.isBlank(getIssuerUrl())) {
throw new ServerApiException(ApiErrorCode.BAD_REQUEST, "Parameter issuerurl is mandatory for an oidc OAuth provider");
}

OauthProviderVO provider = _oauth2mgr.registerOauthProvider(this);

Domain domain = provider.getDomainId() != null ? ApiDBUtils.findDomainById(provider.getDomainId()) : null;
OauthProviderResponse response = new OauthProviderResponse(provider.getUuid(), provider.getProvider(),
provider.getDescription(), provider.getClientId(), provider.getSecretKey(), provider.getRedirectUri(),
provider.getAuthorizeUrl(), provider.getTokenUrl(), domain);
response.setType(provider.getType());
response.setIssuerUrl(provider.getIssuerUrl());
response.setResponseName(getCommandName());
response.setObjectName(ApiConstants.OAUTH_PROVIDER);
setResponseObject(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
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.lang3.StringUtils;

import com.cloud.api.ApiDBUtils;
import com.cloud.domain.Domain;
Expand Down Expand Up @@ -67,6 +68,10 @@ public final class UpdateOAuthProviderCmd extends BaseCmd {
@Parameter(name = ApiConstants.TOKEN_URL, type = CommandType.STRING, description = "Token URL pre-registered in the specific OAuth provider", since = "4.23.0")
private String tokenUrl;

@Parameter(name = ApiConstants.ISSUER_URL, type = CommandType.STRING,
description = "Issuer URL of the OpenID Connect provider, used to read its discovery document", since = "4.24.0")
private String issuerUrl;

@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "OAuth provider will be enabled or disabled based on this value")
private Boolean enabled;

Expand Down Expand Up @@ -113,6 +118,10 @@ public String getTokenUrl() {
return tokenUrl;
}

public String getIssuerUrl() {
return issuerUrl;
}

public Boolean getEnabled() {
return enabled;
}
Expand Down Expand Up @@ -152,15 +161,19 @@ public void execute() {
OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(),
result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(),
result.getAuthorizeUrl(), result.getTokenUrl(), domain);
r.setType(result.getType());
r.setIssuerUrl(result.getIssuerUrl());

List<UserOAuth2Authenticator> userOAuth2AuthenticatorPlugins = _oauthMgr.listUserOAuth2AuthenticationProviders();
List<String> authenticatorPluginNames = new ArrayList<>();
for (UserOAuth2Authenticator authenticator : userOAuth2AuthenticatorPlugins) {
String name = authenticator.getName();
authenticatorPluginNames.add(name);
}
boolean servedByPlugin = authenticatorPluginNames.contains(result.getProvider())
|| (StringUtils.isNotBlank(result.getType()) && authenticatorPluginNames.contains(result.getType()));
boolean oauthEnabled = OAuth2AuthManager.isPluginEnabledForDomain(result.getDomainId());
if (oauthEnabled && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
if (oauthEnabled && servedByPlugin && result.isEnabled()) {
r.setEnabled(true);
} else {
r.setEnabled(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public class OauthProviderResponse extends BaseResponse {
@Param(description = "path of the domain the provider belongs to (empty for global)", since = "4.23.0")
private String domainPath;

@SerializedName(ApiConstants.TYPE)
@Param(description = "Type of the provider, for example oidc for a generic OpenID Connect provider. Empty for the built in providers", since = "4.24.0")
private String type;

@SerializedName(ApiConstants.ISSUER_URL)
@Param(description = "Issuer URL of the OpenID Connect provider, used for discovery", since = "4.24.0")
private String issuerUrl;

@SerializedName(ApiConstants.AUTHORIZE_URL)
@Param(description = "Authorize URL registered in the OAuth provider")
private String authorizeUrl;
Expand Down Expand Up @@ -180,6 +188,22 @@ public void setDomainPath(String domainPath) {
this.domainPath = domainPath;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getIssuerUrl() {
return issuerUrl;
}

public void setIssuerUrl(String issuerUrl) {
this.issuerUrl = issuerUrl;
}

public String getAuthorizeUrl() {
return authorizeUrl;
}
Expand Down
Loading