Add sso_server_url configuration for dual URL SSO setups (#31497)

This change allows configuring a separate URL for SSO callbacks, which
is useful when organizations have different URLs for admin access vs
agent/API access.

Fixes #31480 the SSO issue where organizations with dual URL setups were
getting 'Destination does not match requested URL' errors after
upgrading to v4.71.0 with the new SAML library.

Video demo: https://www.youtube.com/watch?v=dFzNpUY3XKI

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests
- [ ] QA'd all new/changed functionality manually

## New Fleet configuration settings

- [x] Verified that the setting is exported via `fleetctl
generate-gitops`
- [x] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
  - Same PR since this is going to be a 4.71.1 patch
- [ ] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [x] Verified that any relevant UI is disabled when GitOps mode is
enabled

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Added support for configuring a dedicated SSO URL, allowing
organizations to restrict SSO authentication to a specific URL.
* The new SSO URL option is available in both the UI and API
configuration settings.

* **Documentation**
* Updated configuration and API documentation to include the new SSO URL
option with usage examples.

* **Bug Fixes**
* Resolved authentication issues for organizations using separate URLs
for admin and agent/API access.

* **Tests**
* Added new unit and integration tests to verify SSO behavior with and
without the dedicated SSO URL.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky 2025-08-01 20:32:15 +02:00 committed by GitHub
parent 497401a75b
commit 949a1eeabb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 238 additions and 9 deletions

View file

@ -0,0 +1 @@
Added `sso_server_url` configuration option to support SSO setups with separate URLs for admin access vs agent/API access. When set, SSO authentication will only work from the specified URL. This fixes SSO authentication errors for organizations using dual URL configurations.

View file

@ -647,6 +647,7 @@ func (cmd *GenerateGitopsCommand) generateSSOSettings(ssoSettings *fleet.SSOSett
jsonFieldName(t, "Metadata"): ssoSettings.Metadata,
jsonFieldName(t, "MetadataURL"): ssoSettings.MetadataURL,
jsonFieldName(t, "EnableSSOIdPLogin"): ssoSettings.EnableSSOIdPLogin,
jsonFieldName(t, "SSOServerURL"): ssoSettings.SSOServerURL,
}
if cmd.AppConfig.License.IsPremium() {
result[jsonFieldName(t, "EnableJITProvisioning")] = ssoSettings.EnableJITProvisioning

View file

@ -2955,6 +2955,55 @@ func TestGitOpsSSOSettings(t *testing.T) {
require.Nil(t, appConfig.SSOSettings)
}
func TestGitOpsSSOServerURL(t *testing.T) {
tmpDir := t.TempDir()
globalFile, err := os.CreateTemp(tmpDir, "*.yml")
require.NoError(t, err)
_, err = globalFile.WriteString(`
controls:
queries:
policies:
agent_options:
org_settings:
server_settings:
server_url: ` + fleetServerURL + `
org_info:
org_name: ` + orgName + `
sso_settings:
entity_id: "test-entity"
idp_name: "Test IdP"
metadata: "<xml>test-metadata</xml>"
enable_sso: true
sso_server_url: "https://sso.example.com"
secrets:
- secret: test-secret
`)
require.NoError(t, err)
globalFile.Close()
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)
appConfig := fleet.AppConfig{}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &appConfig, nil
}
ds.SaveAppConfigFunc = func(ctx context.Context, config *fleet.AppConfig) error {
appConfig = *config
return nil
}
// Run GitOps with SSO settings including sso_url
_, err = RunAppNoChecks([]string{"gitops", "-f", globalFile.Name()})
require.NoError(t, err)
require.NotNil(t, appConfig.SSOSettings)
require.Equal(t, "https://sso.example.com", appConfig.SSOSettings.SSOServerURL)
require.Equal(t, "test-entity", appConfig.SSOSettings.EntityID)
require.True(t, appConfig.SSOSettings.EnableSSO)
}
func TestGitOpsSMTPSettings(t *testing.T) {
globalFileBasic := createGlobalFileBasic(t, fleetServerURL, orgName)
ds, _, _ := testing_utils.SetupFullGitOpsPremiumServer(t)

View file

@ -55,7 +55,8 @@
"enable_jit_provisioning": false,
"enable_jit_role_sync": false,
"enable_sso": false,
"enable_sso_idp_login": false
"enable_sso_idp_login": false,
"sso_server_url": ""
},
"fleet_desktop": {
"transparency_url": "https://fleetdm.com/transparency"

View file

@ -107,6 +107,7 @@ spec:
issuer_uri: ""
metadata: ""
metadata_url: ""
sso_server_url: ""
vulnerability_settings:
databases_path: /some/path
webhook_settings:

View file

@ -112,7 +112,8 @@
"metadata_url": "",
"idp_name": "",
"enable_sso": false,
"enable_sso_idp_login": false
"enable_sso_idp_login": false,
"sso_server_url": ""
},
"fleet_desktop": {
"transparency_url": "https://fleetdm.com/transparency"

View file

@ -147,6 +147,7 @@ spec:
issuer_uri: ""
metadata: ""
metadata_url: ""
sso_server_url: ""
update_interval:
osquery_detail: 1h0m0s
osquery_policy: 1h0m0s

View file

@ -143,7 +143,8 @@
"enable_sso": true,
"enable_sso_idp_login": false,
"enable_jit_provisioning": true,
"enable_jit_role_sync": false
"enable_jit_role_sync": false,
"sso_server_url": "https://sso.fleetdm.com"
},
"fleet_desktop": {
"transparency_url": "https://fleetdm.com/transparency"

View file

@ -101,6 +101,7 @@ sso_settings:
idp_name: some-idp-name
metadata: some-sso-metadata
metadata_url: http://some-sso-metadata-url.com
sso_server_url: https://sso.fleetdm.com
webhook_settings:
activities_webhook:
destination_url: https://some-activities-webhook-url.com

View file

@ -101,6 +101,7 @@ sso_settings:
idp_name: some-idp-name
metadata: ___GITOPS_COMMENT_9___
metadata_url: ___GITOPS_COMMENT_10___
sso_server_url: https://sso.fleetdm.com
webhook_settings:
activities_webhook:
destination_url: https://some-activities-webhook-url.com

View file

@ -136,6 +136,7 @@ org_settings:
idp_name: some-idp-name
metadata: # TODO: Add your SSO metadata here
metadata_url: # TODO: Add your SSO metadata URL here
sso_server_url: https://sso.fleetdm.com
webhook_settings:
activities_webhook:
destination_url: https://some-activities-webhook-url.com

View file

@ -135,6 +135,7 @@ org_settings:
idp_name: some-idp-name
metadata: # TODO: Add your SSO metadata here
metadata_url: # TODO: Add your SSO metadata URL here
sso_server_url: https://sso.fleetdm.com
webhook_settings:
activities_webhook:
destination_url: https://some-activities-webhook-url.com

View file

@ -107,6 +107,7 @@ spec:
issuer_uri: ""
metadata: ""
metadata_url: ""
sso_server_url: ""
vulnerability_settings:
databases_path: ""
webhook_settings:

View file

@ -107,6 +107,7 @@ spec:
issuer_uri: ""
metadata: ""
metadata_url: ""
sso_server_url: ""
vulnerability_settings:
databases_path: ""
webhook_settings:

View file

@ -664,6 +664,7 @@ org_settings:
metadata: $SSO_METADATA
enable_jit_provisioning: true # Available in Fleet Premium
enable_sso_idp_login: true
sso_server_url: https://admin.example.com # Optional, SSO will only work from this URL
```
### integrations

View file

@ -802,7 +802,8 @@ None.
"idp_name": "",
"enable_sso": false,
"enable_sso_idp_login": false,
"enable_jit_provisioning": false
"enable_jit_provisioning": false,
"sso_server_url": ""
},
"conditional_access": {
"microsoft_entra_tenant_id": "<TENANT ID>",
@ -1443,7 +1444,8 @@ Modifies the Fleet's configuration with the supplied information.
"metadata": "",
"idp_name": "",
"enable_sso_idp_login": false,
"enable_jit_provisioning": false
"enable_jit_provisioning": false,
"sso_server_url": ""
}
}
```

View file

@ -147,6 +147,7 @@ export interface IConfig {
enable_sso_idp_login: boolean;
enable_jit_provisioning: boolean;
enable_jit_role_sync: boolean;
sso_server_url?: string;
};
// configuration details for conditional access. For enabled/disabled status per team, see
// subfields under `integrations`

View file

@ -18,6 +18,7 @@ import { IAppConfigFormProps, IFormField } from "../constants";
const baseClass = "app-config-form";
interface IAdvancedConfigFormData {
ssoUserURL: string;
mdmAppleServerURL: string;
domain: string;
verifySSLCerts: boolean;
@ -33,12 +34,14 @@ interface IAdvancedConfigFormData {
}
interface IAdvancedConfigFormErrors {
ssoUserURL?: string | null;
mdmAppleServerURL?: string | null;
domain?: string | null;
hostExpiryWindow?: string | null;
}
const validateFormData = ({
ssoUserURL,
mdmAppleServerURL,
domain,
hostExpiryWindow,
@ -46,6 +49,12 @@ const validateFormData = ({
}: IAdvancedConfigFormData) => {
const errors: Record<string, string> = {};
if (!ssoUserURL) {
delete errors.ssoUserURL;
} else if (!validUrl({ url: ssoUserURL })) {
errors.ssoUserURL = `${ssoUserURL} is not a valid URL`;
}
if (!mdmAppleServerURL) {
delete errors.mdmAppleServerURL;
} else if (!validUrl({ url: mdmAppleServerURL })) {
@ -75,6 +84,7 @@ const Advanced = ({
const gitOpsModeEnabled = appConfig.gitops.gitops_mode_enabled;
const [formData, setFormData] = useState<IAdvancedConfigFormData>({
ssoUserURL: appConfig.sso_settings?.sso_server_url || "",
mdmAppleServerURL: appConfig.mdm?.apple_server_url || "",
domain: appConfig.smtp_settings?.domain || "",
verifySSLCerts: appConfig.smtp_settings?.verify_ssl_certs || false,
@ -97,6 +107,7 @@ const Advanced = ({
});
const {
ssoUserURL,
mdmAppleServerURL,
domain,
verifySSLCerts,
@ -179,6 +190,9 @@ const Advanced = ({
mdm: {
apple_server_url: mdmAppleServerURL,
},
sso_settings: {
sso_server_url: ssoUserURL,
},
};
handleSubmit(formDataToSubmit);
@ -193,6 +207,25 @@ const Advanced = ({
Most users do not need to modify these options.
</p>
<div className="form">
<GitOpsModeTooltipWrapper
position="left"
renderChildren={(disableChildren) => (
<InputField
disabled={disableChildren}
label="SSO user URL"
onChange={onInputChange}
onBlur={onInputBlur}
name="ssoUserURL"
value={ssoUserURL}
parseTarget
error={formErrors.ssoUserURL}
tooltip={
!disableChildren &&
"Update this URL if you want your Fleet users (admins, maintainers, observers) to login via SSO using a URL that's different than the base URL of your Fleet instance. If not configured, login via SSO will use the base URL of the Fleet instance."
}
/>
)}
/>
{appConfig.mdm.enabled_and_configured && (
<GitOpsModeTooltipWrapper
position="left"

View file

@ -96,7 +96,7 @@ CREATE TABLE `app_config_json` (
PRIMARY KEY (`id`)
) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"manual_agent_install\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"windows_require_bitlocker_pin\": null, \"android_enabled_and_configured\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"gitops\": {\"repository_url\": \"\", \"gitops_mode_enabled\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null, \"custom_scep_proxy\": null, \"conditional_access_enabled\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
INSERT INTO `app_config_json` VALUES (1,'{\"mdm\": {\"ios_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_setup\": {\"script\": null, \"software\": null, \"bootstrap_package\": null, \"manual_agent_install\": null, \"macos_setup_assistant\": null, \"enable_end_user_authentication\": false, \"enable_release_device_manually\": false}, \"macos_updates\": {\"deadline\": null, \"minimum_version\": null}, \"ipados_updates\": {\"deadline\": null, \"minimum_version\": null}, \"macos_settings\": {\"custom_settings\": null}, \"macos_migration\": {\"mode\": \"\", \"enable\": false, \"webhook_url\": \"\"}, \"windows_updates\": {\"deadline_days\": null, \"grace_period_days\": null}, \"apple_server_url\": \"\", \"windows_settings\": {\"custom_settings\": null}, \"apple_bm_terms_expired\": false, \"apple_business_manager\": null, \"enable_disk_encryption\": false, \"enabled_and_configured\": false, \"end_user_authentication\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"issuer_uri\": \"\", \"metadata_url\": \"\"}, \"volume_purchasing_program\": null, \"windows_migration_enabled\": false, \"windows_require_bitlocker_pin\": null, \"android_enabled_and_configured\": false, \"windows_enabled_and_configured\": false, \"apple_bm_enabled_and_configured\": false}, \"gitops\": {\"repository_url\": \"\", \"gitops_mode_enabled\": false}, \"scripts\": null, \"features\": {\"enable_host_users\": true, \"enable_software_inventory\": false}, \"org_info\": {\"org_name\": \"\", \"contact_url\": \"\", \"org_logo_url\": \"\", \"org_logo_url_light_background\": \"\"}, \"integrations\": {\"jira\": null, \"zendesk\": null, \"digicert\": null, \"google_calendar\": null, \"ndes_scep_proxy\": null, \"custom_scep_proxy\": null, \"conditional_access_enabled\": null}, \"sso_settings\": {\"idp_name\": \"\", \"metadata\": \"\", \"entity_id\": \"\", \"enable_sso\": false, \"issuer_uri\": \"\", \"metadata_url\": \"\", \"idp_image_url\": \"\", \"sso_server_url\": \"\", \"enable_jit_role_sync\": false, \"enable_sso_idp_login\": false, \"enable_jit_provisioning\": false}, \"agent_options\": {\"config\": {\"options\": {\"logger_plugin\": \"tls\", \"pack_delimiter\": \"/\", \"logger_tls_period\": 10, \"distributed_plugin\": \"tls\", \"disable_distributed\": false, \"logger_tls_endpoint\": \"/api/osquery/log\", \"distributed_interval\": 10, \"distributed_tls_max_attempts\": 3}, \"decorators\": {\"load\": [\"SELECT uuid AS host_uuid FROM system_info;\", \"SELECT hostname AS hostname FROM system_info;\"]}}, \"overrides\": {}}, \"fleet_desktop\": {\"transparency_url\": \"\"}, \"smtp_settings\": {\"port\": 587, \"domain\": \"\", \"server\": \"\", \"password\": \"\", \"user_name\": \"\", \"configured\": false, \"enable_smtp\": false, \"enable_ssl_tls\": true, \"sender_address\": \"\", \"enable_start_tls\": true, \"verify_ssl_certs\": true, \"authentication_type\": \"0\", \"authentication_method\": \"0\"}, \"server_settings\": {\"server_url\": \"\", \"enable_analytics\": false, \"query_report_cap\": 0, \"scripts_disabled\": false, \"deferred_save_host\": false, \"live_query_disabled\": false, \"ai_features_disabled\": false, \"query_reports_disabled\": false}, \"webhook_settings\": {\"interval\": \"0s\", \"activities_webhook\": {\"destination_url\": \"\", \"enable_activities_webhook\": false}, \"host_status_webhook\": {\"days_count\": 0, \"destination_url\": \"\", \"host_percentage\": 0, \"enable_host_status_webhook\": false}, \"vulnerabilities_webhook\": {\"destination_url\": \"\", \"host_batch_size\": 0, \"enable_vulnerabilities_webhook\": false}, \"failing_policies_webhook\": {\"policy_ids\": null, \"destination_url\": \"\", \"host_batch_size\": 0, \"enable_failing_policies_webhook\": false}}, \"host_expiry_settings\": {\"host_expiry_window\": 0, \"host_expiry_enabled\": false}, \"vulnerability_settings\": {\"databases_path\": \"\"}, \"activity_expiry_settings\": {\"activity_expiry_window\": 0, \"activity_expiry_enabled\": false}}','2020-01-01 01:01:01','2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `batch_script_execution_host_results` (

View file

@ -78,6 +78,10 @@ type SSOSettings struct {
// EnableJITRoleSync sets whether the roles of existing accounts will be updated
// every time SSO users log in (does not have effect if EnableJITProvisioning is false).
EnableJITRoleSync bool `json:"enable_jit_role_sync"`
// SSOServerURL is an optional URL to use for SSO authentication.
// When set, SSO will only work from this URL, not from the server URL.
// This is useful for organizations with separate URLs for admin access vs agent/API access.
SSOServerURL string `json:"sso_server_url"`
}
// ConditionalAccessSettings holds the global settings for the "Conditional access" feature.

View file

@ -528,3 +528,59 @@ func (s *integrationSSOTestSuite) TestSSOLoginSAMLResponseTampered() {
require.NoError(t, err)
require.Contains(t, string(body), "/login?status=error")
}
func (s *integrationSSOTestSuite) TestSSOServerURL() {
t := s.T()
// Use the test metadata instead of trying to fetch from localhost:9080
testMetadata := `
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="http://localhost:9080/simplesaml/saml2/idp/metadata.php">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:9080/simplesaml/saml2/idp/SingleLogoutService.php"/>
<md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="http://localhost:9080/simplesaml/saml2/idp/SSOService.php"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>`
// Configure SSO with a specific SSO server URL and inline metadata
acResp := appConfigResponse{}
s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
"sso_settings": {
"enable_sso": true,
"entity_id": "https://localhost:8080",
"idp_name": "SimpleSAML",
"metadata": %q,
"enable_jit_provisioning": false,
"sso_server_url": "https://admin.localhost:8080"
}
}`, testMetadata)), http.StatusOK, &acResp)
require.NotNil(t, acResp)
// Verify the SSO server URL is set
require.NotNil(t, acResp.SSOSettings)
require.Equal(t, "https://admin.localhost:8080", acResp.SSOSettings.SSOServerURL)
// Initiate SSO
var resIni initiateSSOResponse
s.DoJSON("POST", "/api/v1/fleet/sso", map[string]string{}, http.StatusOK, &resIni)
require.NotEmpty(t, resIni.URL)
// Parse the auth request to verify it uses the SSO URL
parsed, err := url.Parse(resIni.URL)
require.NoError(t, err)
q := parsed.Query()
encoded := q.Get("SAMLRequest")
assert.NotEmpty(t, encoded)
authReq := inflate(t, encoded)
// Check that the ACS URL in the auth request uses the SSO server URL
require.NotNil(t, authReq.AssertionConsumerServiceURL)
assert.Equal(t, "https://admin.localhost:8080/api/v1/fleet/sso/callback", authReq.AssertionConsumerServiceURL)
}

View file

@ -441,7 +441,12 @@ func (svc *Service) InitiateSSO(ctx context.Context, redirectURL string) (sessio
}
serverURL := appConfig.ServerSettings.ServerURL
acsURL := serverURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback"
// Use SSO server URL if configured, otherwise use the server URL
ssoURL := serverURL
if appConfig.SSOSettings != nil && appConfig.SSOSettings.SSOServerURL != "" {
ssoURL = appConfig.SSOSettings.SSOServerURL
}
acsURL := ssoURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback"
// If entityID is not explicitly set, default to host name.
//
@ -650,7 +655,12 @@ func (svc *Service) InitSSOCallback(
}
serverURL := appConfig.ServerSettings.ServerURL
acsURL, err := url.Parse(serverURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback")
// Use SSO server URL if configured, otherwise use the server URL
ssoURL := serverURL
if appConfig.SSOSettings != nil && appConfig.SSOSettings.SSOServerURL != "" {
ssoURL = appConfig.SSOSettings.SSOServerURL
}
acsURL, err := url.Parse(ssoURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback")
if err != nil {
return nil, "", ctxerr.Wrap(ctx, err, "failed to parse ACS URL")
}
@ -658,7 +668,14 @@ func (svc *Service) InitSSOCallback(
expectedAudiences := []string{
appConfig.SSOSettings.EntityID,
appConfig.ServerSettings.ServerURL,
appConfig.ServerSettings.ServerURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback", // ACS
appConfig.ServerSettings.ServerURL + svc.config.Server.URLPrefix + "/api/v1/fleet/sso/callback", // ACS with server URL
}
// Add SSO server URL to expected audiences if configured
if appConfig.SSOSettings != nil && appConfig.SSOSettings.SSOServerURL != "" {
expectedAudiences = append(expectedAudiences,
appConfig.SSOSettings.SSOServerURL,
appConfig.SSOSettings.SSOServerURL+svc.config.Server.URLPrefix+"/api/v1/fleet/sso/callback", // ACS with SSO server URL
)
}
samlProvider, requestID, redirectURL, err := sso.SAMLProviderFromSessionOrConfiguredMetadata(
ctx, sessionID, svc.ssoSessionStore, acsURL, appConfig.SSOSettings, expectedAudiences,

View file

@ -8,6 +8,7 @@ import (
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
@ -436,3 +437,54 @@ func TestGetSSOUser(t *testing.T) {
_, err = svc.GetSSOUser(ctx, auth)
require.Error(t, err)
}
func TestInitiateSSOWithSSOServerURL(t *testing.T) {
ds := new(mock.Store)
pool := redistest.SetupRedis(t, t.Name(), false, false, false)
svc, ctx := newTestServiceWithConfig(t, ds, config.TestConfig(), nil, nil, &TestServerOpts{
Pool: pool,
})
// Mock app config with SSO server URL
appConfig := &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://fleet.example.com",
},
SSOSettings: &fleet.SSOSettings{
EnableSSO: true,
SSOServerURL: "https://admin.fleet.example.com",
SSOProviderSettings: fleet.SSOProviderSettings{
EntityID: "fleet",
IDPName: "TestIDP",
Metadata: `<?xml version="1.0"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="test-idp">
<md:IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:KeyDescriptor use="signing">
<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:X509Data>
<ds:X509Certificate>MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
<md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.example.com/sso"/>
</md:IDPSSODescriptor>
</md:EntityDescriptor>`,
},
},
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return appConfig, nil
}
// Test that ACS URL uses SSO URL
sessionID, _, idpURL, err := svc.InitiateSSO(ctx, "/dashboard")
require.NoError(t, err)
require.NotEmpty(t, sessionID)
require.NotEmpty(t, idpURL)
// The ACS URL should use the SSO server URL
// We can't directly test the ACS URL in the SAML request here since it's embedded in the XML,
// but the integration test verifies this works correctly
}

View file

@ -53,6 +53,7 @@ github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableSSO bool
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableSSOIdPLogin bool
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableJITProvisioning bool
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings EnableJITRoleSync bool
github.com/fleetdm/fleet/v4/server/fleet/SSOSettings SSOServerURL string
github.com/fleetdm/fleet/v4/server/fleet/AppConfig FleetDesktop fleet.FleetDesktopSettings
github.com/fleetdm/fleet/v4/server/fleet/FleetDesktopSettings TransparencyURL string
github.com/fleetdm/fleet/v4/server/fleet/AppConfig VulnerabilitySettings fleet.VulnerabilitySettings