Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

This feature enables CloudStack administrators to configure domain-specific OAuth2 providers. In multi-tenant environments, each domain (representing a customer, department, or team) can have its own OAuth2 provider configuration. This allows organizations to maintain separate OAuth credentials per domain, ensuring users authenticate against the correct identity provider for their team.

Scope:

    Register
  • Allow registration of OAuth providers for specific domains
  • Domain
  • Add domain-level oauth2.enabled setting to control OAuth availability per domain
  • In UI, add
  • new
  • OAuth Login tab
  • for OAuth login (similar to SSO tab
  • (enabled when at least one provider is enabled globally or in a domain)
  • In the
  • new
  • OAuth Login tab, show global providers by default
  • And when domain is entered, show only domain-specific providers; if none exist, indicate with message
  • Error handling - duplicate provider rejection, provider not found, OAuth disabled for domain

UI Mockups: 

Image Added
Implementation:

Step 1: List Providers

ListOAuthProvidersCmd gets called when the user hits the login page. It's sessionless, so it implements APIAuthenticator; the authenticate() hook lets it run without a session. It lists OAuth providers (global and domain-based

...

Global OAuth providers stay visible at all times

...

Error handling - duplicate provider rejection, provider not found, OAuth disabled for domain

), with oauth2.enabled checked per-domain to set the enabled flag on each provider.

Code Block
darora@dell16plus:~$ cmk list oauthprovider
{
  "count": 1,
  "oauthprovider": [
    {
      "clientid": "Ov23liDRnt6bVJcoKwlP",
      "description": "GitHub OAuth for TestOAuthDomain",
      "domain": "TestOAuthDomain",
      "domainid": "fb0bc4d3-9928-42b0-bcd6-1829e09418c3",
      "enabled": true,
      "id": "73977fc9-e1c1-49ff-aebc-cf9b2198066e",
      "name": "github",
      "provider": "github",
      "redirecturi": "http://10.0.33.202:8080/client/#/verifyOauth",
      "secretkey": "ac6252ddaf4c99fb482b9b84c7aac6503cf79908"
    }
  ]
}

Step 2: Redirect to GitHub

User clicks the provider button. Browser redirects to GitHub with the client_id. GitHub authenticates the user and redirects back with a one-time auth code. The secret_key never leaves the backend.

Code Block
Browser                            GitHub
  │                                   │
  │   GET /login/oauth/authorize      │
  │   ?client_id=Ov23liDRnt6bVJ       │
  │   &scope=user:email               │
  │──────────────────────────────────→│
  │                                   │
  │   User logs in, clicks "Allow"	  |
  │                                   │
  │   302 Redirect to:                │
  │   verifyOauth?code=AUTH_CODE      │
  │←──────────────────────────────────│
  ▼
VerifyOauth.vue catches ?code=AUTH_CODE

Step 3: Verify Code and Fetch Email

UI calls verifyOAuthCodeAndGetUser with the auth code, provider, and domain. VerifyOAuthCodeAndGetUserCmd resolves the domain via resolveDomainId(), then passes it to _oauth2mgr.verifyCodeAndFetchEmail() which looks up that domain's OAuth credentials from the DB, exchanges the auth code for an access token at GitHub's token endpoint, fetches the user's email, and returns it to the UI.

Step 4: Login

UI calls oauthlogin with the email, provider, auth code, and domain. OauthLoginAPIAuthenticatorCmd resolves the domain, checks oauth2.enabled for it, finds the CloudStack user by email, then calls loginUser(). On success, records OauthLogin=true and returns a session key. oauth2.enabled is checked in both Step 1 (hides buttons if disabled) and Step 4 (backend safety net against direct API calls).

Code Block
OauthLoginAPIAuthenticatorCmd.authenticate()
  │
  ├── resolves domain
  ├── checks oauth2.enabled for domain
  ├── finds user by email
  │
  ├── _apiServer.loginUser(username, password=null, domainId)
  │         │
  │         ▼
  │   OAuth2UserAuthenticator.authenticate()
  │   ├── checks oauth2.enabled for domain (again)
  │   ├── calls GithubOAuth2Provider.verifyUser(email, code, domainId)
  │   │   ├── looks up domain credentials from DB
  │   │   ├── exchanges code with GitHub
  │   │   └── compares emails
  │   └── returns true → login succeeds
  │
  ├── records OauthLogin=true
  └── returns session key → UI redirects to dashboard

How loginUser() triggers GitHub verification: loginUser() doesn't know about OAuth; it delegates to accountMgr.authenticateUser(), which loops through all registered UserAuthenticator implementations. OAuth2UserAuthenticator sees provider=github and secretcode in the request params, picks it up, looks up GithubOAuth2Provider from the manager's map, and calls verifyUser(email, secretCode, domainId). The original HTTP request params flow through the entire chain as the thread connecting everything.

Code Block
OauthLoginAPIAuthenticatorCmd.authenticate()
  → _apiServer.loginUser()                    
    → accountMgr.authenticateUser()           
      → for (authenticator : _userAuthenticators) 
        → OAuth2UserAuthenticator.authenticate()  
          → GithubOAuth2Provider.verifyUser()     


Step 5: Login Page (External Tab) Behavior

  • On page load, the UI calls listOauthProvider without a domain parameter
  • External tab is enabled if response.listoauthproviderresponse.count > 0
  • Global provider buttons (Google/GitHub) are rendered from the returned list
  • If no global providers exist but domain providers do, the tab is enabled with the message: "Enter your domain to see available providers"
  • When the user enters a domain path in the domain field, the UI calls listOauthProvider with domain=<path>
  • Domain-specific provider buttons are rendered, or "No OAuth providers configured for this domain" is shown

Other Changes

Admin commands — how providers get registered/updated/deleted

  • RegisterOAuthProviderCmd (new domainid param)
  • UpdateOAuthProviderCmd (per-domain enabled check)
  • DeleteOAuthProviderCmd

DB migration — schema changes (domain_id column, FK, unique key)

Config change oauth2.enabled scope from Global to Domain

GitHub/Google provider internals — DAO lookup changed from findByProvider() to findByProviderAndDomainWithGlobalFallback()

API Changes:

registerOAuthProvider

...

Code Block
languagebash
cmk list oauthproviders domainid=<domain-uuid>

...

Code Block
languagebash
cmk list oauthproviders domainid=-1


Database Changes:

Table: oauth_provider:

...

  • domain_id = NULL indicates a global provider
  • Unique constraint on (provider, domain_id) prevents duplicate registrations

Strict Scope for oauth2.enabled

The oauth2.enabled config key uses a new strictScope mode in ConfigKey.valueInScope() to prevent automatic inheritance from global to domain scope. Each domain must explicitly set oauth2.enabled=true to enable OAuth login.

Global oauth2.enabledDomain oauth2.enabledOAuth enabled for domain?
falsenot configuredNo
falsetrueYes
falsefalseNo
truenot configuredNo (no global fallback)
truetrueYes
truefalseNo


Key difference from standard ConfigKey behavior: without strict scope, an unconfigured domain inherits the global value. With strict scope, unconfigured domains return null which is treated as disabled. This ensures domains must explicitly opt in to OAuth.

Implementation Details

  • New overload: ConfigKey.valueInScope(Scope scope, Long id, boolean strictScope)
  • Existing valueInScope(Scope, Long) delegates to new overload with strictScope=false for backward compatibility
  • When strictScope=true and no scope-specific value exists, returns null instead of walking up the parent scope hierarchy
  • All OAuth oauth2.enabled checks use: Boolean.TRUE.equals(OAuth2IsPluginEnabled.valueInScope(ConfigKey.Scope.Domain, domainId, true))