Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dfc45c0
Add domain_id to oauth_provider table and VO
Dec 18, 2025
fcb4778
Add domain-aware methods to OauthProviderDao
Dec 18, 2025
85c64e5
Add domainId parameter to OAuth provider API commands and response
Dec 18, 2025
c46fb98
Add domain support to OAuth2AuthManager
Dec 18, 2025
4ef100c
Add domain-aware OAuth verification
Dec 18, 2025
8e48938
Add domain support to ListOAuthProvidersCmd and update related tests
Dec 18, 2025
9ab26db
fix domain path issue
Dec 18, 2025
3e7fa92
Add domainId support to OAuth provider
Dec 18, 2025
4a6b28e
Return domain name and UUID in OAuth provider API responses using Api…
Damans227 Feb 24, 2026
3ab3ca9
Refactor domain ID resolution in VerifyOAuthCodeAndGetUserCmd to impr…
Damans227 Feb 24, 2026
bb6f137
Enhance OAuth2 plugin support for domain-level configuration and auth…
Damans227 Feb 24, 2026
764495b
Update OAuth2 tests and VerifyOAuthCodeAndGetUserCmdTest
Damans227 Feb 24, 2026
0ec6bcc
Add method to find OAuth provider by domain with global fallback
Damans227 Feb 24, 2026
9609ef0
Update OAuth provider configuration to use 'domain' instead of 'domai…
Damans227 Feb 24, 2026
65ccef1
Refactor OAuth provider methods to support domain-level queries and e…
Damans227 Feb 24, 2026
e3da679
Add caching for access token retrieval in GithubOAuth2Provider
Damans227 Feb 24, 2026
0e32a60
Refactor access token checks in GithubOAuth2Provider to use StringUti…
Damans227 Feb 24, 2026
23c4132
Refactor null checks to use utility for improved readability and cons…
Damans227 Feb 25, 2026
1a23f3a
Update OAuth2UserAuthenticatorTest to include domainId in user verifi…
Damans227 Feb 25, 2026
b8cf181
Merge branch 'main' into oauth-per-domain
Damans227 Feb 25, 2026
6d73964
Remove unnecessary blank line and unused imports in OAuth provider co…
Damans227 Feb 25, 2026
a428d1a
Refactor and cleanup
Damans227 Feb 25, 2026
b367434
Remove unnecessary blank lines
Damans227 Feb 25, 2026
42a8651
Enhance RegisterOAuthProviderCmdTest with additional provider mock data
Damans227 Feb 25, 2026
7a55ecb
Remove startup gate from OAuth plugin initialization to support dynam…
Damans227 Feb 25, 2026
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 @@ -44,6 +44,17 @@ public interface UserOAuth2Authenticator extends Adapter {
*/
String verifyCodeAndFetchEmail(String secretCode);

/**
* Verifies if the logged in user is valid for a specific domain
* @return returns true if its valid user
*/
boolean verifyUser(String email, String secretCode, Long domainId);

/**
* Verifies the code provided by provider and fetches email for a specific domain
* @return returns email
*/
String verifyCodeAndFetchEmail(String secretCode, Long domainId);

/**
* Fetches email using the accessToken
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
-- Schema upgrade from 4.22.1.0 to 4.23.0.0
--;

ALTER TABLE `cloud`.`oauth_provider` ADD COLUMN `domain_id` bigint unsigned DEFAULT NULL COMMENT 'NULL for global provider, domain ID for domain-specific' AFTER `redirect_uri`;
ALTER TABLE `cloud`.`oauth_provider` ADD CONSTRAINT `fk_oauth_provider__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`);
ALTER TABLE `cloud`.`oauth_provider` ADD INDEX `i_oauth_provider__domain_id`(`domain_id`);

ALTER TABLE `cloud`.`oauth_provider` ADD UNIQUE KEY `uk_oauth_provider__provider_domain` (`provider`, `domain_id`);

CREATE TABLE `cloud`.`backup_offering_details` (
`id` bigint unsigned NOT NULL auto_increment,
`backup_offering_id` bigint unsigned NOT NULL COMMENT 'Backup offering id',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

public interface OAuth2AuthManager extends PluggableAPIAuthenticator, PluggableService {
public static ConfigKey<Boolean> OAuth2IsPluginEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "oauth2.enabled", "false",
"Indicates whether OAuth plugin is enabled or not", false);
"Indicates whether OAuth plugin is enabled or not. Can be configured at domain level.", true, ConfigKey.Scope.Domain);
public static final ConfigKey<String> OAuth2Plugins = new ConfigKey<String>("Advanced", String.class, "oauth2.plugins", "google,github",
"List of OAuth plugins", true);
public static final ConfigKey<String> OAuth2PluginsExclude = new ConfigKey<String>("Advanced", String.class, "oauth2.plugins.exclude", "",
Expand All @@ -49,11 +49,11 @@ public interface OAuth2AuthManager extends PluggableAPIAuthenticator, PluggableS
*/
UserOAuth2Authenticator getUserOAuth2AuthenticationProvider(final String providerName);

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

OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd);

List<OauthProviderVO> listOauthProviders(String provider, String uuid);
List<OauthProviderVO> listOauthProviders(String provider, String uuid, Long domainId);

boolean deleteOauthProvider(Long id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,13 @@ public List<Class<?>> getAuthCommands() {

@Override
public boolean start() {
if (isOAuthPluginEnabled()) {
logger.info("OAUTH plugin loaded");
initializeUserOAuth2AuthenticationProvidersMap();
} else {
logger.info("OAUTH plugin not enabled so not loading");
}
initializeUserOAuth2AuthenticationProvidersMap();
logger.info("OAUTH plugin loaded");
return true;
}

protected boolean isOAuthPluginEnabled() {
return OAuth2IsPluginEnabled.value();
protected boolean isOAuthPluginEnabled(Long domainId) {
return OAuth2IsPluginEnabled.valueIn(domainId);
}

@Override
Expand Down Expand Up @@ -125,9 +121,9 @@ protected void initializeUserOAuth2AuthenticationProvidersMap() {
}

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

return email;
}
Expand All @@ -139,25 +135,36 @@ public OauthProviderVO registerOauthProvider(RegisterOAuthProviderCmd cmd) {
String clientId = StringUtils.trim(cmd.getClientId());
String redirectUri = StringUtils.trim(cmd.getRedirectUri());
String secretKey = StringUtils.trim(cmd.getSecretKey());
Long domainId = cmd.getDomainId();

if (!isOAuthPluginEnabled()) {
if (!isOAuthPluginEnabled(domainId)) {
throw new CloudRuntimeException("OAuth is not enabled, please enable to register");
}
OauthProviderVO providerVO = _oauthProviderDao.findByProvider(provider);

// Check for existing provider with same name and domain
OauthProviderVO providerVO = _oauthProviderDao.findByProviderAndDomain(provider, domainId);
if (providerVO != null) {
throw new CloudRuntimeException(String.format("Provider with the name %s is already registered", provider));
if (domainId == null) {
throw new CloudRuntimeException(String.format("Global provider with the name %s is already registered", provider));
} else {
throw new CloudRuntimeException(String.format("Provider with the name %s is already registered for domain %d", provider, domainId));
}
}

return saveOauthProvider(provider, description, clientId, secretKey, redirectUri);
return saveOauthProvider(provider, description, clientId, secretKey, redirectUri, domainId);
}

@Override
public List<OauthProviderVO> listOauthProviders(String provider, String uuid) {
public List<OauthProviderVO> listOauthProviders(String provider, String uuid, Long domainId) {
List<OauthProviderVO> providers;
if (uuid != null) {
providers = Collections.singletonList(_oauthProviderDao.findByUuid(uuid));
} else if (StringUtils.isNotBlank(provider) && domainId != null) {
providers = Collections.singletonList(_oauthProviderDao.findByProviderAndDomain(provider, domainId));
} else if (StringUtils.isNotBlank(provider)) {
providers = Collections.singletonList(_oauthProviderDao.findByProvider(provider));
providers = Collections.singletonList(_oauthProviderDao.findByProviderAndDomain(provider, null));
} else if (domainId != null) {
providers = _oauthProviderDao.listByDomainIncludingGlobal(domainId);
} else {
providers = _oauthProviderDao.listAll();
}
Expand Down Expand Up @@ -199,14 +206,15 @@ 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, Long domainId) {
final OauthProviderVO oauthProviderVO = new OauthProviderVO();

oauthProviderVO.setProvider(provider);
oauthProviderVO.setDescription(description);
oauthProviderVO.setClientId(clientId);
oauthProviderVO.setSecretKey(secretKey);
oauthProviderVO.setRedirectUri(redirectUri);
oauthProviderVO.setDomainId(domainId);
oauthProviderVO.setEnabled(true);

_oauthProviderDao.persist(oauthProviderVO);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import javax.inject.Inject;
import java.util.Map;
import java.util.Objects;

import static org.apache.cloudstack.oauth2.OAuth2AuthManager.OAuth2IsPluginEnabled;

Expand All @@ -49,7 +50,7 @@ public Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username,
logger.debug("Trying OAuth2 auth for user: " + username);
}

if (!isOAuthPluginEnabled()) {
if (!isOAuthPluginEnabled(domainId)) {
logger.debug("OAuth2 plugin is disabled");
return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
} else if (requestParameters == null) {
Expand All @@ -76,7 +77,7 @@ public Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username,
String secretCode = ((secretCodeArray == null) ? null : secretCodeArray[0]);

UserOAuth2Authenticator authenticator = userOAuth2mgr.getUserOAuth2AuthenticationProvider(oauthProvider);
if (user != null && authenticator.verifyUser(email, secretCode)) {
if (Objects.nonNull(user) && authenticator.verifyUser(email, secretCode, domainId)) {
return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
}
}
Expand All @@ -89,7 +90,7 @@ public String encode(String password) {
return null;
}

protected boolean isOAuthPluginEnabled() {
return OAuth2IsPluginEnabled.value();
protected boolean isOAuthPluginEnabled(Long domainId) {
return OAuth2IsPluginEnabled.valueIn(domainId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.api.ApiDBUtils;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
import com.cloud.user.Account;
import com.cloud.utils.component.ComponentContext;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
Expand All @@ -33,6 +38,7 @@
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.auth.UserOAuth2Authenticator;
import org.apache.cloudstack.oauth2.OAuth2AuthManager;
Expand All @@ -59,6 +65,10 @@ public class ListOAuthProvidersCmd extends BaseListCmd implements APIAuthenticat
@Parameter(name = ApiConstants.PROVIDER, type = CommandType.STRING, description = "Name of the provider")
private String provider;

@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class,
description = "List OAuth providers for a specific domain. Use -1 for global providers only.")
private Long domainId;

/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
Expand All @@ -70,12 +80,18 @@ public String getProvider() {
return provider;
}

public Long getDomainId() {
return domainId;
}

/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

OAuth2AuthManager _oauth2mgr;

DomainDao _domainDao;

@Override
public long getEntityOwnerId() {
return Account.Type.NORMAL.ordinal();
Expand All @@ -97,8 +113,20 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
if (ArrayUtils.isNotEmpty(providerArray)) {
provider = providerArray[0];
}
final String[] domainIdArray = (String[])params.get(ApiConstants.DOMAIN_ID);
if (ArrayUtils.isNotEmpty(domainIdArray)) {
String domainUuid = domainIdArray[0];
if ("-1".equals(domainUuid)) {
domainId = -1L; // Special case for global-only filter
} else {
Domain domain = _domainDao.findByUuid(domainUuid);
if (Objects.nonNull(domain)) {
domainId = domain.getId();
}
}
}

List<OauthProviderVO> resultList = _oauth2mgr.listOauthProviders(provider, id);
List<OauthProviderVO> resultList = _oauth2mgr.listOauthProviders(provider, id, domainId);
List<UserOAuth2Authenticator> userOAuth2AuthenticatorPlugins = _oauth2mgr.listUserOAuth2AuthenticationProviders();
List<String> authenticatorPluginNames = new ArrayList<>();
for (UserOAuth2Authenticator authenticator : userOAuth2AuthenticatorPlugins) {
Expand All @@ -107,9 +135,10 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
}
List<OauthProviderResponse> responses = new ArrayList<>();
for (OauthProviderVO result : resultList) {
Domain domain = result.getDomainId() != null ? ApiDBUtils.findDomainById(result.getDomainId()) : null;
OauthProviderResponse r = new OauthProviderResponse(result.getUuid(), result.getProvider(),
result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri());
if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
result.getDescription(), result.getClientId(), result.getSecretKey(), result.getRedirectUri(), domain);
if (OAuth2AuthManager.OAuth2IsPluginEnabled.valueIn(result.getDomainId()) && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
r.setEnabled(true);
} else {
r.setEnabled(false);
Expand Down Expand Up @@ -141,5 +170,9 @@ public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
if (_oauth2mgr == null) {
logger.error("No suitable Pluggable Authentication Manager found for listing OAuth providers");
}
_domainDao = (DomainDao) ComponentContext.getComponent(DomainDao.class);
if (Objects.isNull(_domainDao)) {
logger.error("Could not get DomainDao component");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// under the License.
package org.apache.cloudstack.oauth2.api.command;

import java.util.Objects;

import com.cloud.api.ApiServlet;
import com.cloud.domain.Domain;
import com.cloud.user.User;
Expand Down Expand Up @@ -120,9 +122,6 @@ public void execute() throws ServerApiException {

@Override
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
if (!OAuth2IsPluginEnabled.value()) {
throw new CloudAuthenticationException("OAuth is not enabled in CloudStack, users cannot login using OAuth");
}
final String[] provider = (String[])params.get(ApiConstants.PROVIDER);
final String[] emailArray = (String[])params.get(ApiConstants.EMAIL);
final String[] secretCodeArray = (String[])params.get(ApiConstants.SECRET_CODE);
Expand All @@ -138,6 +137,15 @@ public String authenticate(String command, Map<String, Object[]> params, HttpSes
final String[] domainName = (String[])params.get(ApiConstants.DOMAIN);
String domain = getDomainName(auditTrailSb, domainName);

final Domain userDomain = _domainService.findDomainByIdOrPath(domainId, domain);
if (Objects.nonNull(userDomain)) {
domainId = userDomain.getId();
}

if (!OAuth2IsPluginEnabled.valueIn(domainId)) {
throw new CloudAuthenticationException("OAuth is not enabled, users cannot login using OAuth");
}

return doOauthAuthentication(session, domainId, domain, email, params, remoteAddress, responseType, auditTrailSb);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.context.CallContext;

import com.cloud.api.ApiDBUtils;
import com.cloud.domain.Domain;
import com.cloud.exception.ConcurrentOperationException;

import java.util.Collection;
Expand Down Expand Up @@ -56,6 +59,10 @@ 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.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class,
description = "Domain ID for domain-specific OAuth provider. If not provided, registers as global provider")
private Long domainId;

@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 @@ -85,6 +92,10 @@ public String getRedirectUri() {
return redirectUri;
}

public Long getDomainId() {
return domainId;
}

public Map getDetails() {
if (MapUtils.isEmpty(details)) {
return null;
Expand All @@ -100,8 +111,9 @@ public Map getDetails() {
public void execute() throws ServerApiException, ConcurrentOperationException, EntityExistsException {
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.getDescription(), provider.getClientId(), provider.getSecretKey(), provider.getRedirectUri(), domain);
response.setResponseName(getCommandName());
response.setObjectName(ApiConstants.OAUTH_PROVIDER);
setResponseObject(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// under the License.
package org.apache.cloudstack.oauth2.api.command;

import com.cloud.api.ApiDBUtils;
import com.cloud.domain.Domain;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.auth.UserOAuth2Authenticator;
import org.apache.cloudstack.oauth2.OAuth2AuthManager;
Expand Down Expand Up @@ -114,16 +116,17 @@ public ApiCommandResourceType getApiResourceType() {
public void execute() {
OauthProviderVO result = _oauthMgr.updateOauthProvider(this);
if (result != null) {
Domain domain = result.getDomainId() != null ? ApiDBUtils.findDomainById(result.getDomainId()) : 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(), domain);

List<UserOAuth2Authenticator> userOAuth2AuthenticatorPlugins = _oauthMgr.listUserOAuth2AuthenticationProviders();
List<String> authenticatorPluginNames = new ArrayList<>();
for (UserOAuth2Authenticator authenticator : userOAuth2AuthenticatorPlugins) {
String name = authenticator.getName();
authenticatorPluginNames.add(name);
}
if (OAuth2AuthManager.OAuth2IsPluginEnabled.value() && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
if (OAuth2AuthManager.OAuth2IsPluginEnabled.valueIn(result.getDomainId()) && authenticatorPluginNames.contains(result.getProvider()) && result.isEnabled()) {
r.setEnabled(true);
} else {
r.setEnabled(false);
Expand Down
Loading
Loading