diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 0268b7747..e511462e2 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -176,7 +176,13 @@ public function boot(IBootContext $context): void { } $multipleUserBackEnds = $samlSettings->allowMultipleUserBackEnds(); - $configuredIdps = $samlSettings->getListOfIdps(); + $configuredIdps = $samlSettings->getListOfConfiguredIdps(); + // If no IdP has the minimum required config (entityId + SSO URL), fall through to normal login + // only for regular SAML mode. Environment-variable mode can still redirect to SAMLController::login() + // without requiring configured IdP metadata. + if ($type === 'saml' && empty($configuredIdps)) { + return; + } $showLoginOptions = $type !== 'environment-variable' && ($multipleUserBackEnds || count($configuredIdps) > 1); if ($redirectSituation === true && $showLoginOptions) { diff --git a/lib/SAMLSettings.php b/lib/SAMLSettings.php index 8f409bfd7..e09e8e952 100644 --- a/lib/SAMLSettings.php +++ b/lib/SAMLSettings.php @@ -100,6 +100,28 @@ public function getListOfIdps(): array { return $result; } + /** + * Get list of IDPs that have the minimum required configuration (entityId + SSO URL). + * Used to avoid blocking login when SAML is enabled but not yet configured. + * + * @return array + * @throws Exception + */ + public function getListOfConfiguredIdps(): array { + $this->ensureConfigurationsLoaded(); + + $result = []; + foreach ($this->configurations as $configID => $config) { + $entityId = trim((string)($config['idp-entityId'] ?? '')); + $ssoUrl = trim((string)($config['idp-singleSignOnService.url'] ?? '')); + if ($entityId !== '' && $ssoUrl !== '') { + $result[$configID] = $config['general-idp0_display_name'] ?? ''; + } + } + + return $result; + } + /** * Check if multiple user back ends are allowed */ diff --git a/tests/unit/SAMLSettingsTest.php b/tests/unit/SAMLSettingsTest.php new file mode 100644 index 000000000..08841499f --- /dev/null +++ b/tests/unit/SAMLSettingsTest.php @@ -0,0 +1,141 @@ +urlGenerator = $this->createMock(IURLGenerator::class); + $this->config = $this->createMock(IConfig::class); + $this->session = $this->createMock(ISession::class); + $this->mapper = $this->createMock(ConfigurationsMapper::class); + + $this->samlSettings = new SAMLSettings( + $this->urlGenerator, + $this->config, + $this->session, + $this->mapper, + ); + } + + public function testGetListOfConfiguredIdpsReturnsEmptyWhenNoIdpsExist(): void { + $this->mapper->expects($this->once()) + ->method('getAll') + ->willReturn([]); + + $result = $this->samlSettings->getListOfConfiguredIdps(); + + $this->assertSame([], $result); + } + + public function testGetListOfConfiguredIdpsReturnsEmptyWhenEntityIdAndSsoUrlMissing(): void { + $this->mapper->expects($this->once()) + ->method('getAll') + ->willReturn([ + 1 => [ + 'general-idp0_display_name' => 'My IdP', + // no idp-entityId, no idp-singleSignOnService.url + ], + ]); + + $result = $this->samlSettings->getListOfConfiguredIdps(); + + $this->assertSame([], $result); + } + + public function testGetListOfConfiguredIdpsReturnsEmptyWhenValuesAreOnlyWhitespace(): void { + $this->mapper->expects($this->once()) + ->method('getAll') + ->willReturn([ + 1 => [ + 'general-idp0_display_name' => 'My IdP', + 'idp-entityId' => ' ', + 'idp-singleSignOnService.url' => "\t", + ], + ]); + + $result = $this->samlSettings->getListOfConfiguredIdps(); + + $this->assertSame([], $result); + } + + public function testGetListOfConfiguredIdpsReturnsIdpWhenFullyConfigured(): void { + $this->mapper->expects($this->once()) + ->method('getAll') + ->willReturn([ + 1 => [ + 'general-idp0_display_name' => 'My IdP', + 'idp-entityId' => 'https://idp.example.com', + 'idp-singleSignOnService.url' => 'https://idp.example.com/sso', + ], + ]); + + $result = $this->samlSettings->getListOfConfiguredIdps(); + + $this->assertSame([1 => 'My IdP'], $result); + } + + public function testGetListOfConfiguredIdpsFiltersOutPartiallyConfiguredIdps(): void { + $this->mapper->expects($this->once()) + ->method('getAll') + ->willReturn([ + 1 => [ + 'general-idp0_display_name' => 'Configured IdP', + 'idp-entityId' => 'https://idp.example.com', + 'idp-singleSignOnService.url' => 'https://idp.example.com/sso', + ], + 2 => [ + 'general-idp0_display_name' => 'Missing SSO URL', + 'idp-entityId' => 'https://idp2.example.com', + // missing idp-singleSignOnService.url + ], + 3 => [ + 'general-idp0_display_name' => 'Missing Entity ID', + // missing idp-entityId + 'idp-singleSignOnService.url' => 'https://idp3.example.com/sso', + ], + ]); + + $result = $this->samlSettings->getListOfConfiguredIdps(); + + $this->assertSame([1 => 'Configured IdP'], $result); + } + + public function testGetListOfConfiguredIdpsUsesDisplayNameAsValue(): void { + $this->mapper->expects($this->once()) + ->method('getAll') + ->willReturn([ + 1 => [ + 'idp-entityId' => 'https://idp.example.com', + 'idp-singleSignOnService.url' => 'https://idp.example.com/sso', + // no display name set + ], + ]); + + $result = $this->samlSettings->getListOfConfiguredIdps(); + + $this->assertSame([1 => ''], $result); + } +}